add PC save file formats and encrypt/decrypt functions

This commit is contained in:
Martin Michelsen
2023-09-23 17:01:37 -07:00
parent 85897baaeb
commit cc70280761
30 changed files with 412 additions and 79 deletions
+187 -21
View File
@@ -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));