feat: complete runtimedata decryption.

This commit is contained in:
2025-08-20 14:44:56 +08:00
parent ddecaeee6b
commit 729cd72330
4 changed files with 352 additions and 5 deletions

View File

@@ -0,0 +1,178 @@
#include "blob_impl.h"
#include "xtea_ll.hpp"
#include "third-party/chacha20.hpp"
#include "util/aes.h"
namespace {
// clang-format off
constexpr auto BLOB_MAGIC = 0x534442666F0B0E0BuLL;
constexpr auto BLOB_HEADER_SIZE = 0x14;
constexpr std::array<uint8_t, 16> CHACHA20_MAGIC = { // lldev's ninjutsu.
0xE5, 0x93, 0x8E, 0xE6,
0x88, 0x91, 0xE8, 0x8D,
0x89, 0x52, 0x42, 0x4E,
0xE6, 0x80, 0x8E, 0xE4,
};
constexpr std::array<uint8_t, 32> CHACHA20_KEY = {
0xE5, 0x90, 0x93, 0xE6,
0x88, 0x91, 0xE4, 0xB8,
0x80, 0xE8, 0xB7, 0xB3,
0xE9, 0x87, 0x8A, 0xE6,
0x94, 0xBE, 0xE5, 0xBF,
0x8D, 0xE6, 0x9C, 0xAF,
0x61, 0x77, 0x63, 0x7A,
0x72, 0x62, 0x6E, 0x00,
};
constexpr std::array<uint32_t, 4> DATAKEY_PART1_PRECURSOR = {
0xE79E8CE7,
0xADE5818C,
0x9E9BE590,
0x0082ADE9,
};
constexpr std::array<uint8_t, 32> DATAKEY_PART2_PRECURSOR_1 = {
0x9C, 0x99, 0x18, 0xB8, 0x74, 0xF1, 0xF8, 0x6B,
0x25, 0xAE, 0x87, 0xCE, 0x61, 0x32, 0xA0, 0x0C,
0x01, 0xF3, 0x3A, 0x81, 0x5A, 0x16, 0x06, 0x4E,
0x93, 0x16, 0xBE, 0xE8, 0xE3, 0xCA, 0x6F, 0xDF,
};
constexpr std::array<uint8_t, 32> DATAKEY_PART2_PRECURSOR_2 = {
0x45, 0xB6, 0x4F, 0xDC, 0xAA, 0x0B, 0x4C, 0x67,
0x11, 0x91, 0x3B, 0x75, 0x91, 0x83, 0x98, 0x05,
0x77, 0x51, 0x5F, 0xD2, 0x09, 0x37, 0x1B, 0x36,
0x0A, 0x77, 0x9B, 0x5E, 0xD6, 0xD9, 0xEE, 0x42,
};
// clang-format on
} // namespace
/*
Magic Blob File Format
v1.13.0
--------------------------------------- 0x0
| (u64) Magic Number |
--------------------------------------- 0x8
| (u32) UNK |
--------------------------------------- 0xC
| (u64) ChaCha20 Nonce |
--------------------------------------- 0x14
| ***** ChaCha20 Encrypted Data ***** |
--------------------------------------- EOF
ChaCha20 Decrypted Data
--------------------------------------- 0x14 (before file header)
| (u64) UNK |
--------------------------------------- 0x1c
| (u64) AES Encrypted Data Part 1 Len |
--------------------------------------- 0x24
| (b16) Stored Key Lower 16 Bytes |
--------------------------------------- 0x34
| **** AES Encrypted Data Part 1 **** |
--------------------------------------- 0x34 + enc_len_p1
| (b16) Stored Key Higher 16 Bytes |
--------------------------------------- 0x34 + enc_len_p1 + 0x10
| (u64) AES Encrypted Data Part 2 Len |
--------------------------------------- 0x34 + enc_len_p1 + 0x18
| **** AES Encrypted Data Part 2 **** |
--------------------------------------- EOF
*/
namespace di::data_format::_pl::v1_13_0 {
void MagicBlobImpl::read(const fs::path& path) {
StreamedIO::read(path);
// header
auto magic = get<uint64_t>();
auto unk_1 = get<uint32_t>();
auto chacha20_nonce = get<uint64_t>();
if (magic != BLOB_MAGIC) {
std::println("error!");
return;
}
if (unk_1 != 1) {
std::println("error!");
return;
}
// perform chacha20 decryption
Chacha20 chacha20(
CHACHA20_MAGIC.data(),
CHACHA20_KEY.data(),
(uint8_t*)&chacha20_nonce,
0
);
chacha20.decrypt(
reinterpret_cast<uint8_t*>(underlying().data()) + BLOB_HEADER_SIZE,
size() - BLOB_HEADER_SIZE
);
// chacha20 decrypted
XTeaLL::Block data_key;
auto unk_2 = get<uint64_t>(); // may be signed, L241
auto enc_data_part1_len = get<uint64_t>();
data_key.parts[0] = get<uint32_t>(); // L231
data_key.parts[1] = get<uint32_t>();
data_key.parts[2] = get<uint32_t>();
data_key.parts[3] = get<uint32_t>();
seek(position() + enc_data_part1_len);
data_key.parts[4] = get<uint32_t>(); // L244
data_key.parts[5] = get<uint32_t>();
data_key.parts[6] = get<uint32_t>();
data_key.parts[7] = get<uint32_t>();
auto enc_data_part2_len = get<uint64_t>();
// perform aes decryption
auto enc_data_part1_begin = underlying().data() + 0x34;
auto enc_data_part2_begin =
underlying().data() + 0x34 + enc_data_part1_len + 0x18;
XTeaLL({
DATAKEY_PART1_PRECURSOR[0],
DATAKEY_PART1_PRECURSOR[1],
DATAKEY_PART1_PRECURSOR[2],
DATAKEY_PART1_PRECURSOR[3],
})
.decrypt(data_key);
std::array<uint8_t, 32> aes_key_part1;
std::memcpy(aes_key_part1.data(), (char*)&data_key.parts, 32);
std::array<uint8_t, 32> aes_key_part2;
for (int i = 0; i < 32; i++) {
aes_key_part2[i] = DATAKEY_PART2_PRECURSOR_1[i]
^ DATAKEY_PART2_PRECURSOR_2[i] ^ aes_key_part1[i];
}
auto data_part1 = AES(aes_key_part1)
.decrypt(
{reinterpret_cast<uint8_t*>(enc_data_part1_begin),
enc_data_part1_len}
);
auto data_part2 = AES(aes_key_part2)
.decrypt(
{reinterpret_cast<uint8_t*>(enc_data_part2_begin),
enc_data_part2_len}
);
}
} // namespace di::data_format::_pl::v1_13_0

