add PC save file formats and encrypt/decrypt functions
This commit is contained in:
+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));
|
||||
|
||||
Reference in New Issue
Block a user