summaryrefslogtreecommitdiff
path: root/other/find_key.py
blob: 491e598fc4474640a50ba3c9acb8b9ceb8b40b83 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#!/usr/bin/env python

"""Script that calculates the master XOR key for a Nekopara volume
by exploiting the cryptographic weakness in single-key XOR.
"""

import copy
import os
import sys

KEY_SEARCH_SPACE = 4294967296

# Because single-key XOR is used, only one byte is really needed. It's
# not the first byte because that's often encrypted with another key.
KNOWN_MAGICS = [("png", b"\x50")]


def find_files(extension, current_path):
    """Returns a list of all paths with a given
    extension from the given base path.
    """
    found = []
    for entry in os.listdir(current_path):
        if os.path.isdir(entry):
            found += find_files(extension, entry)
        elif entry[-len(extension):] == extension:
            found.append(current_path + "/" + entry)
    return found


def get_file_keys(path):
    """Parses a comma-separated file containing
    file keys and their respective paths.
    """
    file_keys = {}
    with open(path) as output:
        for line in output:
            encrypted, path, key = line.split(',')
            if encrypted:
                file_keys[path] = int(key, 16)
    return file_keys


def setup_structures(magic_byte, paths, file_keys):
    """Parses every file in a given list of paths such that a structure
    exists containing a byte from the known plaintext, the encrypted
    byte, and the integer used to derive an encryption key. This
    massively decreases IO overhead.
    """
    structures = []
    for path in paths:
        file_key = file_keys[path]
        with open(path, "rb") as encrypted:
            encrypted_byte = encrypted.read()[1]
            structures.append((magic_byte, encrypted_byte, file_key))
    return structures


def derive_primary_key(master_key, file_key):
    """Derives a single-XOR key from given master and file keys."""
    base_key = file_key ^ master_key
    return (base_key >> 24 ^ base_key >> 16 ^ base_key >> 8 ^ base_key) & 0xff


def try_master_key(structures, key):
    """Attempts decryption on a list of structures, returning True if
    and only if it was successful in decrypting them.
    """
    for desired_byte, encrypted_byte, file_key in structures:
        primary_key = derive_primary_key(key, file_key)
        if primary_key == 0:
            continue
        elif key ^ file_key == 0 or key ^ file_key & 0xff == 1:
            continue
        if encrypted_byte ^ primary_key != desired_byte:
            return False
    return True


def get_progress_message(key):
    """Returns a message preceeded with a carraige-return
    displaying how far the script is in bruteforcing.
    """
    percent_complete = key / KEY_SEARCH_SPACE * 100
    return "\r%.1f%% (Trying %s)" % (percent_complete, hex(key))


if __name__ == "__main__":
    if len(sys.argv) != 3:
        sys.stderr.write("USAGE: %s [file keys] [path]\n" % sys.argv[0])
        sys.exit(1)

    structures = []
    file_keys = get_file_keys(sys.argv[1])
    for extension, magic in KNOWN_MAGICS:
        paths = find_files(extension, ".")
        structures += setup_structures(magic, paths, file_keys)

    potential_keys = []
    for key in range(KEY_SEARCH_SPACE):
        print(get_progress_message(key), end="")
        if try_master_key(structures, key):
            potential_keys.append(hex(key))
    print("Potential keys:")
    for key in potential_keys:
        print("* %s" % key)