View File

@@ -6,16 +6,17 @@ namespace di::data_format::_pl::v1_13_0 {
class MagicBlobImpl : public MagicBlob {
public:
using for_each_callback_t =
std::function<void(hash_t, shared_entry_t const&)>;
MagicBlobImpl() = default;
void read(const fs::path& path) override;
shared_entry_t query(std::string_view symbol) const override;
shared_entry_t query(std::string_view symbol) const override {
return std::make_shared<MagicEntry>(0, 0);
}
void for_each(const for_each_callback_t& callback) const override;
void for_each(const for_each_callback_t& callback) const override {}
size_t count() const override;
size_t count() const override { return 0; }
};
} // namespace di::data_format::_pl::v1_13_0

View File

@@ -3,6 +3,8 @@
// An xtea variant algorithm.
// IDX = 11, L247 ~ L400
namespace di::data_format::_pl::v1_13_0 {
class XTeaLL {
public:
struct Block {
@@ -139,3 +141,5 @@ private:
return transform_block_b<true>(v1, v2);
}
};
} // namespace di::data_format::_pl::v1_13_0

164
src/third-party/chacha20.hpp vendored Normal file
View File

@@ -0,0 +1,164 @@
#pragma once
// --------------------------------------------------
// From: https://github.com/983/ChaCha20 (Unlicense)
// Modified to support custom magic constants.
// --------------------------------------------------
// This is high quality software because the includes are sorted alphabetically.
#include <assert.h>
#include <stddef.h>
#include <stdint.h>
struct Chacha20Block {
// This is basically a random number generator seeded with key and nonce.
// Generates 64 random bytes every time count is incremented.
uint32_t state[16];
static uint32_t rotl32(uint32_t x, int n) {
return (x << n) | (x >> (32 - n));
}
static uint32_t pack4(const uint8_t* a) {
return uint32_t(a[0] << 0 * 8) | uint32_t(a[1] << 1 * 8)
| uint32_t(a[2] << 2 * 8) | uint32_t(a[3] << 3 * 8);
}
static void unpack4(uint32_t src, uint8_t* dst) {
dst[0] = (src >> 0 * 8) & 0xff;
dst[1] = (src >> 1 * 8) & 0xff;
dst[2] = (src >> 2 * 8) & 0xff;
dst[3] = (src >> 3 * 8) & 0xff;
}
Chacha20Block(const uint8_t key[32], const uint8_t nonce[8])
: Chacha20Block((uint8_t*)"expand 32-byte k", key, nonce) {}
Chacha20Block(
const uint8_t magic[16],
const uint8_t key[32],
const uint8_t nonce[8]
) {
state[0] = pack4(magic + 0 * 4);
state[1] = pack4(magic + 1 * 4);
state[2] = pack4(magic + 2 * 4);
state[3] = pack4(magic + 3 * 4);
state[4] = pack4(key + 0 * 4);
state[5] = pack4(key + 1 * 4);
state[6] = pack4(key + 2 * 4);
state[7] = pack4(key + 3 * 4);
state[8] = pack4(key + 4 * 4);
state[9] = pack4(key + 5 * 4);
state[10] = pack4(key + 6 * 4);
state[11] = pack4(key + 7 * 4);
// 64 bit counter initialized to zero by default.
state[12] = 0;
state[13] = 0;
state[14] = pack4(nonce + 0 * 4);
state[15] = pack4(nonce + 1 * 4);
}
void set_counter(uint64_t counter) {
// Want to process many blocks in parallel?
// No problem! Just set the counter to the block you want to process.
state[12] = uint32_t(counter);
state[13] = counter >> 32;
}
void next(uint32_t result[16]) {
// This is where the crazy voodoo magic happens.
// Mix the bytes a lot and hope that nobody finds out how to undo it.
for (int i = 0; i < 16; i++) result[i] = state[i];
#define CHACHA20_QUARTERROUND(x, a, b, c, d) \
x[a] += x[b]; \
x[d] = rotl32(x[d] ^ x[a], 16); \
x[c] += x[d]; \
x[b] = rotl32(x[b] ^ x[c], 12); \
x[a] += x[b]; \
x[d] = rotl32(x[d] ^ x[a], 8); \
x[c] += x[d]; \
x[b] = rotl32(x[b] ^ x[c], 7);
for (int i = 0; i < 10; i++) {
CHACHA20_QUARTERROUND(result, 0, 4, 8, 12)
CHACHA20_QUARTERROUND(result, 1, 5, 9, 13)
CHACHA20_QUARTERROUND(result, 2, 6, 10, 14)
CHACHA20_QUARTERROUND(result, 3, 7, 11, 15)
CHACHA20_QUARTERROUND(result, 0, 5, 10, 15)
CHACHA20_QUARTERROUND(result, 1, 6, 11, 12)
CHACHA20_QUARTERROUND(result, 2, 7, 8, 13)
CHACHA20_QUARTERROUND(result, 3, 4, 9, 14)
}
#undef CHACHA20_QUARTERROUND
for (int i = 0; i < 16; i++) result[i] += state[i];
uint32_t* counter = state + 12;
// increment counter
counter[0]++;
if (0 == counter[0]) {
// wrap around occured, increment higher 32 bits of counter
counter[1]++;
// Limited to 2^64 blocks of 64 bytes each.
// If you want to process more than 1180591620717411303424 bytes
// you have other problems.
// We could keep counting with counter[2] and counter[3] (nonce),
// but then we risk reusing the nonce which is very bad.
assert(0 != counter[1]);
}
}
void next(uint8_t result8[64]) {
uint32_t temp32[16];
next(temp32);
for (size_t i = 0; i < 16; i++) unpack4(temp32[i], result8 + i * 4);
}
};
struct Chacha20 {
// XORs plaintext/encrypted bytes with whatever Chacha20Block generates.
// Encryption and decryption are the same operation.
// Chacha20Blocks can be skipped, so this can be done in parallel.
// If keys are reused, messages can be decrypted.
// Known encrypted text with known position can be tampered with.
// See https://en.wikipedia.org/wiki/Stream_cipher_attack
Chacha20Block block;
uint8_t keystream8[64];
size_t position;
Chacha20(
const uint8_t magic[16],
const uint8_t key[32],
const uint8_t nonce[8],
uint64_t counter
)
: block(magic, key, nonce),
position(64) {
block.set_counter(counter);
}
Chacha20(const uint8_t key[32], const uint8_t nonce[8], uint64_t counter)
: block(key, nonce),
position(64) {
block.set_counter(counter);
}
void crypt(uint8_t* bytes, size_t n_bytes) {
for (size_t i = 0; i < n_bytes; i++) {
if (position >= 64) {
block.next(keystream8);
position = 0;
}
bytes[i] ^= keystream8[position];
position++;
}
}
// Just an alias.
void decrypt(uint8_t* bytes, size_t n_bytes) { crypt(bytes, n_bytes); }
};