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
+4 -1
View File
@@ -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
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
+2
View File
@@ -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);
+13
View File
@@ -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
View File
@@ -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);
}
}
+2 -3
View File
@@ -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
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));
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
+20 -20
View File
@@ -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
+18
View File
@@ -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 -7
View File
@@ -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