SL2 Analyzer
Read a FromSoftware .sl2 save and report what is in it
Loading...
Searching...
No Matches
crypto.py
Go to the documentation of this file.
1"""Per-game entry decryption. Each returns the plaintext game data for one entry,
2or None on a bad read.
3"""
4
5import hashlib
6
7from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
8
9from .keys import DS2_KEY, NR_KEY
10from .reader import u32
11
12
13##
14# @brief AES-128-CBC decrypt, truncated to a whole number of blocks.
15# @param key The 16-byte key.
16# @param iv The 16-byte initialisation vector.
17# @param ct The ciphertext.
18# @return The decrypted bytes.
19def _aes_cbc(key, iv, ct):
20 ct = ct[: len(ct) // 16 * 16]
21 return Cipher(algorithms.AES(key), modes.CBC(iv)).decryptor().update(ct)
22
23
24##
25# @brief Decrypt a DS2 entry: [16B MD5][16B IV][ciphertext], plaintext prefixed
26# by a uint32 length.
27# @param blob The raw entry bytes.
28# @param key DS2_KEY (Scholar) or DS2_VANILLA_KEY (the DX9 original). The two
29# variants share this layout exactly; only the key differs.
30# @return The game data, or None if the length prefix is unreadable.
31def decrypt_ds2(blob, key=DS2_KEY):
32 pt = _aes_cbc(key, blob[16:32], blob[32:])
33 dlen = u32(pt, 0)
34 # The length must fit the block. A wrong key decrypts to noise whose "length" is
35 # a random uint32, so this doubles as a key check: rejecting it here means a
36 # mismatched key yields None (feature off) instead of a buffer of noise that the
37 # world-block readers would happily mistake for set event flags.
38 if dlen is None or not 0 < dlen <= len(pt) - 4:
39 return None
40 return pt[4 : 4 + dlen]
41
42
43##
44# @brief Decrypt a DSR or DS3 entry. The IV doubles as the first ciphertext
45# block, so the first 16 decrypted bytes are discarded; the length sits at
46# offset 16 and the data starts at 20.
47# @param blob The raw entry bytes.
48# @param key DSR or DS3 key.
49# @return The game data, or None if the length is unreadable.
50def decrypt_iv_prefixed(blob, key):
51 dec = _aes_cbc(key, blob[16:32], blob[16:])
52 dlen = u32(dec, 16)
53 return None if dlen is None else dec[20 : 20 + dlen]
54
55
56##
57# @brief "Decrypt" an unencrypted entry (PtDE, Elden Ring). Only the MD5+IV
58# header is stripped; the rest is already plaintext.
59# @param blob The raw entry bytes.
60# @return The game data.
61def decrypt_none(blob):
62 return blob[16:]
63
64
65##
66# @brief Decrypt an Elden Ring Nightreign entry: [16B IV][ciphertext].
67# @details Close to DS3's layout and NOT the same, which is the trap. DS3 and DSR put
68# a 16-byte checksum FIRST and the IV second, so their reader takes `blob[16:32]` as
69# the IV; Nightreign leads with the IV and keeps its MD5 at the *end* of the
70# plaintext, 28 bytes back. Reading it the DS3 way decrypts to noise that still looks
71# like a buffer.
72#
73# There is no length prefix either, so nothing here truncates: the whole plaintext is
74# the payload. Use @ref nr_checksum_ok to tell a good decrypt from a bad one.
75# @param blob The raw entry bytes.
76# @param key @ref NR_KEY.
77# @return The plaintext, or None if the entry is too short to hold an IV.
78def decrypt_nr(blob, key=NR_KEY):
79 if len(blob) <= 16:
80 return None
81 return _aes_cbc(key, blob[:16], blob[16:])
82
83
84## @brief Where the MD5 sits, counting back from the end of a Nightreign plaintext,
85# and how much of the plaintext it covers. The hash runs from offset 4 to the start
86# of the digest, so the first four bytes and the twelve trailing ones are outside it.
87NR_MD5_FROM_END, NR_MD5_SKIP = 28, 4
88
89
90##
91# @brief Does a decrypted Nightreign entry hash to the digest it carries?
92# @details This is the game's own integrity check, and it doubles as the key check —
93# which is why the key above is stated as measured rather than trusted.
94# @param pt The decrypted entry.
95# @return True if the stored MD5 matches the data it covers.
97 if pt is None or len(pt) <= NR_MD5_FROM_END + NR_MD5_SKIP:
98 return False
99 end = len(pt) - NR_MD5_FROM_END
100 return (
101 hashlib.md5(pt[NR_MD5_SKIP:end], usedforsecurity=False).digest()
102 == pt[end : end + 16]
103 )
nr_checksum_ok(pt)
Does a decrypted Nightreign entry hash to the digest it carries?
Definition crypto.py:96