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,
2
or None on a bad read.
3
"""
4
5
import
hashlib
6
7
from
cryptography.hazmat.primitives.ciphers
import
Cipher, algorithms, modes
8
9
from
.keys
import
DS2_KEY, NR_KEY
10
from
.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.
19
def
_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.
31
def
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.
50
def
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.
61
def
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.
78
def
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.
87
NR_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.
96
def
nr_checksum_ok
(pt):
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
)
sl2.crypto.nr_checksum_ok
nr_checksum_ok(pt)
Does a decrypted Nightreign entry hash to the digest it carries?
Definition
crypto.py:96
sl2
crypto.py
Generated by
1.9.8