diff --git a/CMakeLists.txt b/CMakeLists.txt index 8916e209..c60315bd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -144,11 +144,14 @@ add_test( NAME "decode-vms" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${CMAKE_SOURCE_DIR}/tests/test-decode-vms.sh ${CMAKE_BINARY_DIR}/newserv) - add_test( NAME "decode-gci" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${CMAKE_SOURCE_DIR}/tests/test-decode-gci.sh ${CMAKE_BINARY_DIR}/newserv) +add_test( + NAME "decode-pc" + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMAND ${CMAKE_SOURCE_DIR}/tests/test-decode-pc.sh ${CMAKE_BINARY_DIR}/newserv) # Installation configuration diff --git a/TODO.md b/TODO.md index 6cb0aca9..944f541c 100644 --- a/TODO.md +++ b/TODO.md @@ -13,6 +13,8 @@ - Implement per-game logging - Add default values in all command structures (like we use for Episode 3 battle commands) - Check for RCE potential in 6x6B-6x6E commands +- Fix symbol chat header (including face_spec) across PC/GC boundary +- Check size of name field in GuildCardPC ## Episode 3 diff --git a/src/Main.cc b/src/Main.cc index 3145470b..bd5c67bb 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -271,10 +271,15 @@ enum class Behavior { DECRYPT_DATA, ENCRYPT_TRIVIAL_DATA, DECRYPT_TRIVIAL_DATA, + DECRYPT_REGISTRY_VALUE, ENCRYPT_CHALLENGE_DATA, DECRYPT_CHALLENGE_DATA, ENCRYPT_GCI_SAVE, DECRYPT_GCI_SAVE, + ENCRYPT_PC_SAVE, + DECRYPT_PC_SAVE, + ENCRYPT_SAVE_DATA, + DECRYPT_SAVE_DATA, DECODE_GCI_SNAPSHOT, ENCODE_GVM, FIND_DECRYPTION_SEED, @@ -315,13 +320,18 @@ static bool behavior_takes_input_filename(Behavior b) { (b == Behavior::DECRYPT_DATA) || (b == Behavior::ENCRYPT_TRIVIAL_DATA) || (b == Behavior::DECRYPT_TRIVIAL_DATA) || + (b == Behavior::DECRYPT_REGISTRY_VALUE) || (b == Behavior::ENCRYPT_CHALLENGE_DATA) || (b == Behavior::DECRYPT_CHALLENGE_DATA) || + (b == Behavior::ENCRYPT_GCI_SAVE) || (b == Behavior::DECRYPT_GCI_SAVE) || + (b == Behavior::ENCRYPT_PC_SAVE) || + (b == Behavior::DECRYPT_PC_SAVE) || + (b == Behavior::ENCRYPT_SAVE_DATA) || + (b == Behavior::DECRYPT_SAVE_DATA) || (b == Behavior::DECODE_GCI_SNAPSHOT) || (b == Behavior::ENCODE_GVM) || (b == Behavior::SALVAGE_GCI) || - (b == Behavior::ENCRYPT_GCI_SAVE) || (b == Behavior::DECODE_QUEST_FILE) || (b == Behavior::ENCODE_QST) || (b == Behavior::DISASSEMBLE_QUEST_SCRIPT) || @@ -349,10 +359,15 @@ static bool behavior_takes_output_filename(Behavior b) { (b == Behavior::DECRYPT_DATA) || (b == Behavior::ENCRYPT_TRIVIAL_DATA) || (b == Behavior::DECRYPT_TRIVIAL_DATA) || + (b == Behavior::DECRYPT_REGISTRY_VALUE) || (b == Behavior::ENCRYPT_CHALLENGE_DATA) || (b == Behavior::DECRYPT_CHALLENGE_DATA) || - (b == Behavior::DECRYPT_GCI_SAVE) || (b == Behavior::ENCRYPT_GCI_SAVE) || + (b == Behavior::DECRYPT_GCI_SAVE) || + (b == Behavior::ENCRYPT_PC_SAVE) || + (b == Behavior::DECRYPT_PC_SAVE) || + (b == Behavior::ENCRYPT_SAVE_DATA) || + (b == Behavior::DECRYPT_SAVE_DATA) || (b == Behavior::DECODE_GCI_SNAPSHOT) || (b == Behavior::ENCODE_GVM) || (b == Behavior::ENCODE_QST) || @@ -520,6 +535,8 @@ int main(int argc, char** argv) { behavior = Behavior::ENCRYPT_TRIVIAL_DATA; } else if (!strcmp(argv[x], "decrypt-trivial-data")) { behavior = Behavior::DECRYPT_TRIVIAL_DATA; + } else if (!strcmp(argv[x], "decrypt-registry-value")) { + behavior = Behavior::DECRYPT_REGISTRY_VALUE; } else if (!strcmp(argv[x], "encrypt-challenge-data")) { behavior = Behavior::ENCRYPT_CHALLENGE_DATA; } else if (!strcmp(argv[x], "decrypt-challenge-data")) { @@ -528,6 +545,14 @@ int main(int argc, char** argv) { behavior = Behavior::DECRYPT_GCI_SAVE; } else if (!strcmp(argv[x], "encrypt-gci-save")) { behavior = Behavior::ENCRYPT_GCI_SAVE; + } else if (!strcmp(argv[x], "decrypt-pc-save")) { + behavior = Behavior::DECRYPT_PC_SAVE; + } else if (!strcmp(argv[x], "encrypt-pc-save")) { + behavior = Behavior::ENCRYPT_PC_SAVE; + } else if (!strcmp(argv[x], "decrypt-save-data")) { + behavior = Behavior::DECRYPT_SAVE_DATA; + } else if (!strcmp(argv[x], "encrypt-save-data")) { + behavior = Behavior::ENCRYPT_SAVE_DATA; } else if (!strcmp(argv[x], "decode-gci-snapshot")) { behavior = Behavior::DECODE_GCI_SNAPSHOT; } else if (!strcmp(argv[x], "encode-gvm")) { @@ -919,6 +944,13 @@ int main(int argc, char** argv) { break; } + case Behavior::DECRYPT_REGISTRY_VALUE: { + string data = read_input_data(); + string out_data = decrypt_v2_registry_value(data.data(), data.size()); + write_output_data(out_data.data(), out_data.size()); + break; + } + case Behavior::ENCRYPT_CHALLENGE_DATA: case Behavior::DECRYPT_CHALLENGE_DATA: { string data = read_input_data(); @@ -957,12 +989,12 @@ int main(int argc, char** argv) { auto process_file = [&]() { if (is_decrypt) { const void* data_section = r.getv(header.data_size); - auto decrypted = decrypt_gci_fixed_size_file_data_section( + auto decrypted = decrypt_fixed_size_data_section_t( 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(); - auto encrypted = encrypt_gci_fixed_size_file_data_section( + auto encrypted = encrypt_fixed_size_data_section_t( s, round1_seed); if (data_start_offset + encrypted.size() > data.size()) { throw runtime_error("encrypted result exceeds file size"); @@ -997,6 +1029,89 @@ int main(int argc, char** argv) { break; } + case Behavior::ENCRYPT_PC_SAVE: + case Behavior::DECRYPT_PC_SAVE: { + if (seed.empty()) { + throw runtime_error("--seed must be given to specify the serial number"); + } + uint32_t round1_seed = stoul(seed, nullptr, 16); + + bool is_decrypt = (behavior == Behavior::DECRYPT_PC_SAVE); + + auto data = read_input_data(); + if (data.size() == sizeof(PSOPCGuildCardFile)) { + if (is_decrypt) { + data = decrypt_fixed_size_data_section_s( + data.data(), offsetof(PSOPCGuildCardFile, end_padding), round1_seed, skip_checksum, override_round2_seed); + } else { + data = encrypt_fixed_size_data_section_s( + data.data(), offsetof(PSOPCGuildCardFile, end_padding), round1_seed); + } + data.resize((sizeof(PSOPCGuildCardFile) + 0x1FF) & (~0x1FF), '\0'); + } else if (data.size() == sizeof(PSOPCCharacterFile)) { + PSOPCCharacterFile* charfile = reinterpret_cast(data.data()); + if (is_decrypt) { + for (size_t z = 0; z < charfile->entries.size(); z++) { + if (charfile->entries[z].present) { + try { + charfile->entries[z].character = decrypt_fixed_size_data_section_t( + &charfile->entries[z].character, sizeof(charfile->entries[z].character), round1_seed, skip_checksum, override_round2_seed); + } catch (const exception& e) { + fprintf(stderr, "warning: cannot decrypt character %zu: %s\n", z, e.what()); + } + } + } + } else { + for (size_t z = 0; z < charfile->entries.size(); z++) { + if (charfile->entries[z].present) { + string encrypted = encrypt_fixed_size_data_section_t( + charfile->entries[z].character, round1_seed); + if (encrypted.size() != sizeof(PSOPCCharacterFile::CharacterEntry::Character)) { + throw logic_error("incorrect encrypted result size"); + } + charfile->entries[z].character = *reinterpret_cast(encrypted.data()); + } + } + } + } else if (data.size() == sizeof(PSOPCCreationTimeFile)) { + throw runtime_error("the PSO______FLS file is not encrypted; it is just random data"); + } else if (data.size() == sizeof(PSOPCSystemFile)) { + throw runtime_error("the PSO______COM file is not encrypted"); + } else { + throw runtime_error("unknown save file type"); + } + + write_output_data(data.data(), data.size()); + break; + } + + case Behavior::ENCRYPT_SAVE_DATA: + case Behavior::DECRYPT_SAVE_DATA: { + if (seed.empty()) { + throw runtime_error("--seed must be given to specify the round1 seed"); + } + uint32_t round1_seed = stoul(seed, nullptr, 16); + + bool is_decrypt = (behavior == Behavior::DECRYPT_SAVE_DATA); + + auto data = read_input_data(); + StringReader r(data); + + string output_data; + size_t effective_size = bytes ? min(bytes, data.size()) : data.size(); + if (is_decrypt) { + output_data = big_endian + ? decrypt_fixed_size_data_section_s(data.data(), effective_size, round1_seed, skip_checksum, override_round2_seed) + : decrypt_fixed_size_data_section_s(data.data(), effective_size, round1_seed, skip_checksum, override_round2_seed); + } else { + output_data = big_endian + ? encrypt_fixed_size_data_section_s(data.data(), effective_size, round1_seed) + : encrypt_fixed_size_data_section_s(data.data(), effective_size, round1_seed); + } + write_output_data(output_data.data(), output_data.size()); + break; + } + case Behavior::DECODE_GCI_SNAPSHOT: { auto data = read_input_data(); StringReader r(data); @@ -1066,14 +1181,14 @@ int main(int argc, char** argv) { [&](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( + string decrypted = decrypt_gci_fixed_size_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( + auto decrypted = decrypt_fixed_size_data_section_t( data_section, header.data_size, seed, diff --git a/src/PSOEncryption.cc b/src/PSOEncryption.cc index d6cc2b03..f2444be3 100644 --- a/src/PSOEncryption.cc +++ b/src/PSOEncryption.cc @@ -40,36 +40,44 @@ template void PSOLFGEncryption::encrypt_t(void* vdata, size_t size, bool advance) { using U32T = typename std::conditional::type; - if (size & 3) { - throw invalid_argument("size must be a multiple of 4"); - } if (!advance && (size != 4)) { throw logic_error("cannot peek-encrypt/decrypt with size > 4"); } - size >>= 2; + size_t uint32_count = size >> 2; + size_t extra_bytes = size & 3; U32T* data = reinterpret_cast(vdata); - for (size_t x = 0; x < size; x++) { + for (size_t x = 0; x < uint32_count; x++) { data[x] ^= this->next(advance); } + if (extra_bytes) { + U32T last = 0; + memcpy(&last, &data[uint32_count], extra_bytes); + last ^= this->next(advance); + memcpy(&data[uint32_count], &last, extra_bytes); + } } template void PSOLFGEncryption::encrypt_minus_t(void* vdata, size_t size, bool advance) { using U32T = typename std::conditional::type; - if (size & 3) { - throw invalid_argument("size must be a multiple of 4"); - } if (!advance && (size != 4)) { throw logic_error("cannot peek-encrypt/decrypt with size > 4"); } - size >>= 2; + size_t uint32_count = size >> 2; + size_t extra_bytes = size & 3; U32T* data = reinterpret_cast(vdata); - for (size_t x = 0; x < size; x++) { + for (size_t x = 0; x < uint32_count; x++) { data[x] = this->next(advance) - data[x]; } + if (extra_bytes) { + U32T last = 0; + memcpy(&last, &data[uint32_count], extra_bytes); + last = this->next(advance) - last; + memcpy(&data[uint32_count], &last, extra_bytes); + } } void PSOLFGEncryption::encrypt(void* vdata, size_t size, bool advance) { @@ -975,3 +983,12 @@ std::u16string decrypt_challenge_rank_text(const std::u16string& data) { std::u16string encrypt_challenge_rank_text(const std::u16string& data) { return encrypt_challenge_rank_text(data.data(), data.size()); } + +string decrypt_v2_registry_value(const void* data, size_t size) { + string ret(reinterpret_cast(data), size); + PSOV2Encryption crypt(0x66); + for (size_t z = 0; z < size; z++) { + ret[z] ^= (crypt.next() & 0x7F); + } + return ret; +} diff --git a/src/PSOEncryption.hh b/src/PSOEncryption.hh index 66141f89..fcadc9be 100644 --- a/src/PSOEncryption.hh +++ b/src/PSOEncryption.hh @@ -272,3 +272,5 @@ template std::u16string encrypt_challenge_rank_text(const ptext& data) { return encrypt_challenge_rank_text(data.data(), data.size()); } + +std::string decrypt_v2_registry_value(const void* data, size_t size); diff --git a/src/PlayerSubordinates.hh b/src/PlayerSubordinates.hh index ad7f01ed..f3f45726 100644 --- a/src/PlayerSubordinates.hh +++ b/src/PlayerSubordinates.hh @@ -174,6 +174,19 @@ struct PlayerDispDataBB { void apply_dressing_room(const PlayerDispDataBBPreview&); } __attribute__((packed)); +struct GuildCardPC { + /* 00 */ le_uint32_t player_tag = 0; + /* 04 */ le_uint32_t guild_card_number = 0; + // TODO: Is the length of the name field correct here? + /* 08 */ ptext name; + /* 38 */ ptext description; + /* EC */ uint8_t present = 0; + /* ED */ uint8_t language = 0; + /* EE */ uint8_t section_id = 0; + /* EF */ uint8_t char_class = 0; + /* F0 */ +} __attribute__((packed)); + // TODO: Is this the same for XB as it is for GC? (This struct is based on the // GC format) struct GuildCardV3 { diff --git a/src/Quest.cc b/src/Quest.cc index 10187f17..7da2270b 100644 --- a/src/Quest.cc +++ b/src/Quest.cc @@ -83,10 +83,8 @@ struct PSOGCIDLQFileEncryptedHeader : PSOMemCardDLQFileEncryptedHeader { } __attribute__((packed)); template -string decrypt_gci_or_vms_v2_download_quest_data_section( - const void* data_section, size_t size, uint32_t seed) { - string decrypted = decrypt_gci_or_vms_v2_data_section( - data_section, size, seed); +string decrypt_download_quest_data_section(const void* data_section, size_t size, uint32_t seed) { + string decrypted = decrypt_data_section(data_section, size, seed); size_t orig_size = decrypted.size(); decrypted.resize((decrypted.size() + 3) & (~3)); @@ -161,13 +159,13 @@ string decrypt_vms_v1_data_section(const void* data_section, size_t size) { } template -string find_seed_and_decrypt_gci_or_vms_v2_download_quest_data_section( +string find_seed_and_decrypt_download_quest_data_section( const void* data_section, size_t size, size_t num_threads) { mutex result_lock; string result; uint64_t result_seed = parallel_range([&](uint64_t seed, size_t) { try { - string ret = decrypt_gci_or_vms_v2_download_quest_data_section( + string ret = decrypt_download_quest_data_section( data_section, size, seed); lock_guard g(result_lock); result = std::move(ret); @@ -530,11 +528,11 @@ string Quest::decode_gci_file( // fields, so assume it's encrypted if any of them are nonzero. if (dlq_header.round2_seed || dlq_header.checksum || dlq_header.round3_seed) { if (known_seed >= 0) { - return decrypt_gci_or_vms_v2_download_quest_data_section( + return decrypt_download_quest_data_section( r.getv(header.data_size), header.data_size, known_seed); } else if (header.embedded_seed != 0) { - return decrypt_gci_or_vms_v2_download_quest_data_section( + return decrypt_download_quest_data_section( r.getv(header.data_size), header.data_size, header.embedded_seed); } else { @@ -544,7 +542,7 @@ string Quest::decode_gci_file( if (find_seed_num_threads == 0) { find_seed_num_threads = thread::hardware_concurrency(); } - return find_seed_and_decrypt_gci_or_vms_v2_download_quest_data_section( + return find_seed_and_decrypt_download_quest_data_section( r.getv(header.data_size), header.data_size, find_seed_num_threads); } @@ -617,7 +615,7 @@ string Quest::decode_vms_file( } if (known_seed >= 0) { - return decrypt_gci_or_vms_v2_download_quest_data_section( + return decrypt_download_quest_data_section( data_section, header.data_size, known_seed); } else { @@ -627,7 +625,7 @@ string Quest::decode_vms_file( if (find_seed_num_threads == 0) { find_seed_num_threads = thread::hardware_concurrency(); } - return find_seed_and_decrypt_gci_or_vms_v2_download_quest_data_section( + return find_seed_and_decrypt_download_quest_data_section( data_section, header.data_size, find_seed_num_threads); } } diff --git a/src/SaveFileFormats.cc b/src/SaveFileFormats.cc index 64f074d9..19d9bd50 100644 --- a/src/SaveFileFormats.cc +++ b/src/SaveFileFormats.cc @@ -101,14 +101,13 @@ uint32_t compute_psogc_timestamp( return second + (minute + (hour + (res_day * 24)) * 60) * 60; } -string decrypt_gci_fixed_size_file_data_section_for_salvage( +string decrypt_gci_fixed_size_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); + string decrypted = decrypt_data_section(data_section, size, round1_seed, max_decrypt_bytes); PSOV2Encryption round2_crypt(round2_seed); round2_crypt.encrypt_big_endian(decrypted.data(), decrypted.size()); diff --git a/src/SaveFileFormats.hh b/src/SaveFileFormats.hh index 8bf430a0..3a937ce9 100644 --- a/src/SaveFileFormats.hh +++ b/src/SaveFileFormats.hh @@ -117,6 +117,27 @@ struct PSOGCSaveFileSymbolChatEntry { /* 58 */ } __attribute__((packed)); +struct PSOPCSaveFileSymbolChatEntry { + /* 00 */ le_uint32_t present; + /* 04 */ ptext name; + /* 34 */ uint8_t face_spec; + /* 35 */ uint8_t flags; + /* 36 */ be_uint16_t unused; + struct CornerObject { + uint8_t type; + uint8_t flags_color; + } __attribute__((packed)); + /* 38 */ parray corner_objects; + struct FacePart { + uint8_t type; + uint8_t x; + uint8_t y; + uint8_t flags; + } __attribute__((packed)); + /* 40 */ parray face_parts; + /* 70 */ +} __attribute__((packed)); + struct PSOGCSaveFileChatShortcutEntry { /* 00 */ be_uint32_t present_type; /* 04 */ parray definition; @@ -298,8 +319,7 @@ struct PSOGCSnapshotFile { } __attribute__((packed)); template -std::string decrypt_gci_or_vms_v2_data_section( - const void* data_section, size_t size, uint32_t round1_seed, size_t max_decrypt_bytes = 0) { +std::string decrypt_data_section(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 { @@ -322,8 +342,7 @@ std::string decrypt_gci_or_vms_v2_data_section( } template -std::string encrypt_gci_or_vms_v2_data_section( - const void* data_section, size_t size, uint32_t round1_seed) { +std::string encrypt_data_section(const void* data_section, size_t size, uint32_t round1_seed) { std::string encrypted(reinterpret_cast(data_section), size); encrypted.resize((encrypted.size() + 3) & (~3)); @@ -338,23 +357,66 @@ std::string encrypt_gci_or_vms_v2_data_section( return ret; } -template -StructT decrypt_gci_fixed_size_file_data_section( +template +std::string decrypt_fixed_size_data_section_s( 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); + using U32T = std::conditional_t; + if (size < 2 * sizeof(U32T)) { + throw runtime_error("data size is too small"); + } + std::string decrypted = decrypt_data_section(data_section, size, round1_seed); + + uint32_t round2_seed = override_round2_seed < 0x100000000 + ? static_cast(override_round2_seed) + : reinterpret_cast(decrypted.data() + decrypted.size() - sizeof(U32T))->load(); + PSOV2Encryption round2_crypt(round2_seed); + if (IsBigEndian) { + round2_crypt.encrypt_big_endian(decrypted.data(), decrypted.size() - sizeof(U32T)); + } else { + round2_crypt.encrypt(decrypted.data(), decrypted.size() - sizeof(U32T)); + } + + if (!skip_checksum) { + U32T& checksum = *reinterpret_cast(decrypted.data()); + uint32_t expected_crc = checksum; + checksum = 0; + uint32_t actual_crc = crc32(decrypted.data(), decrypted.size()); + 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 decrypted; +} + +template +StructT decrypt_fixed_size_data_section_t( + 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_data_section(data_section, size, round1_seed); if (decrypted.size() < sizeof(StructT)) { throw std::runtime_error("file too small for structure"); } StructT ret = *reinterpret_cast(decrypted.data()); PSOV2Encryption round2_crypt(override_round2_seed < 0x100000000 ? override_round2_seed : ret.round2_seed.load()); - round2_crypt.encrypt_big_endian(&ret, offsetof(StructT, round2_seed)); + if (IsBigEndian) { + round2_crypt.encrypt_big_endian(&ret, offsetof(StructT, round2_seed)); + } else { + round2_crypt.encrypt(&ret, offsetof(StructT, round2_seed)); + } if (!skip_checksum) { uint32_t expected_crc = ret.checksum; @@ -371,28 +433,55 @@ StructT decrypt_gci_fixed_size_file_data_section( 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_fixed_size_data_section_s(const void* data, size_t size, uint32_t round1_seed) { + using U32T = std::conditional_t; -template -std::string encrypt_gci_fixed_size_file_data_section( - const StructT& s, uint32_t round1_seed) { + if (size < 2 * sizeof(U32T)) { + throw runtime_error("data size is too small"); + } + + uint32_t round2_seed = random_object(); + + string encrypted(reinterpret_cast(data), size); + *reinterpret_cast(encrypted.data()) = 0; + *reinterpret_cast(encrypted.data() + encrypted.size() - sizeof(U32T)) = round2_seed; + *reinterpret_cast(encrypted.data()) = crc32(encrypted.data(), encrypted.size()); + + PSOV2Encryption round2_crypt(round2_seed); + if (IsBigEndian) { + round2_crypt.encrypt_big_endian(encrypted.data(), encrypted.size()); + } else { + round2_crypt.encrypt(encrypted.data(), encrypted.size()); + } + + return encrypt_data_section(encrypted.data(), encrypted.size(), round1_seed); +} + +template +std::string encrypt_fixed_size_data_section_t(const StructT& s, uint32_t round1_seed) { StructT encrypted = s; encrypted.checksum = 0; encrypted.round2_seed = random_object(); encrypted.checksum = crc32(&encrypted, sizeof(encrypted)); PSOV2Encryption round2_crypt(encrypted.round2_seed); - round2_crypt.encrypt_big_endian(&encrypted, offsetof(StructT, round2_seed)); + if (IsBigEndian) { + round2_crypt.encrypt_big_endian(&encrypted, offsetof(StructT, round2_seed)); + } else { + round2_crypt.encrypt(&encrypted, offsetof(StructT, round2_seed)); + } - return encrypt_gci_or_vms_v2_data_section( - &encrypted, sizeof(StructT), round1_seed); + return encrypt_data_section(&encrypted, sizeof(StructT), round1_seed); } +std::string decrypt_gci_fixed_size_data_section_for_salvage( + const void* data_section, + size_t size, + uint32_t round1_seed, + uint64_t round2_seed, + size_t max_decrypt_bytes); + uint32_t compute_psogc_timestamp( uint16_t year, uint8_t month, @@ -400,3 +489,80 @@ uint32_t compute_psogc_timestamp( uint8_t hour, uint8_t minute, uint8_t second); + +struct PSOPCCreationTimeFile { // PSO______FLS + // The game creates this file if necessary and fills it with random data. + // Most of the random data appears to be a decoy; only one field is used. + // As in other PSO versions, creation_timestamp is used as an encryption key + // for the other save files, but only if the serial number isn't set in the + // Windows registry. + /* 0000 */ parray unused1; + /* 0624 */ le_uint32_t creation_timestamp; + /* 0628 */ parray unused2; + /* 1400 */ +} __attribute__((packed)); + +struct PSOPCSystemFile { // PSO_____COM + /* 0000 */ le_uint32_t checksum; + // Most of these fields are guesses based on the format used in GC and the + // assumption that Sega didn't change much between versions. + /* 0004 */ le_int16_t music_volume; + /* 0006 */ int8_t sound_volume; + /* 0007 */ uint8_t language; + /* 0008 */ le_uint32_t unknown_a3; + /* 000C */ parray unknown_a4; // Last one is always 0x1234? + /* 002C */ parray event_flags; + /* 012C */ le_uint32_t round1_seed; + /* 0130 */ parray end_padding; + /* 0200 */ +} __attribute__((packed)); + +struct PSOPCGuildCardFile { // PSO______GUD + /* 0000 */ le_uint32_t checksum; + // TODO: Figure out the PC guild card format. + /* 0004 */ parray unknown_a1; + /* 7984 */ le_uint32_t creation_timestamp; + /* 7988 */ le_uint32_t round2_seed; + /* 798C */ parray end_padding; + /* 7A00 */ +} __attribute__((packed)); + +struct PSOPCCharacterFile { // PSO______SYS and PSO______SYD + /* 00000 */ le_uint32_t signature; // 'CAEN' (stored as 4E 45 41 43) + /* 00004 */ le_uint32_t extra_headers; // 1 + /* 00008 */ le_uint32_t num_entries; // 0x80 + /* 0000C */ le_uint32_t entry_size; // 0x1D54 (actual entry size is +0x40) + /* 00010 */ parray unknown_a1; + struct CharacterEntry { + /* 0000 */ le_uint32_t present; // 1 if character present, 0 if empty + struct Character { + /* 0000 */ le_uint32_t checksum; + /* 0004 */ PlayerInventory inventory; + /* 0350 */ PlayerDispDataDCPCV3 disp; + /* 0420 */ be_uint32_t unknown_a1; + /* 0424 */ be_uint32_t creation_timestamp; + /* 0428 */ be_uint32_t signature; // == 0x6C5D889E? + /* 042C */ be_uint32_t play_time_seconds; + /* 0430 */ be_uint32_t option_flags; // TODO: document bits in this field + /* 0434 */ be_uint32_t save_count; + // TODO: Figure out what this is. On GC, this is where the bank data goes. + /* 0438 */ parray unknown_a2; + /* 0C0C */ GuildCardPC guild_card; + /* 0CFC */ parray symbol_chats; + // TODO: Figure out what this is. On GC, this is where chat shortcuts and + // challenge/battle records go. + /* 123C */ parray unknown_a3; + /* 1CDC */ parray tech_menu_shortcut_entries; + /* 1D04 */ parray unknown_a4; + /* 1D30 */ ptext serial_number; // As %08X (not decimal) + /* 1D40 */ ptext access_key; // As decimal + /* 1D50 */ le_uint32_t round2_seed; + /* 1D54 */ + } __attribute__((packed)); + /* 0004 */ Character character; + /* 1D58 */ parray unused; + /* 1D94 */ + } __attribute__((packed)); + /* 00440 */ parray entries; + /* ECE40 */ +} __attribute__((packed)); diff --git a/tests/vms/lionel-v1.dec b/tests/saves/lionel-v1.dec similarity index 100% rename from tests/vms/lionel-v1.dec rename to tests/saves/lionel-v1.dec diff --git a/tests/vms/lionel-v1.vms b/tests/saves/lionel-v1.vms similarity index 100% rename from tests/vms/lionel-v1.vms rename to tests/saves/lionel-v1.vms diff --git a/tests/vms/lionel-v2.dec b/tests/saves/lionel-v2.dec similarity index 100% rename from tests/vms/lionel-v2.dec rename to tests/saves/lionel-v2.dec diff --git a/tests/vms/lionel-v2.vms b/tests/saves/lionel-v2.vms similarity index 100% rename from tests/vms/lionel-v2.vms rename to tests/saves/lionel-v2.vms diff --git a/tests/saves/pc_gud.bin b/tests/saves/pc_gud.bin new file mode 100644 index 00000000..71f5d658 Binary files /dev/null and b/tests/saves/pc_gud.bin differ diff --git a/tests/saves/pc_gud.dec b/tests/saves/pc_gud.dec new file mode 100755 index 00000000..f765afa3 Binary files /dev/null and b/tests/saves/pc_gud.dec differ diff --git a/tests/saves/pc_sys.bin b/tests/saves/pc_sys.bin new file mode 100644 index 00000000..9470b420 Binary files /dev/null and b/tests/saves/pc_sys.bin differ diff --git a/tests/saves/pc_sys.dec b/tests/saves/pc_sys.dec new file mode 100755 index 00000000..30f8a681 Binary files /dev/null and b/tests/saves/pc_sys.dec differ diff --git a/tests/gci/quest-ep3.dec b/tests/saves/quest-ep3.dec similarity index 100% rename from tests/gci/quest-ep3.dec rename to tests/saves/quest-ep3.dec diff --git a/tests/gci/quest-ep3.gci b/tests/saves/quest-ep3.gci similarity index 100% rename from tests/gci/quest-ep3.gci rename to tests/saves/quest-ep3.gci diff --git a/tests/gci/quest-unencrypted.dec b/tests/saves/quest-unencrypted.dec similarity index 100% rename from tests/gci/quest-unencrypted.dec rename to tests/saves/quest-unencrypted.dec diff --git a/tests/gci/quest-unencrypted.gci b/tests/saves/quest-unencrypted.gci similarity index 100% rename from tests/gci/quest-unencrypted.gci rename to tests/saves/quest-unencrypted.gci diff --git a/tests/gci/quest-with-key.dec b/tests/saves/quest-with-key.dec similarity index 100% rename from tests/gci/quest-with-key.dec rename to tests/saves/quest-with-key.dec diff --git a/tests/gci/quest-with-key.gci b/tests/saves/quest-with-key.gci similarity index 100% rename from tests/gci/quest-with-key.gci rename to tests/saves/quest-with-key.gci diff --git a/tests/gci/quest-without-key.dec b/tests/saves/quest-without-key.dec similarity index 100% rename from tests/gci/quest-without-key.dec rename to tests/saves/quest-without-key.dec diff --git a/tests/gci/quest-without-key.gci b/tests/saves/quest-without-key.gci similarity index 100% rename from tests/gci/quest-without-key.gci rename to tests/saves/quest-without-key.gci diff --git a/tests/gci/save-charfile.gcid b/tests/saves/save-charfile.gcid similarity index 100% rename from tests/gci/save-charfile.gcid rename to tests/saves/save-charfile.gcid diff --git a/tests/gci/save-system.gci b/tests/saves/save-system.gci similarity index 100% rename from tests/gci/save-system.gci rename to tests/saves/save-system.gci diff --git a/tests/test-decode-gci.sh b/tests/test-decode-gci.sh index c380e25f..a01666b5 100755 --- a/tests/test-decode-gci.sh +++ b/tests/test-decode-gci.sh @@ -7,30 +7,30 @@ if [ "$EXECUTABLE" == "" ]; then EXECUTABLE="./newserv" fi -echo "... decode gci/quest-ep3.gci" -$EXECUTABLE decode-gci tests/gci/quest-ep3.gci -diff tests/gci/quest-ep3.dec tests/gci/quest-ep3.gci.dec -echo "... decode gci/quest-unencrypted.gci" -$EXECUTABLE decode-gci tests/gci/quest-unencrypted.gci -diff tests/gci/quest-unencrypted.dec tests/gci/quest-unencrypted.gci.dec -echo "... decode gci/quest-with-key.gci" -$EXECUTABLE decode-gci tests/gci/quest-with-key.gci -diff tests/gci/quest-with-key.dec tests/gci/quest-with-key.gci.dec -echo "... decode gci/quest-without-key.gci" -$EXECUTABLE decode-gci tests/gci/quest-without-key.gci --seed=1705B11E -diff tests/gci/quest-without-key.dec tests/gci/quest-without-key.gci.dec +echo "... decode saves/quest-ep3.gci" +$EXECUTABLE decode-gci tests/saves/quest-ep3.gci +diff tests/saves/quest-ep3.dec tests/saves/quest-ep3.gci.dec +echo "... decode saves/quest-unencrypted.gci" +$EXECUTABLE decode-gci tests/saves/quest-unencrypted.gci +diff tests/saves/quest-unencrypted.dec tests/saves/quest-unencrypted.gci.dec +echo "... decode saves/quest-with-key.gci" +$EXECUTABLE decode-gci tests/saves/quest-with-key.gci +diff tests/saves/quest-with-key.dec tests/saves/quest-with-key.gci.dec +echo "... decode saves/quest-without-key.gci" +$EXECUTABLE decode-gci tests/saves/quest-without-key.gci --seed=1705B11E +diff tests/saves/quest-without-key.dec tests/saves/quest-without-key.gci.dec -echo "... re-encrypt gci/save-charfile.gci" -$EXECUTABLE encrypt-gci-save --sys=tests/gci/save-system.gci tests/gci/save-charfile.gcid tests/gci/save-charfile.gci -$EXECUTABLE decrypt-gci-save --sys=tests/gci/save-system.gci tests/gci/save-charfile.gci tests/gci/save-charfile-redec.gcid -hexdump -vC tests/gci/save-charfile.gcid > tests/gci/save-charfile.gcid.hex -hexdump -vC tests/gci/save-charfile-redec.gcid > tests/gci/save-charfile-redec.gcid.hex +echo "... re-encrypt saves/save-charfile.gci" +$EXECUTABLE encrypt-gci-save --sys=tests/saves/save-system.gci tests/saves/save-charfile.gcid tests/saves/save-charfile.gci +$EXECUTABLE decrypt-gci-save --sys=tests/saves/save-system.gci tests/saves/save-charfile.gci tests/saves/save-charfile-redec.gcid +hexdump -vC tests/saves/save-charfile.gcid > tests/saves/save-charfile.gcid.hex +hexdump -vC tests/saves/save-charfile-redec.gcid > tests/saves/save-charfile-redec.gcid.hex # There should be differences on two lines: the checksum and the round2 seed -NUM_DIFF_LINES=$(diff -y --suppress-common-lines tests/gci/save-charfile.gcid.hex tests/gci/save-charfile-redec.gcid.hex | wc -l) +NUM_DIFF_LINES=$(diff -y --suppress-common-lines tests/saves/save-charfile.gcid.hex tests/saves/save-charfile-redec.gcid.hex | wc -l) if [[ $NUM_DIFF_LINES -ne 2 ]]; then - diff -U3 tests/gci/save-charfile.gcid.hex tests/gci/save-charfile-redec.gcid.hex + diff -U3 tests/saves/save-charfile.gcid.hex tests/saves/save-charfile-redec.gcid.hex exit 1 fi echo "... clean up" -rm tests/gci/*.gci.dec tests/gci/save-charfile.gci tests/gci/save-charfile-redec.gcid tests/gci/*.hex +rm tests/saves/*.gci.dec tests/saves/save-charfile.gci tests/saves/save-charfile-redec.gcid tests/saves/*.gcid.hex diff --git a/tests/test-decode-pc.sh b/tests/test-decode-pc.sh new file mode 100755 index 00000000..58da3617 --- /dev/null +++ b/tests/test-decode-pc.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +set -e + +EXECUTABLE="$1" +if [ "$EXECUTABLE" == "" ]; then + EXECUTABLE="./newserv" +fi + +echo "... decrypt saves/pc_gud.bin" +$EXECUTABLE decrypt-pc-save tests/saves/pc_gud.bin --seed=1705B11E +diff tests/saves/pc_gud.dec tests/saves/pc_gud.bin.dec +echo "... decrypt saves/pc_sys.bin" +$EXECUTABLE decrypt-pc-save tests/saves/pc_sys.bin --seed=1705B11E +diff tests/saves/pc_sys.dec tests/saves/pc_sys.bin.dec + +echo "... clean up" +rm tests/saves/pc_*.bin.dec diff --git a/tests/test-decode-vms.sh b/tests/test-decode-vms.sh index 2b95891b..27c9394e 100755 --- a/tests/test-decode-vms.sh +++ b/tests/test-decode-vms.sh @@ -7,12 +7,12 @@ if [ "$EXECUTABLE" == "" ]; then EXECUTABLE="./newserv" fi -echo "... decode vms/lionel-v1.vms" -$EXECUTABLE decode-vms tests/vms/lionel-v1.vms -diff tests/vms/lionel-v1.dec tests/vms/lionel-v1.vms.dec -echo "... decode vms/lionel-v2.vms" -$EXECUTABLE decode-vms tests/vms/lionel-v2.vms --seed=D0231610 -diff tests/vms/lionel-v2.dec tests/vms/lionel-v2.vms.dec +echo "... decode saves/lionel-v1.vms" +$EXECUTABLE decode-vms tests/saves/lionel-v1.vms +diff tests/saves/lionel-v1.dec tests/saves/lionel-v1.vms.dec +echo "... decode saves/lionel-v2.vms" +$EXECUTABLE decode-vms tests/saves/lionel-v2.vms --seed=D0231610 +diff tests/saves/lionel-v2.dec tests/saves/lionel-v2.vms.dec echo "... clean up" -rm tests/vms/*.vms.dec +rm tests/saves/*.vms.dec