implement save file decryption/encryption

This commit is contained in:
Martin Michelsen
2023-03-31 23:56:25 -07:00
parent 06ba95ed97
commit 3b9a76eec8
39 changed files with 666 additions and 205 deletions
+108 -2
View File
@@ -24,6 +24,7 @@
#include "ProxyServer.hh"
#include "PSOGCObjectGraph.hh"
#include "ReplaySession.hh"
#include "SaveFileFormats.hh"
#include "SendCommands.hh"
#include "Server.hh"
#include "ServerShell.hh"
@@ -124,6 +125,12 @@ The actions are:\n\
trivial algorithm. If SEED is given, it should be specified as one hex\n\
byte. If SEED is not given, newserv will try all possible seeds and return\n\
the one that results in the greatest number of zero bytes in the output.\n\
encrypt-gci-save CRYPT-OPTION INPUT-FILENAME [OUTPUT-FILENAME]\n\
decrypt-gci-save CRYPT-OPTION INPUT-FILENAME [OUTPUT-FILENAME]\n\
Encrypt or decrypt a character or Guild Card file. If encrypting, the\n\
checksum is also recomputed and stored in the encrypted file. CRYPT-OPTION\n\
is required; it can be either --sys=SYSTEM-FILENAME or --seed=ROUND1-SEED\n\
(specified in hex).\n\
find-decryption-seed <OPTIONS...>\n\
Perform a brute-force search for a decryption seed of the given data. The\n\
ciphertext is specified with the --encrypted=DATA option and the expected\n\
@@ -197,6 +204,8 @@ enum class Behavior {
ENCRYPT_DATA,
DECRYPT_DATA,
DECRYPT_TRIVIAL_DATA,
ENCRYPT_GCI_SAVE,
DECRYPT_GCI_SAVE,
FIND_DECRYPTION_SEED,
DECODE_QUEST_FILE,
DECODE_SJIS,
@@ -220,6 +229,8 @@ static bool behavior_takes_input_filename(Behavior b) {
(b == Behavior::ENCRYPT_DATA) ||
(b == Behavior::DECRYPT_DATA) ||
(b == Behavior::DECRYPT_TRIVIAL_DATA) ||
(b == Behavior::DECRYPT_GCI_SAVE) ||
(b == Behavior::ENCRYPT_GCI_SAVE) ||
(b == Behavior::DECODE_QUEST_FILE) ||
(b == Behavior::DECODE_SJIS) ||
(b == Behavior::FORMAT_ITEMRT_ENTRY) ||
@@ -239,6 +250,8 @@ static bool behavior_takes_output_filename(Behavior b) {
(b == Behavior::ENCRYPT_DATA) ||
(b == Behavior::DECRYPT_DATA) ||
(b == Behavior::DECRYPT_TRIVIAL_DATA) ||
(b == Behavior::DECRYPT_GCI_SAVE) ||
(b == Behavior::ENCRYPT_GCI_SAVE) ||
(b == Behavior::DECODE_SJIS) ||
(b == Behavior::EXTRACT_GSL) ||
(b == Behavior::EXTRACT_BML);
@@ -267,6 +280,7 @@ int main(int argc, char** argv) {
vector<const char*> find_decryption_seed_plaintexts;
const char* input_filename = nullptr;
const char* output_filename = nullptr;
const char* system_filename = nullptr;
const char* replay_required_access_key = "";
const char* replay_required_password = "";
uint32_t root_object_address = 0;
@@ -291,6 +305,8 @@ int main(int argc, char** argv) {
cli_version = GameVersion::BB;
} else if (!strncmp(argv[x], "--seed=", 7)) {
seed = &argv[x][7];
} else if (!strncmp(argv[x], "--sys=", 6)) {
system_filename = &argv[x][6];
} else if (!strncmp(argv[x], "--key=", 6)) {
key_file_name = &argv[x][6];
} else if (!strncmp(argv[x], "--encrypted=", 12)) {
@@ -337,6 +353,10 @@ int main(int argc, char** argv) {
behavior = Behavior::DECRYPT_DATA;
} else if (!strcmp(argv[x], "decrypt-trivial-data")) {
behavior = Behavior::DECRYPT_TRIVIAL_DATA;
} else if (!strcmp(argv[x], "decrypt-gci-save")) {
behavior = Behavior::DECRYPT_GCI_SAVE;
} else if (!strcmp(argv[x], "encrypt-gci-save")) {
behavior = Behavior::ENCRYPT_GCI_SAVE;
} else if (!strcmp(argv[x], "find-decryption-seed")) {
behavior = Behavior::FIND_DECRYPTION_SEED;
} else if (!strcmp(argv[x], "decode-sjis")) {
@@ -408,13 +428,29 @@ int main(int argc, char** argv) {
} else if (!output_filename && input_filename && strcmp(input_filename, "-")) {
string filename = input_filename;
if (behavior == Behavior::COMPRESS_PRS) {
if (ends_with(filename, ".bind") || ends_with(filename, ".datd") || ends_with(filename, ".mnmd")) {
if (ends_with(filename, ".bind") ||
ends_with(filename, ".datd") ||
ends_with(filename, ".mnmd")) {
filename.resize(filename.size() - 1);
} else {
filename += ".prs";
}
} else if (behavior == Behavior::DECOMPRESS_PRS) {
if (ends_with(filename, ".bin") || ends_with(filename, ".dat") || ends_with(filename, ".mnm")) {
if (ends_with(filename, ".bin") ||
ends_with(filename, ".dat") ||
ends_with(filename, ".mnm")) {
filename += "d";
} else {
filename += ".dec";
}
} else if (behavior == Behavior::ENCRYPT_GCI_SAVE) {
if (ends_with(filename, ".gcid")) {
filename.resize(filename.size() - 1);
} else {
filename += ".gci";
}
} else if (behavior == Behavior::DECRYPT_GCI_SAVE) {
if (ends_with(filename, ".gci")) {
filename += "d";
} else {
filename += ".dec";
@@ -575,6 +611,76 @@ int main(int argc, char** argv) {
break;
}
case Behavior::ENCRYPT_GCI_SAVE:
case Behavior::DECRYPT_GCI_SAVE: {
uint32_t round1_seed;
if (system_filename) {
string system_data = load_file(system_filename);
StringReader r(system_data);
const auto& header = r.get<PSOGCIFileHeader>();
header.check();
const auto& system = r.get<PSOGCSystemFile>();
round1_seed = system.creation_internet_time;
} else if (!seed.empty()) {
round1_seed = stoul(seed, nullptr, 16);
} else {
// TODO: We can support brute-forcing character file encryption, but I'm
// lazy and this would probably not be useful for anyone.
throw runtime_error("either --sys or --seed must be given");
}
bool is_decrypt = (behavior == Behavior::DECRYPT_GCI_SAVE);
auto data = read_input_data();
StringReader r(data);
const auto& header = r.get<PSOGCIFileHeader>();
header.check();
size_t data_start_offset = r.where();
auto process_file = [&]<typename StructT>() {
if (is_decrypt) {
const void* data_section = r.getv(header.data_size);
auto decrypted = decrypt_gci_fixed_size_file_data_section<StructT>(
data_section, header.data_size, round1_seed);
*reinterpret_cast<StructT*>(data.data() + data_start_offset) = decrypted;
} else {
const auto& s = r.get<StructT>();
auto encrypted = encrypt_gci_fixed_size_file_data_section<StructT>(
s, round1_seed);
if (data_start_offset + encrypted.size() > data.size()) {
throw runtime_error("encrypted result exceeds file size");
}
memcpy(data.data() + data_start_offset, encrypted.data(), encrypted.size());
}
};
if (header.data_size == sizeof(PSOGCGuildCardFile)) {
process_file.template operator()<PSOGCGuildCardFile>();
} else if (header.is_ep12() && (header.data_size == sizeof(PSOGCCharacterFile))) {
process_file.template operator()<PSOGCCharacterFile>();
} else if (header.is_ep3() && (header.data_size == sizeof(PSOGCEp3CharacterFile))) {
auto* charfile = reinterpret_cast<PSOGCEp3CharacterFile*>(data.data() + data_start_offset);
if (!is_decrypt) {
for (size_t z = 0; z < charfile->characters.size(); z++) {
charfile->characters[z].ep3_config.encrypt(random_object<uint8_t>());
}
}
process_file.template operator()<PSOGCEp3CharacterFile>();
if (is_decrypt) {
for (size_t z = 0; z < charfile->characters.size(); z++) {
charfile->characters[z].ep3_config.decrypt();
}
}
} else {
throw runtime_error("unrecognized save type");
}
write_output_data(data.data(), data.size());
break;
}
case Behavior::FIND_DECRYPTION_SEED: {
if (find_decryption_seed_plaintexts.empty() || !find_decryption_seed_ciphertext) {
throw runtime_error("both --encrypted and --decrypted must be specified");