add PC save file formats and encrypt/decrypt functions
This commit is contained in:
+4
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+121
-6
@@ -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 = [&]<typename StructT>() {
|
||||
if (is_decrypt) {
|
||||
const void* data_section = r.getv(header.data_size);
|
||||
auto decrypted = decrypt_gci_fixed_size_file_data_section<StructT>(
|
||||
auto decrypted = decrypt_fixed_size_data_section_t<StructT, true>(
|
||||
data_section, header.data_size, round1_seed, skip_checksum, override_round2_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>(
|
||||
auto encrypted = encrypt_fixed_size_data_section_t<StructT, true>(
|
||||
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<false>(
|
||||
data.data(), offsetof(PSOPCGuildCardFile, end_padding), round1_seed, skip_checksum, override_round2_seed);
|
||||
} else {
|
||||
data = encrypt_fixed_size_data_section_s<false>(
|
||||
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<PSOPCCharacterFile*>(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<PSOPCCharacterFile::CharacterEntry::Character, false>(
|
||||
&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<PSOPCCharacterFile::CharacterEntry::Character, false>(
|
||||
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<const PSOPCCharacterFile::CharacterEntry::Character*>(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<size_t>(bytes, data.size()) : data.size();
|
||||
if (is_decrypt) {
|
||||
output_data = big_endian
|
||||
? decrypt_fixed_size_data_section_s<true>(data.data(), effective_size, round1_seed, skip_checksum, override_round2_seed)
|
||||
: decrypt_fixed_size_data_section_s<false>(data.data(), effective_size, round1_seed, skip_checksum, override_round2_seed);
|
||||
} else {
|
||||
output_data = big_endian
|
||||
? encrypt_fixed_size_data_section_s<true>(data.data(), effective_size, round1_seed)
|
||||
: encrypt_fixed_size_data_section_s<false>(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<StructT>(
|
||||
auto decrypted = decrypt_fixed_size_data_section_t<StructT, true>(
|
||||
data_section,
|
||||
header.data_size,
|
||||
seed,
|
||||
|
||||
+27
-10
@@ -40,36 +40,44 @@ template <bool IsBigEndian>
|
||||
void PSOLFGEncryption::encrypt_t(void* vdata, size_t size, bool advance) {
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::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<U32T*>(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 <bool IsBigEndian>
|
||||
void PSOLFGEncryption::encrypt_minus_t(void* vdata, size_t size, bool advance) {
|
||||
using U32T = typename std::conditional<IsBigEndian, be_uint32_t, le_uint32_t>::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<U32T*>(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<const char*>(data), size);
|
||||
PSOV2Encryption crypt(0x66);
|
||||
for (size_t z = 0; z < size; z++) {
|
||||
ret[z] ^= (crypt.next() & 0x7F);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -272,3 +272,5 @@ template <size_t Size>
|
||||
std::u16string encrypt_challenge_rank_text(const ptext<char16_t, Size>& data) {
|
||||
return encrypt_challenge_rank_text(data.data(), data.size());
|
||||
}
|
||||
|
||||
std::string decrypt_v2_registry_value(const void* data, size_t size);
|
||||
|
||||
@@ -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<char16_t, 0x18> name;
|
||||
/* 38 */ ptext<char16_t, 0x5A> 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 {
|
||||
|
||||
+9
-11
@@ -83,10 +83,8 @@ struct PSOGCIDLQFileEncryptedHeader : PSOMemCardDLQFileEncryptedHeader<true> {
|
||||
} __attribute__((packed));
|
||||
|
||||
template <bool IsBigEndian>
|
||||
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<IsBigEndian>(
|
||||
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<IsBigEndian>(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 <bool IsBigEndian>
|
||||
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>([&](uint64_t seed, size_t) {
|
||||
try {
|
||||
string ret = decrypt_gci_or_vms_v2_download_quest_data_section<IsBigEndian>(
|
||||
string ret = decrypt_download_quest_data_section<IsBigEndian>(
|
||||
data_section, size, seed);
|
||||
lock_guard<mutex> 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<true>(
|
||||
return decrypt_download_quest_data_section<true>(
|
||||
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<true>(
|
||||
return decrypt_download_quest_data_section<true>(
|
||||
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<true>(
|
||||
return find_seed_and_decrypt_download_quest_data_section<true>(
|
||||
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<false>(
|
||||
return decrypt_download_quest_data_section<false>(
|
||||
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<false>(
|
||||
return find_seed_and_decrypt_download_quest_data_section<false>(
|
||||
data_section, header.data_size, find_seed_num_threads);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<true>(
|
||||
data_section, size, round1_seed, max_decrypt_bytes);
|
||||
string decrypted = decrypt_data_section<true>(data_section, size, round1_seed, max_decrypt_bytes);
|
||||
|
||||
PSOV2Encryption round2_crypt(round2_seed);
|
||||
round2_crypt.encrypt_big_endian(decrypted.data(), decrypted.size());
|
||||
|
||||
+187
-21
@@ -117,6 +117,27 @@ struct PSOGCSaveFileSymbolChatEntry {
|
||||
/* 58 */
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PSOPCSaveFileSymbolChatEntry {
|
||||
/* 00 */ le_uint32_t present;
|
||||
/* 04 */ ptext<char16_t, 0x18> 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<CornerObject, 4> corner_objects;
|
||||
struct FacePart {
|
||||
uint8_t type;
|
||||
uint8_t x;
|
||||
uint8_t y;
|
||||
uint8_t flags;
|
||||
} __attribute__((packed));
|
||||
/* 40 */ parray<FacePart, 12> face_parts;
|
||||
/* 70 */
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PSOGCSaveFileChatShortcutEntry {
|
||||
/* 00 */ be_uint32_t present_type;
|
||||
/* 04 */ parray<uint8_t, 0x50> definition;
|
||||
@@ -298,8 +319,7 @@ struct PSOGCSnapshotFile {
|
||||
} __attribute__((packed));
|
||||
|
||||
template <bool IsBigEndian>
|
||||
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 <bool IsBigEndian>
|
||||
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<const char*>(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 <typename StructT>
|
||||
StructT decrypt_gci_fixed_size_file_data_section(
|
||||
template <bool IsBigEndian>
|
||||
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<true>(
|
||||
data_section, size, round1_seed);
|
||||
using U32T = std::conditional_t<IsBigEndian, be_uint32_t, le_uint32_t>;
|
||||
|
||||
if (size < 2 * sizeof(U32T)) {
|
||||
throw runtime_error("data size is too small");
|
||||
}
|
||||
std::string decrypted = decrypt_data_section<IsBigEndian>(data_section, size, round1_seed);
|
||||
|
||||
uint32_t round2_seed = override_round2_seed < 0x100000000
|
||||
? static_cast<uint32_t>(override_round2_seed)
|
||||
: reinterpret_cast<const U32T*>(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<U32T*>(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 <typename StructT, bool IsBigEndian>
|
||||
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<IsBigEndian>(data_section, size, round1_seed);
|
||||
if (decrypted.size() < sizeof(StructT)) {
|
||||
throw std::runtime_error("file too small for structure");
|
||||
}
|
||||
StructT ret = *reinterpret_cast<const StructT*>(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 <bool IsBigEndian>
|
||||
std::string encrypt_fixed_size_data_section_s(const void* data, size_t size, uint32_t round1_seed) {
|
||||
using U32T = std::conditional_t<IsBigEndian, be_uint32_t, le_uint32_t>;
|
||||
|
||||
template <typename StructT>
|
||||
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<uint32_t>();
|
||||
|
||||
string encrypted(reinterpret_cast<const char*>(data), size);
|
||||
*reinterpret_cast<U32T*>(encrypted.data()) = 0;
|
||||
*reinterpret_cast<U32T*>(encrypted.data() + encrypted.size() - sizeof(U32T)) = round2_seed;
|
||||
*reinterpret_cast<U32T*>(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<IsBigEndian>(encrypted.data(), encrypted.size(), round1_seed);
|
||||
}
|
||||
|
||||
template <typename StructT, bool IsBigEndian>
|
||||
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<uint32_t>();
|
||||
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<true>(
|
||||
&encrypted, sizeof(StructT), round1_seed);
|
||||
return encrypt_data_section<IsBigEndian>(&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<uint8_t, 0x624> unused1;
|
||||
/* 0624 */ le_uint32_t creation_timestamp;
|
||||
/* 0628 */ parray<uint8_t, 0xDD8> 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<le_uint16_t, 0x10> unknown_a4; // Last one is always 0x1234?
|
||||
/* 002C */ parray<uint8_t, 0x100> event_flags;
|
||||
/* 012C */ le_uint32_t round1_seed;
|
||||
/* 0130 */ parray<uint8_t, 0xD0> end_padding;
|
||||
/* 0200 */
|
||||
} __attribute__((packed));
|
||||
|
||||
struct PSOPCGuildCardFile { // PSO______GUD
|
||||
/* 0000 */ le_uint32_t checksum;
|
||||
// TODO: Figure out the PC guild card format.
|
||||
/* 0004 */ parray<uint8_t, 0x7980> unknown_a1;
|
||||
/* 7984 */ le_uint32_t creation_timestamp;
|
||||
/* 7988 */ le_uint32_t round2_seed;
|
||||
/* 798C */ parray<uint8_t, 0x74> 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<uint8_t, 0x430> 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<uint8_t, 0x7D4> unknown_a2;
|
||||
/* 0C0C */ GuildCardPC guild_card;
|
||||
/* 0CFC */ parray<PSOPCSaveFileSymbolChatEntry, 12> symbol_chats;
|
||||
// TODO: Figure out what this is. On GC, this is where chat shortcuts and
|
||||
// challenge/battle records go.
|
||||
/* 123C */ parray<uint8_t, 0xAA0> unknown_a3;
|
||||
/* 1CDC */ parray<le_uint16_t, 20> tech_menu_shortcut_entries;
|
||||
/* 1D04 */ parray<uint8_t, 0x2C> unknown_a4;
|
||||
/* 1D30 */ ptext<char, 0x10> serial_number; // As %08X (not decimal)
|
||||
/* 1D40 */ ptext<char, 0x10> access_key; // As decimal
|
||||
/* 1D50 */ le_uint32_t round2_seed;
|
||||
/* 1D54 */
|
||||
} __attribute__((packed));
|
||||
/* 0004 */ Character character;
|
||||
/* 1D58 */ parray<uint8_t, 0x3C> unused;
|
||||
/* 1D94 */
|
||||
} __attribute__((packed));
|
||||
/* 00440 */ parray<CharacterEntry, 0x80> entries;
|
||||
/* ECE40 */
|
||||
} __attribute__((packed));
|
||||
|
||||
Binary file not shown.
Executable
BIN
Binary file not shown.
Binary file not shown.
Executable
BIN
Binary file not shown.
+20
-20
@@ -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
|
||||
|
||||
Executable
+18
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user