forked from puddly/eac_logsigner
-
Notifications
You must be signed in to change notification settings - Fork 0
/
eac.py
151 lines (109 loc) · 5.04 KB
/
eac.py
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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
#!/usr/bin/python
import sys
import argparse
import contextlib
import pprp
CHECKSUM_MIN_VERSION = ('V1.0', 'beta', '1')
def eac_checksum(text):
# Ignore newlines
text = text.replace('\r', '').replace('\n', '')
# Fuzzing reveals BOMs are also ignored
text = text.replace('\ufeff', '').replace('\ufffe', '')
# Setup Rijndael-256 with a 256-bit blocksize
cipher = pprp.crypto_3.rijndael(
# Probably SHA256('super secret password') but it doesn't actually matter
key=bytes.fromhex('9378716cf13e4265ae55338e940b376184da389e50647726b35f6f341ee3efd9'),
block_size=256 // 8
)
# Encode the text as UTF-16-LE
plaintext = text.encode('utf-16-le')
# The IV is all zeroes so we don't have to handle it
signature = b'\x00' * 32
# Process it block-by-block
for i in range(0, len(plaintext), 32):
# Zero-pad the last block, if necessary
plaintext_block = plaintext[i:i + 32].ljust(32, b'\x00')
# CBC mode (XOR the previous ciphertext block into the plaintext)
cbc_plaintext = bytes(a ^ b for a, b in zip(signature, plaintext_block))
# New signature is the ciphertext.
signature = cipher.encrypt(cbc_plaintext)
# Textual signature is just the hex representation
return signature.hex().upper()
def extract_info(text):
version = text.splitlines()[0]
if not version.startswith('Exact Audio Copy'):
version = None
else:
version = tuple(version.split()[3:6])
if '\r\n\r\n==== Log checksum' not in text:
signature = None
else:
text, signature_parts = text.split('\r\n\r\n==== Log checksum', 1)
signature = signature_parts.split()[0].strip()
return text, version, signature
def eac_verify(data):
# Log is encoded as Little Endian UTF-16
text = data.decode('utf-16-le')
# Strip off the BOM
if text.startswith('\ufeff'):
text = text[1:]
# Null bytes screw it up
if '\x00' in text:
text = text[:text.index('\x00')]
# EAC crashes if there are more than 2^14 bytes in a line
if any(len(l) + 1 > 2**13 for l in text.split('\n')):
raise RuntimeError('EAC cannot handle lines longer than 2^13 chars')
unsigned_text, version, old_signature = extract_info(text)
return unsigned_text, version, old_signature, eac_checksum(unsigned_text)
class FixedFileType(argparse.FileType):
def __call__(self, string):
file = super().__call__(string)
# Properly handle stdin/stdout with 'b' mode
if 'b' in self._mode and file in (sys.stdin, sys.stdout):
return file.buffer
return file
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Verifies and resigns EAC logs')
subparsers = parser.add_subparsers(dest='command', required=True)
verify_parser = subparsers.add_parser('verify', help='verify a log')
verify_parser.add_argument('files', type=FixedFileType(mode='rb'), nargs='+', help='input log file(s)')
sign_parser = subparsers.add_parser('sign', help='sign or fix an existing log')
sign_parser.add_argument('--force', action='store_true', help='forces signing even if EAC version is too old')
sign_parser.add_argument('input_file', type=FixedFileType(mode='rb'), help='input log file')
sign_parser.add_argument('output_file', type=FixedFileType(mode='wb'), help='output log file')
args = parser.parse_args()
if args.command == 'sign':
with contextlib.closing(args.input_file) as handle:
try:
data, version, old_signature, actual_signature = eac_verify(handle.read())
except ValueError as e:
print(args.input_file, ': ', e, sep='')
sys.exit(1)
if not args.force and (version is None or version <= CHECKSUM_MIN_VERSION):
raise ValueError('EAC version is too old to be signed')
data += f'\r\n\r\n==== Log checksum {actual_signature} ====\r\n'
with contextlib.closing(args.output_file or args.input_file) as handle:
handle.write(b'\xff\xfe' + data.encode('utf-16le'))
elif args.command == 'verify':
max_length = max(len(f.name) for f in args.files)
for file in args.files:
prefix = (file.name + ':').ljust(max_length + 2)
with contextlib.closing(file) as handle:
try:
data, version, old_signature, actual_signature = eac_verify(handle.read())
except RuntimeError as e:
print(prefix, e)
continue
except ValueError as e:
print(prefix, 'Not a log file')
continue
if version is None:
print(prefix, 'Not a log file')
elif old_signature is None:
print(prefix, 'Log file without a signature')
elif old_signature != actual_signature:
print(prefix, 'Malformed')
elif version <= CHECKSUM_MIN_VERSION:
print(prefix, 'Forged')
else:
print(prefix, 'OK')