1use crate::error::SaveError;
4use crate::xxtea::{derive_key, xxtea_decrypt};
5
6const META_MAGIC: u32 = 0xEEEEEEBE;
8
9const META_LENGTH_VANILLA: usize = 0x68; const META_LENGTH_WAYPOINT: usize = 0x168; const META_LENGTH_WORLDS_PART_I: usize = 0x180; const META_LENGTH_WORLDS_PART_II: usize = 0x1B0; const ROUNDS_VANILLA: usize = 8;
17
18const ROUNDS_DEFAULT: usize = 6;
20
21const META_FORMAT_VANILLA: u32 = 0x7D0; const META_FORMAT_FOUNDATION: u32 = 0x7D1; const VALID_META_LENGTHS: [usize; 4] = [
27 META_LENGTH_VANILLA,
28 META_LENGTH_WAYPOINT,
29 META_LENGTH_WORLDS_PART_I,
30 META_LENGTH_WORLDS_PART_II,
31];
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42#[repr(u8)]
43pub enum StorageSlot {
44 UserSettings = 0,
45 AccountData = 1,
46 PlayerState1 = 2,
47 PlayerState2 = 3,
48 PlayerState3 = 4,
49 PlayerState4 = 5,
50 PlayerState5 = 6,
51 PlayerState6 = 7,
52 PlayerState7 = 8,
53 PlayerState8 = 9,
54 PlayerState9 = 10,
55 PlayerState10 = 11,
56 PlayerState11 = 12,
57 PlayerState12 = 13,
58 PlayerState13 = 14,
59 PlayerState14 = 15,
60 PlayerState15 = 16,
61 PlayerState16 = 17,
62 PlayerState17 = 18,
63 PlayerState18 = 19,
64 PlayerState19 = 20,
65 PlayerState20 = 21,
66 PlayerState21 = 22,
67 PlayerState22 = 23,
68 PlayerState23 = 24,
69 PlayerState24 = 25,
70 PlayerState25 = 26,
71 PlayerState26 = 27,
72 PlayerState27 = 28,
73 PlayerState28 = 29,
74 PlayerState29 = 30,
75 PlayerState30 = 31,
76}
77
78impl StorageSlot {
79 pub const ALL: [StorageSlot; 32] = [
81 Self::UserSettings,
82 Self::AccountData,
83 Self::PlayerState1,
84 Self::PlayerState2,
85 Self::PlayerState3,
86 Self::PlayerState4,
87 Self::PlayerState5,
88 Self::PlayerState6,
89 Self::PlayerState7,
90 Self::PlayerState8,
91 Self::PlayerState9,
92 Self::PlayerState10,
93 Self::PlayerState11,
94 Self::PlayerState12,
95 Self::PlayerState13,
96 Self::PlayerState14,
97 Self::PlayerState15,
98 Self::PlayerState16,
99 Self::PlayerState17,
100 Self::PlayerState18,
101 Self::PlayerState19,
102 Self::PlayerState20,
103 Self::PlayerState21,
104 Self::PlayerState22,
105 Self::PlayerState23,
106 Self::PlayerState24,
107 Self::PlayerState25,
108 Self::PlayerState26,
109 Self::PlayerState27,
110 Self::PlayerState28,
111 Self::PlayerState29,
112 Self::PlayerState30,
113 ];
114
115 pub fn is_account(&self) -> bool {
117 matches!(self, Self::UserSettings | Self::AccountData)
118 }
119}
120
121#[derive(Debug, Clone)]
127pub struct SaveMetadata {
128 pub format_version: u32,
130 pub decompressed_size: u32,
132 pub compressed_size: u32,
134 pub profile_hash: u32,
136 pub spooky_hash: Option<[u64; 2]>,
138 pub sha256_hash: Option<[u8; 32]>,
140 pub decrypted_with_slot: StorageSlot,
142}
143
144pub fn read_metadata(data: &[u8], slot: StorageSlot) -> Result<SaveMetadata, SaveError> {
157 if !VALID_META_LENGTHS.contains(&data.len()) {
158 return Err(SaveError::InvalidMetaLength { length: data.len() });
159 }
160
161 let iterations = if data.len() == META_LENGTH_VANILLA {
162 ROUNDS_VANILLA
163 } else {
164 ROUNDS_DEFAULT
165 };
166
167 let u32_count = data.len() / 4;
168 let words: Vec<u32> = (0..u32_count)
169 .map(|i| u32::from_le_bytes(data[i * 4..(i + 1) * 4].try_into().unwrap()))
170 .collect();
171
172 let is_account = slot.is_account();
174 let slots_to_try: Vec<StorageSlot> = std::iter::once(slot)
175 .chain(
176 StorageSlot::ALL
177 .iter()
178 .copied()
179 .filter(|&s| s != slot && s.is_account() == is_account),
180 )
181 .collect();
182
183 for try_slot in &slots_to_try {
184 let mut attempt = words.clone();
185 let key = derive_key(*try_slot);
186 xxtea_decrypt(&mut attempt, &key, iterations);
187
188 if attempt[0] == META_MAGIC {
189 return parse_decrypted_metadata(&attempt, *try_slot);
190 }
191 }
192
193 Err(SaveError::MetaDecryptionFailed)
194}
195
196pub fn verify_sha256(metadata: &SaveMetadata, raw_save_bytes: &[u8]) -> bool {
201 use sha2::{Digest, Sha256};
202
203 match metadata.sha256_hash {
204 Some(expected) => {
205 let mut hasher = Sha256::new();
206 hasher.update(raw_save_bytes);
207 let actual: [u8; 32] = hasher.finalize().into();
208 actual == expected
209 }
210 None => true,
211 }
212}
213
214fn parse_decrypted_metadata(words: &[u32], slot: StorageSlot) -> Result<SaveMetadata, SaveError> {
220 let format_version = words[1];
221
222 if format_version == META_FORMAT_VANILLA {
223 return Err(SaveError::UnsupportedMetaFormat {
224 version: format_version,
225 });
226 }
227
228 let spooky_hash = if format_version == META_FORMAT_FOUNDATION {
229 let h0 = (words[2] as u64) | ((words[3] as u64) << 32);
230 let h1 = (words[4] as u64) | ((words[5] as u64) << 32);
231 Some([h0, h1])
232 } else {
233 None
234 };
235
236 let sha256_hash = if format_version == META_FORMAT_FOUNDATION {
237 let mut hash = [0u8; 32];
238 for i in 0..8 {
239 hash[i * 4..(i + 1) * 4].copy_from_slice(&words[6 + i].to_le_bytes());
240 }
241 Some(hash)
242 } else {
243 None
244 };
245
246 let decompressed_size = words[14];
247 let compressed_size = words[15];
248 let profile_hash = words[16];
249
250 Ok(SaveMetadata {
251 format_version,
252 decompressed_size,
253 compressed_size,
254 profile_hash,
255 spooky_hash,
256 sha256_hash,
257 decrypted_with_slot: slot,
258 })
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use crate::xxtea::{META_ENCRYPTION_KEY, xxtea_encrypt};
265
266 #[test]
267 fn invalid_meta_length() {
268 let data = vec![0u8; 50];
269 let err = read_metadata(&data, StorageSlot::PlayerState1).unwrap_err();
270 match err {
271 SaveError::InvalidMetaLength { length } => assert_eq!(length, 50),
272 _ => panic!("expected InvalidMetaLength, got {err:?}"),
273 }
274 }
275
276 #[test]
277 fn valid_meta_lengths_reach_decryption() {
278 for &len in &[0x68, 0x168, 0x180, 0x1B0] {
279 let data = vec![0u8; len];
280 let err = read_metadata(&data, StorageSlot::PlayerState1).unwrap_err();
281 assert!(
282 matches!(err, SaveError::MetaDecryptionFailed),
283 "length {len:#x} should reach decryption stage, got {err:?}"
284 );
285 }
286 }
287
288 #[test]
289 fn parse_decrypted_metadata_format_2002() {
290 let mut words = vec![0u32; 26];
291 words[0] = META_MAGIC;
292 words[1] = 0x7D2; words[14] = 1_000_000;
294 words[15] = 500_000;
295 words[16] = 0x12345678;
296
297 let meta = parse_decrypted_metadata(&words, StorageSlot::PlayerState1).unwrap();
298 assert_eq!(meta.format_version, 0x7D2);
299 assert_eq!(meta.decompressed_size, 1_000_000);
300 assert_eq!(meta.compressed_size, 500_000);
301 assert_eq!(meta.profile_hash, 0x12345678);
302 assert!(meta.spooky_hash.is_none());
303 assert!(meta.sha256_hash.is_none());
304 }
305
306 #[test]
307 fn parse_decrypted_metadata_format_2001() {
308 let mut words = vec![0u32; 26];
309 words[0] = META_MAGIC;
310 words[1] = META_FORMAT_FOUNDATION;
311 words[2] = 0xAABBCCDD;
312 words[3] = 0x11223344;
313 words[4] = 0x55667788;
314 words[5] = 0x99AABBCC;
315 for i in 6..14 {
316 words[i] = (i as u32) * 0x01010101;
317 }
318 words[14] = 2_000_000;
319 words[15] = 800_000;
320 words[16] = 0;
321
322 let meta = parse_decrypted_metadata(&words, StorageSlot::PlayerState1).unwrap();
323 assert_eq!(meta.format_version, 0x7D1);
324 let spooky = meta.spooky_hash.unwrap();
325 assert_eq!(spooky[0], 0x11223344_AABBCCDD_u64);
326 assert_eq!(spooky[1], 0x99AABBCC_55667788_u64);
327 assert!(meta.sha256_hash.is_some());
328 }
329
330 #[test]
331 fn unsupported_vanilla_format() {
332 let mut words = vec![0u32; 26];
333 words[0] = META_MAGIC;
334 words[1] = META_FORMAT_VANILLA;
335 let err = parse_decrypted_metadata(&words, StorageSlot::PlayerState1).unwrap_err();
336 match err {
337 SaveError::UnsupportedMetaFormat { version } => assert_eq!(version, 0x7D0),
338 _ => panic!("expected UnsupportedMetaFormat, got {err:?}"),
339 }
340 }
341
342 #[test]
343 fn storage_slot_is_account() {
344 assert!(StorageSlot::UserSettings.is_account());
345 assert!(StorageSlot::AccountData.is_account());
346 assert!(!StorageSlot::PlayerState1.is_account());
347 assert!(!StorageSlot::PlayerState30.is_account());
348 }
349
350 #[test]
351 fn storage_slot_all_has_32_entries() {
352 assert_eq!(StorageSlot::ALL.len(), 32);
353 }
354
355 #[test]
356 fn read_metadata_with_synthetic_encrypted_data() {
357 let slot = StorageSlot::PlayerState1;
359 let iterations = ROUNDS_DEFAULT;
360
361 let u32_count = META_LENGTH_WAYPOINT / 4;
363 let mut words = vec![0u32; u32_count];
364 words[0] = META_MAGIC;
365 words[1] = 0x7D2; words[14] = 5_000_000;
367 words[15] = 2_000_000;
368 words[16] = 0xDEADBEEF;
369
370 let key = derive_key(slot);
372 let mut encrypted = words.clone();
373 xxtea_encrypt(&mut encrypted, &key, iterations);
374
375 let data: Vec<u8> = encrypted.iter().flat_map(|w| w.to_le_bytes()).collect();
377 assert_eq!(data.len(), META_LENGTH_WAYPOINT);
378
379 let meta = read_metadata(&data, slot).unwrap();
381 assert_eq!(meta.format_version, 0x7D2);
382 assert_eq!(meta.decompressed_size, 5_000_000);
383 assert_eq!(meta.compressed_size, 2_000_000);
384 assert_eq!(meta.profile_hash, 0xDEADBEEF);
385 assert_eq!(meta.decrypted_with_slot, slot);
386 }
387
388 #[test]
389 fn read_metadata_tries_other_slots() {
390 let actual_slot = StorageSlot::PlayerState5;
392 let wrong_slot = StorageSlot::PlayerState1;
393 let iterations = ROUNDS_DEFAULT;
394
395 let u32_count = META_LENGTH_WAYPOINT / 4;
396 let mut words = vec![0u32; u32_count];
397 words[0] = META_MAGIC;
398 words[1] = 0x7D3;
399 words[14] = 100;
400 words[15] = 50;
401
402 let key = derive_key(actual_slot);
403 let mut encrypted = words.clone();
404 xxtea_encrypt(&mut encrypted, &key, iterations);
405
406 let data: Vec<u8> = encrypted.iter().flat_map(|w| w.to_le_bytes()).collect();
407
408 let meta = read_metadata(&data, wrong_slot).unwrap();
410 assert_eq!(meta.decrypted_with_slot, actual_slot);
411 }
412
413 #[test]
414 fn read_metadata_vanilla_length_uses_8_rounds() {
415 let slot = StorageSlot::PlayerState1;
416 let iterations = ROUNDS_VANILLA;
417
418 let u32_count = META_LENGTH_VANILLA / 4;
419 let mut words = vec![0u32; u32_count];
420 words[0] = META_MAGIC;
421 words[1] = META_FORMAT_FOUNDATION; words[14] = 999;
423 words[15] = 500;
424
425 let key = derive_key(slot);
426 let mut encrypted = words.clone();
427 xxtea_encrypt(&mut encrypted, &key, iterations);
428
429 let data: Vec<u8> = encrypted.iter().flat_map(|w| w.to_le_bytes()).collect();
430 assert_eq!(data.len(), META_LENGTH_VANILLA);
431
432 let meta = read_metadata(&data, slot).unwrap();
433 assert_eq!(meta.format_version, 0x7D1);
434 assert_eq!(meta.decompressed_size, 999);
435 }
436
437 #[test]
438 fn verify_sha256_no_hash_returns_true() {
439 let meta = SaveMetadata {
440 format_version: 0x7D2,
441 decompressed_size: 0,
442 compressed_size: 0,
443 profile_hash: 0,
444 spooky_hash: None,
445 sha256_hash: None,
446 decrypted_with_slot: StorageSlot::PlayerState1,
447 };
448 assert!(verify_sha256(&meta, b"anything"));
449 }
450
451 #[test]
452 fn verify_sha256_correct_hash() {
453 use sha2::{Digest, Sha256};
454
455 let data = b"test data for hashing";
456 let hash: [u8; 32] = Sha256::digest(data).into();
457
458 let meta = SaveMetadata {
459 format_version: 0x7D1,
460 decompressed_size: 0,
461 compressed_size: 0,
462 profile_hash: 0,
463 spooky_hash: None,
464 sha256_hash: Some(hash),
465 decrypted_with_slot: StorageSlot::PlayerState1,
466 };
467 assert!(verify_sha256(&meta, data));
468 }
469
470 #[test]
471 fn verify_sha256_wrong_hash() {
472 let meta = SaveMetadata {
473 format_version: 0x7D1,
474 decompressed_size: 0,
475 compressed_size: 0,
476 profile_hash: 0,
477 spooky_hash: None,
478 sha256_hash: Some([0xFF; 32]),
479 decrypted_with_slot: StorageSlot::PlayerState1,
480 };
481 assert!(!verify_sha256(&meta, b"test data"));
482 }
483
484 #[test]
485 fn meta_encryption_key_values() {
486 assert_eq!(META_ENCRYPTION_KEY[0], 0x5345414E);
488 assert_eq!(META_ENCRYPTION_KEY[1], 0x44415645);
489 assert_eq!(META_ENCRYPTION_KEY[2], 0x5259414E);
490 assert_eq!(META_ENCRYPTION_KEY[3], 0x47524E54);
491 }
492}