GCC Code Coverage Report


Directory: src/
File: src/mpq-crypto.c
Date: 2026-09-02 16:40:44
Exec Total Coverage
Lines: 68 72 94.4%
Functions: 5 5 100.0%
Branches: 24 32 75.0%

Line Branch Exec Source
1 /*
2 * mpq-crypto.c -- internal hash, encryption and key-recovery helpers.
3 *
4 * Copyright (c) 2003-2026 Maik Broemme <mbroemme@libmpq.org>
5 *
6 * This file is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU Lesser General Public License as published by
8 * the Free Software Foundation; either version 2.1 of the License, or
9 * (at your option) any later version.
10 *
11 * This file is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this file; if not, see <https://www.gnu.org/licenses/>.
18 */
19
20 #include "mpq-crypto.h"
21 #include "mpq-crypt-buf.h"
22 #include "mpq-endian.h"
23 #include "mpq-internal.h"
24 #include <libmpq/mpq.h>
25
26 #include <ctype.h>
27 #include <stdlib.h>
28
29 /* Hash an MPQ table name or file name with one of the Storm hash table offsets.
30 * The caller supplies the table offset that selects the required hash phase,
31 * and the returned value is used for MPQ lookup and encryption-key derivation. */
32 uint32_t
33 15348 libmpq__crypto_hash_string(const char *key, uint32_t offset)
34 {
35
36 /* Storm hashing starts with fixed seed values for every input string. */
37 15348 uint32_t seed1 = 0x7FED7FED;
38 15348 uint32_t seed2 = 0xEEEEEEEE;
39
40 uint32_t ch;
41
42
2/2
✓ Branch 0 taken 138283 times.
✓ Branch 1 taken 15348 times.
153631 while (*key != 0) {
43 138283 ch = toupper(*key++);
44 138283 seed1 = crypt_buf[offset + ch] ^ (seed1 + seed2);
45 138283 seed2 = ch + seed1 + seed2 + (seed2 << 5) + 3;
46 }
47
48 15348 return seed1;
49 }
50
51 /* Encrypt a block in place using the MPQ block cipher and the supplied seed.
52 * The routine serializes complete little-endian words and leaves any trailing
53 * incomplete bytes untouched because MPQ encryption is word-oriented. */
54 int32_t
55 523 libmpq__crypto_encrypt_block(uint8_t *in_buf, uint32_t in_size, uint32_t seed)
56 {
57
58 /* The cipher updates both seeds for every 32-bit word. */
59 523 uint32_t seed2 = 0xEEEEEEEE;
60 uint32_t ch;
61 uint32_t value;
62
63 /* The MPQ block cipher operates on complete 32-bit words. */
64
2/2
✓ Branch 0 taken 331013 times.
✓ Branch 1 taken 523 times.
331536 for (; in_size >= 4; in_size -= 4) {
65 331013 seed2 += crypt_buf[0x400 + (seed & 0xFF)];
66 331013 value = libmpq__load_le32(in_buf);
67 331013 ch = value ^ (seed + seed2);
68 331013 seed = ((~seed << 0x15) + 0x11111111) | (seed >> 0x0B);
69 331013 seed2 = value + seed2 + (seed2 << 5) + 3;
70 331013 libmpq__store_le32(in_buf, ch);
71 331013 in_buf += sizeof(uint32_t);
72 }
73
74 /* Block encryption has no recoverable per-word error state. */
75 523 return LIBMPQ_SUCCESS;
76 }
77
78 /* Decrypt a block in place using the MPQ block cipher and the supplied seed.
79 * It reverses the word transformation performed by the matching encryptor,
80 * so callers can reuse the same buffer without allocating a second copy. */
81 int32_t
82 525 libmpq__crypto_decrypt_block(uint8_t *in_buf, uint32_t in_size, uint32_t seed)
83 {
84
85 /* The cipher updates both seeds for every 32-bit word. */
86 525 uint32_t seed2 = 0xEEEEEEEE;
87 uint32_t ch;
88
89 /* The MPQ block cipher operates on complete 32-bit words. */
90
2/2
✓ Branch 0 taken 319131 times.
✓ Branch 1 taken 525 times.
319656 for (; in_size >= 4; in_size -= 4) {
91 319131 seed2 += crypt_buf[0x400 + (seed & 0xFF)];
92 319131 ch = libmpq__load_le32(in_buf) ^ (seed + seed2);
93 319131 seed = ((~seed << 0x15) + 0x11111111) | (seed >> 0x0B);
94 319131 seed2 = ch + seed2 + (seed2 << 5) + 3;
95 319131 libmpq__store_le32(in_buf, ch);
96 319131 in_buf += sizeof(uint32_t);
97 }
98
99 /* Block decryption has no recoverable per-word error state. */
100 525 return LIBMPQ_SUCCESS;
101 }
102
103 /* Recover a file seed by matching StormLib's small set of known file signatures.
104 * The function tests the encrypted prefix against RIFF, executable, XML, and
105 * MPQ signatures and writes the unique matching seed to the caller's output. */
106 int32_t
107 49 libmpq__crypto_detect_file_key(
108 const uint8_t *in_buf, uint32_t in_size, uint32_t file_size, uint32_t *key
109 )
110 {
111 static const uint32_t wave_magic = 0x46464952; /* "RIFF" */
112 static const uint32_t exe_magic = 0x00905A4D; /* "MZ" and DOS stub signature */
113 static const uint32_t xml_magic = 0x6D783F3C; /* "<?xm" */
114 static const uint32_t mpq_magic = 0x1A51504D; /* "MPQ\x1A" */
115 uint32_t encrypted_first;
116 uint32_t encrypted_second;
117 uint32_t first;
118 static const uint32_t known_first[] = { wave_magic, exe_magic, xml_magic, mpq_magic };
119 49 const uint32_t known_second[] = { file_size - 8, 3, 0x6576206C, 32 }; /* "l ve" */
120 uint32_t i;
121
122
3/6
✓ Branch 0 taken 49 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 49 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 49 times.
49 if (in_buf == NULL || key == NULL || in_size < 8) {
123 return LIBMPQ_ERROR_DECRYPT;
124 }
125
126 49 encrypted_first = libmpq__load_le32(in_buf);
127 49 encrypted_second = libmpq__load_le32(in_buf + sizeof(encrypted_first));
128
129
1/2
✓ Branch 0 taken 5657 times.
✗ Branch 1 not taken.
5657 for (i = 0; i < 0x100; i++) {
130 uint32_t j;
131
132 /* Invert the first cipher word for each known signature. */
133
2/2
✓ Branch 0 taken 22481 times.
✓ Branch 1 taken 5608 times.
28089 for (j = 0; j < sizeof(known_first) / sizeof(known_first[0]); j++) {
134 22481 uint32_t seed = (encrypted_first ^ known_first[j]) - 0xEEEEEEEE - crypt_buf[0x400 + i];
135 uint32_t seed2;
136 uint32_t next_seed;
137 uint32_t second;
138
139
2/2
✓ Branch 0 taken 22322 times.
✓ Branch 1 taken 159 times.
22481 if ((seed & 0xFF) != i) {
140 22322 continue;
141 }
142
143 159 first = known_first[j];
144 159 seed2 = 0xEEEEEEEE + crypt_buf[0x400 + i];
145 159 next_seed = ((~seed << 0x15) + 0x11111111) | (seed >> 0x0B);
146 159 seed2 = first + seed2 + (seed2 << 5) + 3;
147 159 seed2 += crypt_buf[0x400 + (next_seed & 0xFF)];
148 159 second = encrypted_second ^ (next_seed + seed2);
149
150
2/2
✓ Branch 0 taken 49 times.
✓ Branch 1 taken 110 times.
159 if (second == known_second[j]) {
151 49 *key = seed;
152 49 return LIBMPQ_SUCCESS;
153 }
154 }
155 }
156
157 return LIBMPQ_ERROR_DECRYPT;
158 }
159
160 /* Recover the per-file block-table seed from the first encrypted block offsets.
161 * It checks all possible low-byte cipher candidates and accepts only a seed
162 * whose first two decoded offsets describe a plausible sector-table layout. */
163 int32_t
164 111 libmpq__crypto_derive_block_table_seed(
165 uint8_t *in_buf, uint32_t in_size, uint32_t block_size, uint32_t *key
166 )
167 {
168
169 /* Candidate seed saved after matching the first known block offset. */
170 uint32_t saveseed1;
171
172 /* Intermediate value matching seed1 + seed2 for the first encrypted word. */
173 uint32_t temp;
174 111 uint32_t i = 0;
175
176 /* Derive the first seed candidate from the known block-table size. */
177
3/6
✓ Branch 0 taken 111 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 111 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 111 times.
111 if (in_buf == NULL || key == NULL || in_size < 8) {
178 return LIBMPQ_ERROR_DECRYPT;
179 }
180
181 111 temp = (libmpq__load_le32(in_buf) ^ in_size) - 0xEEEEEEEE;
182
183 /* Try every possible low byte used to index the encryption table. */
184
1/2
✓ Branch 0 taken 11176 times.
✗ Branch 1 not taken.
11176 for (i = 0; i < 0x100; i++) {
185
186 /* Candidate seeds and decrypted block offsets for this low byte. */
187 uint32_t seed1;
188 11176 uint32_t seed2 = 0xEEEEEEEE;
189 uint32_t ch;
190 uint32_t ch2;
191
192 /* The first encrypted value must decrypt to the block table size. */
193 11176 seed1 = temp - crypt_buf[0x400 + i];
194 11176 seed2 += crypt_buf[0x400 + (seed1 & 0xFF)];
195 11176 ch = libmpq__load_le32(in_buf) ^ (seed1 + seed2);
196
197
2/2
✓ Branch 0 taken 10993 times.
✓ Branch 1 taken 183 times.
11176 if (ch != in_size) {
198 10993 continue;
199 }
200
201 /* Sector offset tables are encrypted with one less than the file seed. */
202 183 saveseed1 = seed1 + 1;
203 183 ch2 = ch;
204
205 /*
206 * The second decrypted offset is unknown, but each compressed sector is at most
207 * 0xFFFF bytes, so the distance from the first offset must fit in block_size.
208 */
209 183 seed1 = ((~seed1 << 0x15) + 0x11111111) | (seed1 >> 0x0B);
210 183 seed2 = ch + seed2 + (seed2 << 5) + 3;
211 183 seed2 += crypt_buf[0x400 + (seed1 & 0xFF)];
212 183 ch = libmpq__load_le32(in_buf + sizeof(uint32_t)) ^ (seed1 + seed2);
213
214
2/2
✓ Branch 0 taken 111 times.
✓ Branch 1 taken 72 times.
183 if ((ch - ch2) <= block_size) {
215 111 *key = saveseed1;
216 111 return LIBMPQ_SUCCESS;
217 }
218 }
219
220 /* No candidate produced a plausible block offset sequence. */
221 return LIBMPQ_ERROR_DECRYPT;
222 }
223