mirror of
https://github.com/veekun/pokedex.git
synced 2024-08-20 18:16:34 +00:00
Added pokedex.savefile, which can encrypt/decrypt Pokémon save structs.
This commit is contained in:
parent
67b36748e2
commit
6da2b325fa
1 changed files with 134 additions and 0 deletions
134
pokedex/savefile.py
Normal file
134
pokedex/savefile.py
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
# encoding: utf8
|
||||||
|
u"""
|
||||||
|
Handles reading and encryption/decryption of Pokémon save file data.
|
||||||
|
|
||||||
|
See: http://projectpokemon.org/wiki/Pokemon_NDS_Structure
|
||||||
|
|
||||||
|
Kudos to LordLandon for his pkmlib.py, from which this module was originally
|
||||||
|
derived.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import itertools, struct
|
||||||
|
|
||||||
|
def pokemon_prng(seed):
|
||||||
|
u"""Creates a generator that simulates the main Pokémon PRNG."""
|
||||||
|
while True:
|
||||||
|
seed = 0x41C64E6D * seed + 0x6073
|
||||||
|
seed &= 0xFFFFFFFF
|
||||||
|
yield seed >> 16
|
||||||
|
|
||||||
|
|
||||||
|
class PokemonSave(object):
|
||||||
|
u"""Represents an individual Pokémon, from the game's point of view.
|
||||||
|
|
||||||
|
Handles translating between the on-disk encrypted form, the in-RAM blob
|
||||||
|
(also used by pokesav), and something vaguely intelligible.
|
||||||
|
|
||||||
|
XXX: Okay, well, right now it's just encryption and decryption. But, you
|
||||||
|
know.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, blob, encrypted=False):
|
||||||
|
u"""Wraps a Pokémon save struct in a friendly object.
|
||||||
|
|
||||||
|
If `encrypted` is True, the blob will be decrypted as though it were an
|
||||||
|
on-disk save. Otherwise, the blob is taken to be already decrypted and
|
||||||
|
is left alone.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# XXX Sometime this should have an abstract internal representation.
|
||||||
|
# For now, just store the decrypted version
|
||||||
|
|
||||||
|
if encrypted:
|
||||||
|
# Decrypt it.
|
||||||
|
# Interpret as one word (pid), followed by a bunch of shorts
|
||||||
|
struct_def = "I" + "H" * ((len(blob) - 4) / 2)
|
||||||
|
shuffled = list( struct.unpack(struct_def, blob) )
|
||||||
|
|
||||||
|
# Apply standard Pokémon decryption, undo the block shuffling, and
|
||||||
|
# done
|
||||||
|
self.reciprocal_crypt(shuffled)
|
||||||
|
words = self.shuffle_chunks(shuffled, reverse=True)
|
||||||
|
self.blob = struct.pack(struct_def, *words)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Already decrypted
|
||||||
|
self.blob = blob
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def as_struct(self):
|
||||||
|
u"""Returns a decrypted struct, aka .pkm file."""
|
||||||
|
return self.blob
|
||||||
|
|
||||||
|
@property
|
||||||
|
def as_encrypted(self):
|
||||||
|
u"""Returns an encrypted struct the game expects in a save file."""
|
||||||
|
|
||||||
|
# Interpret as one word (pid), followed by a bunch of shorts
|
||||||
|
struct_def = "I" + "H" * ((len(self.blob) - 4) / 2)
|
||||||
|
words = list( struct.unpack(struct_def, self.blob) )
|
||||||
|
|
||||||
|
# Apply the block shuffle and standard Pokémon encryption
|
||||||
|
shuffled = self.shuffle_chunks(words)
|
||||||
|
self.reciprocal_crypt(shuffled)
|
||||||
|
|
||||||
|
# Stuff back into a string, and done
|
||||||
|
return struct.pack(struct_def, *shuffled)
|
||||||
|
|
||||||
|
|
||||||
|
### Utility methods
|
||||||
|
|
||||||
|
shuffle_orders = list( itertools.permutations(range(4)) )
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def shuffle_chunks(cls, words, reverse=False):
|
||||||
|
"""The main 128 encrypted bytes (or 64 words) in a save block are split
|
||||||
|
into four chunks and shuffled around in some order, based on
|
||||||
|
personality. The actual order of shuffling is a permutation of four
|
||||||
|
items in order, indexed by the shuffle index. That is, 0 yields 0123,
|
||||||
|
1 yields 0132, 2 yields 0213, etc.
|
||||||
|
|
||||||
|
Given a list of words (the first of which should be the pid), this
|
||||||
|
function returns the words in shuffled order. Pass reverse=True to
|
||||||
|
unshuffle instead.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pid = words[0]
|
||||||
|
shuffle_index = (pid >> 0xD & 0x1F) % 24
|
||||||
|
|
||||||
|
shuffle_order = cls.shuffle_orders[shuffle_index]
|
||||||
|
if reverse:
|
||||||
|
# Decoding requires going the other way; invert the order
|
||||||
|
shuffle_order = [shuffle_order.index(i) for i in range(4)]
|
||||||
|
|
||||||
|
shuffled = words[:3] # skip the unencrypted stuff
|
||||||
|
for chunk in shuffle_order:
|
||||||
|
shuffled += words[ chunk * 16 + 3 : chunk * 16 + 19 ]
|
||||||
|
shuffled += words[67:] # extra bytes are also left alone
|
||||||
|
|
||||||
|
return shuffled
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def reciprocal_crypt(cls, words):
|
||||||
|
u"""Applies the reciprocal Pokémon save file cipher to the provided
|
||||||
|
list of words.
|
||||||
|
|
||||||
|
Returns nothing; the list is changed in-place.
|
||||||
|
"""
|
||||||
|
# Apply regular Pokémon "encryption": xor everything with the output of
|
||||||
|
# the PRNG. First three items are pid/unused/checksum and are not
|
||||||
|
# encrypted.
|
||||||
|
|
||||||
|
# Main data is encrypted using the checksum as a seed
|
||||||
|
prng = pokemon_prng(words[2])
|
||||||
|
for i in range(3, 67):
|
||||||
|
words[i] ^= next(prng)
|
||||||
|
|
||||||
|
if len(words) > 67:
|
||||||
|
# Extra bytes are encrypted using the pid as a seed
|
||||||
|
prng = pokemon_prng(words[0])
|
||||||
|
for i in range(67, len(words)):
|
||||||
|
words[i] ^= next(prng)
|
||||||
|
|
||||||
|
return
|
Loading…
Reference in a new issue