1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 Fernando Sahmkow
#include "crypto.h"
#include <array>
#include <cctype>
namespace whiteout::storages::mpq {
// ============================================================================
// Encryption Table (1280 entries, 5 × 256)
// ============================================================================
namespace {
std::array<u32, 1280> buildEncryptionTable() {
std::array<u32, 1280> table{};
u32 seed = 0x00100001;
for (u32 index1 = 0; index1 < 256; ++index1) {
u32 index2 = index1;
for (u32 i = 0; i < 5; ++i) {
seed = (seed * 125 + 3) % 0x2AAAAB;
u32 const temp1 = (seed & 0xFFFF) << 16;
seed = (seed * 125 + 3) % 0x2AAAAB;
u32 const temp2 = seed & 0xFFFF;
table[index2] = temp1 | temp2;
index2 += 256;
}
}
return table;
}
const std::array<u32, 1280>& encryptionTable() {
static const auto table = buildEncryptionTable();
return table;
}
} // anonymous namespace
const u32* getEncryptionTable() {
return encryptionTable().data();
}
// ============================================================================
// Hash String
// ============================================================================
u32 hashString(const std::string& filename, HashType hashType) {
const auto& table = encryptionTable();
u32 seed1 = 0x7FED7FED;
u32 seed2 = 0xEEEEEEEE;
for (char const ch : filename) {
// Normalize: uppercase + '/' → '\\'
unsigned char uc = static_cast<unsigned char>(ch);
if (uc == '/')
uc = '\\';
uc = static_cast<unsigned char>(std::toupper(uc));
seed1 = table[static_cast<u32>(hashType) * 256 + uc] ^ (seed1 + seed2);
seed2 = static_cast<u32>(uc) + seed1 + seed2 + (seed2 << 5) + 3;
}
return seed1;
}
// ============================================================================
// Block Encrypt / Decrypt
// ============================================================================
void decryptBlock(u32* data, size_t count, u32 key) {
const auto& table = encryptionTable();
u32 seed = 0xEEEEEEEE;
for (size_t i = 0; i < count; ++i) {
seed += table[0x400 + (key & 0xFF)];
u32 const ch = data[i] ^ (key + seed);
key = ((~key << 21) + 0x11111111) | (key >> 11);
seed = ch + seed + (seed << 5) + 3;
data[i] = ch;
}
}
void encryptBlock(u32* data, size_t count, u32 key) {
const auto& table = encryptionTable();
u32 seed = 0xEEEEEEEE;
for (size_t i = 0; i < count; ++i) {
seed += table[0x400 + (key & 0xFF)];
u32 const ch = data[i];
data[i] = ch ^ (key + seed);
key = ((~key << 21) + 0x11111111) | (key >> 11);
seed = ch + seed + (seed << 5) + 3;
}
}
// ============================================================================
// File Key Detection
// ============================================================================
u32 detectFileKey(const u32* encryptedBlock, u32 sectorSize, u32 fileSize) {
// The first entry of the sector offset table for a compressed file is the
// size of the sector offset table itself. For a file with N sectors the
// table has (N+1) entries × 4 bytes each.
// N = ceil(fileSize / sectorSize), so table size = (N + 1) * 4.
u32 const numSectors = (fileSize + sectorSize - 1) / sectorSize;
u32 const tableSize = (numSectors + 1) * 4;
const auto& table = encryptionTable();
// Try all possible encryption keys that would produce the known plaintext
// at position 0. The decryption formula for the first u32 is:
// seed = 0xEEEEEEEE + table[0x400 + (key & 0xFF)]
// plaintext = encrypted[0] ^ (key + seed)
// So: key + seed = encrypted[0] ^ tableSize
// key = (encrypted[0] ^ tableSize) - seed
// We iterate over the 256 possible (key & 0xFF) values.
u32 const encrypted0 = encryptedBlock[0];
u32 const encrypted1 = encryptedBlock[1];
for (u32 keyByte = 0; keyByte < 256; ++keyByte) {
u32 const seed = 0xEEEEEEEE + table[0x400 + keyByte];
u32 const candidateKey = (encrypted0 ^ tableSize) - seed;
if ((candidateKey & 0xFF) != keyByte)
continue;
// Verify with the second u32. The second entry of the sector offset
// table should be <= sectorSize + tableSize (compressed sector can't
// be larger than uncompressed).
u32 key2 = candidateKey;
u32 seed2 = seed;
// Advance key/seed by one step (after decrypting first value).
seed2 = tableSize + seed2 + (seed2 << 5) + 3;
key2 = ((~key2 << 21) + 0x11111111) | (key2 >> 11);
seed2 += table[0x400 + (key2 & 0xFF)];
u32 const decrypted1 = encrypted1 ^ (key2 + seed2);
// Sector offset[1] should be reasonable: > tableSize and
// <= tableSize + sectorSize (or slightly more due to compression expansion).
if (decrypted1 > tableSize && decrypted1 <= tableSize + sectorSize) {
return candidateKey;
}
}
return 0; // Detection failed.
}
} // namespace whiteout::storages::mpq