diff --git a/src/Main.cc b/src/Main.cc index 60642fd0..c10987cd 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -124,6 +124,10 @@ The actions are:\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\ + salvage-gci INPUT-FILENAME [--round2] [CRYPT-OPTION] [--bytes=SIZE]\n\ + Attempt to find either the round-1 or round-2 decryption seed for a\n\ + corrupted GCI file. If --round2 is given, then CRYPT-OPTION must be given\n\ + (and should specify either a valid system file or the round1 seed).\n\ find-decryption-seed \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\ @@ -205,6 +209,7 @@ enum class Behavior { ENCRYPT_GCI_SAVE, DECRYPT_GCI_SAVE, FIND_DECRYPTION_SEED, + SALVAGE_GCI, DECODE_QUEST_FILE, DECODE_SJIS, EXTRACT_GSL, @@ -232,6 +237,7 @@ static bool behavior_takes_input_filename(Behavior b) { (b == Behavior::DECRYPT_DATA) || (b == Behavior::DECRYPT_TRIVIAL_DATA) || (b == Behavior::DECRYPT_GCI_SAVE) || + (b == Behavior::SALVAGE_GCI) || (b == Behavior::ENCRYPT_GCI_SAVE) || (b == Behavior::DECODE_QUEST_FILE) || (b == Behavior::DECODE_SJIS) || @@ -279,7 +285,13 @@ int main(int argc, char** argv) { bool big_endian = false; bool skip_little_endian = false; bool skip_big_endian = false; + bool round2 = false; + bool skip_checksum = false; + uint64_t override_round2_seed = 0xFFFFFFFFFFFFFFFF; + size_t offset = 0; + size_t stride = 1; size_t num_threads = 0; + size_t bytes = 0; const char* find_decryption_seed_ciphertext = nullptr; vector find_decryption_seed_plaintexts; const char* input_filename = nullptr; @@ -309,8 +321,20 @@ int main(int argc, char** argv) { cli_version = GameVersion::XB; } else if (!strcmp(argv[x], "--bb")) { cli_version = GameVersion::BB; + } else if (!strcmp(argv[x], "--round2")) { + round2 = true; + } else if (!strncmp(argv[x], "--bytes=", 8)) { + bytes = strtoull(&argv[x][8], nullptr, 0); + } else if (!strncmp(argv[x], "--offset=", 9)) { + offset = strtoull(&argv[x][9], nullptr, 0); + } else if (!strncmp(argv[x], "--stride=", 9)) { + stride = strtoull(&argv[x][9], nullptr, 0); + } else if (!strcmp(argv[x], "--skip-checksum")) { + skip_checksum = true; } else if (!strncmp(argv[x], "--seed=", 7)) { seed = &argv[x][7]; + } else if (!strncmp(argv[x], "--round2-seed=", 14)) { + override_round2_seed = strtoull(&argv[x][14], nullptr, 16); } else if (!strncmp(argv[x], "--key=", 6)) { key_file_name = &argv[x][6]; } else if (!strncmp(argv[x], "--sys=", 6)) { @@ -370,6 +394,8 @@ int main(int argc, char** argv) { behavior = Behavior::ENCRYPT_GCI_SAVE; } else if (!strcmp(argv[x], "find-decryption-seed")) { behavior = Behavior::FIND_DECRYPTION_SEED; + } else if (!strcmp(argv[x], "salvage-gci")) { + behavior = Behavior::SALVAGE_GCI; } else if (!strcmp(argv[x], "decode-sjis")) { behavior = Behavior::DECODE_SJIS; } else if (!strcmp(argv[x], "decode-gci")) { @@ -661,7 +687,7 @@ int main(int argc, char** argv) { if (is_decrypt) { const void* data_section = r.getv(header.data_size); auto decrypted = decrypt_gci_fixed_size_file_data_section( - data_section, header.data_size, round1_seed); + data_section, header.data_size, round1_seed, skip_checksum, override_round2_seed); *reinterpret_cast(data.data() + data_start_offset) = decrypted; } else { const auto& s = r.get(); @@ -700,6 +726,101 @@ int main(int argc, char** argv) { break; } + case Behavior::SALVAGE_GCI: { + uint64_t likely_round1_seed = 0xFFFFFFFFFFFFFFFF; + if (system_filename) { + try { + string system_data = load_file(system_filename); + StringReader r(system_data); + const auto& header = r.get(); + header.check(); + const auto& system = r.get(); + likely_round1_seed = system.creation_timestamp; + log_info("System file appears to be in order; round1 seed is %08" PRIX64, likely_round1_seed); + } catch (const exception& e) { + log_warning("Cannot parse system file (%s); ignoring it", e.what()); + } + } else if (!seed.empty()) { + likely_round1_seed = stoul(seed, nullptr, 16); + log_info("Specified round1 seed is %08" PRIX64, likely_round1_seed); + } + + if (round2 && likely_round1_seed > 0x100000000) { + throw invalid_argument("cannot find round2 seed without known round1 seed"); + } + + auto data = read_input_data(); + StringReader r(data); + const auto& header = r.get(); + header.check(); + + const void* data_section = r.getv(header.data_size); + + auto process_file = [&]() { + vector> top_seeds_by_thread( + num_threads ? num_threads : thread::hardware_concurrency()); + parallel_range( + [&](uint64_t seed, size_t thread_num) -> bool { + size_t zero_count; + if (round2) { + string decrypted = decrypt_gci_fixed_size_file_data_section_for_salvage( + data_section, header.data_size, likely_round1_seed, seed, bytes); + zero_count = count_zeroes( + decrypted.data() + offset, + decrypted.size() - offset, + stride); + } else { + auto decrypted = decrypt_gci_fixed_size_file_data_section( + data_section, + header.data_size, + seed, + true); + zero_count = count_zeroes( + reinterpret_cast(&decrypted) + offset, + sizeof(decrypted) - offset, + stride); + } + auto& top_seeds = top_seeds_by_thread[thread_num]; + if (top_seeds.size() < 10 || (zero_count >= top_seeds.begin()->second)) { + top_seeds.emplace(zero_count, seed); + if (top_seeds.size() > 10) { + top_seeds.erase(top_seeds.begin()); + } + } + return false; + }, + 0, + 0x100000000, + num_threads); + + multimap top_seeds; + for (const auto& thread_top_seeds : top_seeds_by_thread) { + for (const auto& it : thread_top_seeds) { + top_seeds.emplace(it.first, it.second); + } + } + for (const auto& it : top_seeds) { + const char* sys_seed_str = (!round2 && (it.second == likely_round1_seed)) + ? " (this is the seed from the system file)" + : ""; + log_info("Round %c seed %08" PRIX32 " resulted in %zu zero bytes%s", + round2 ? '2' : '1', it.second, it.first, sys_seed_str); + } + }; + + if (header.data_size == sizeof(PSOGCGuildCardFile)) { + process_file.template operator()(); + } else if (header.is_ep12() && (header.data_size == sizeof(PSOGCCharacterFile))) { + process_file.template operator()(); + } else if (header.is_ep3() && (header.data_size == sizeof(PSOGCEp3CharacterFile))) { + process_file.template operator()(); + } else { + throw runtime_error("unrecognized save type"); + } + + 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"); diff --git a/src/SaveFileFormats.cc b/src/SaveFileFormats.cc index 8c70f5f6..9531e61e 100644 --- a/src/SaveFileFormats.cc +++ b/src/SaveFileFormats.cc @@ -70,8 +70,7 @@ void PSOGCIFileHeader::check() const { if (this->game_id[1] != 'P') { throw runtime_error("GCI file is not for Phantasy Star Online"); } - if ((this->game_id[1] != 'P') || - ((this->game_id[2] != 'S') && (this->game_id[2] != 'O'))) { + if ((this->game_id[2] != 'S') && (this->game_id[2] != 'O')) { throw runtime_error("GCI file is not for Phantasy Star Online"); } } @@ -101,3 +100,18 @@ uint32_t compute_psogc_timestamp( uint32_t res_day = (day - 1) + year_start_day + month_start_day[month - 1]; return second + (minute + (hour + (res_day * 24)) * 60) * 60; } + +string decrypt_gci_fixed_size_file_data_section_for_salvage( + const void* data_section, + size_t size, + uint32_t round1_seed, + uint64_t round2_seed, + size_t max_decrypt_bytes) { + string decrypted = decrypt_gci_or_vms_v2_data_section( + data_section, size, round1_seed, max_decrypt_bytes); + + PSOV2Encryption round2_crypt(round2_seed); + round2_crypt.encrypt_big_endian(decrypted.data(), decrypted.size()); + + return decrypted; +} diff --git a/src/SaveFileFormats.hh b/src/SaveFileFormats.hh index f6f4b4de..3063f7dd 100644 --- a/src/SaveFileFormats.hh +++ b/src/SaveFileFormats.hh @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -275,12 +276,17 @@ struct PSOGCGuildCardFile { template std::string decrypt_gci_or_vms_v2_data_section( - const void* data_section, size_t size, uint32_t round1_seed) { + const void* data_section, size_t size, uint32_t round1_seed, size_t max_decrypt_bytes = 0) { + if (max_decrypt_bytes == 0) { + max_decrypt_bytes = size; + } else { + max_decrypt_bytes = std::min(max_decrypt_bytes, size); + } - std::string decrypted(size, '\0'); + std::string decrypted(max_decrypt_bytes, '\0'); PSOV2Encryption shuf_crypt(round1_seed); ShuffleTables shuf(shuf_crypt); - shuf.shuffle(decrypted.data(), data_section, size, true); + shuf.shuffle(decrypted.data(), data_section, max_decrypt_bytes, true); size_t orig_size = decrypted.size(); decrypted.resize((decrypted.size() + 3) & (~3)); @@ -311,7 +317,11 @@ std::string encrypt_gci_or_vms_v2_data_section( template StructT decrypt_gci_fixed_size_file_data_section( - const void* data_section, size_t size, uint32_t round1_seed) { + const void* data_section, + size_t size, + uint32_t round1_seed, + bool skip_checksum = false, + uint64_t override_round2_seed = 0xFFFFFFFFFFFFFFFF) { std::string decrypted = decrypt_gci_or_vms_v2_data_section( data_section, size, round1_seed); @@ -320,22 +330,31 @@ StructT decrypt_gci_fixed_size_file_data_section( } StructT ret = *reinterpret_cast(decrypted.data()); - PSOV2Encryption round2_crypt(ret.round2_seed); + PSOV2Encryption round2_crypt(override_round2_seed < 0x100000000 ? override_round2_seed : ret.round2_seed.load()); round2_crypt.encrypt_big_endian(&ret, offsetof(StructT, round2_seed)); - uint32_t expected_crc = ret.checksum; - ret.checksum = 0; - uint32_t actual_crc = crc32(&ret, sizeof(ret)); - ret.checksum = expected_crc; - if (expected_crc != actual_crc) { - throw std::runtime_error(string_printf( - "incorrect decrypted data section checksum: expected %08" PRIX32 "; received %08" PRIX32, - expected_crc, actual_crc)); + if (!skip_checksum) { + uint32_t expected_crc = ret.checksum; + ret.checksum = 0; + uint32_t actual_crc = crc32(&ret, sizeof(ret)); + ret.checksum = expected_crc; + if (expected_crc != actual_crc) { + throw std::runtime_error(string_printf( + "incorrect decrypted data section checksum: expected %08" PRIX32 "; received %08" PRIX32, + expected_crc, actual_crc)); + } } return ret; } +std::string decrypt_gci_fixed_size_file_data_section_for_salvage( + const void* data_section, + size_t size, + uint32_t round1_seed, + uint64_t round2_seed, + size_t max_decrypt_bytes); + template std::string encrypt_gci_fixed_size_file_data_section( const StructT& s, uint32_t round1_seed) {