12 Commits
Author SHA1 Message Date
itismadness 84f35b3349 fix version regex in setup.py 2020-09-21 14:44:08 +01:00
itismadness c57b56d586 fix failing travis build 2020-09-21 14:34:47 +01:00
itismadness 7f98399782 remove usage of deprecated pytest-runner 2020-09-21 14:32:15 +01:00
itismadness 12d96935ca update README 2020-09-21 14:32:00 +01:00
itismadness fc3856de07 update setup.py 2020-09-21 14:17:56 +01:00
itismadness ef17ea3b7f add additional python targets to travis matrix 2020-09-21 14:17:44 +01:00
itismadness 67e93d1732 bump version to 0.8.1 2020-09-21 14:17:30 +01:00
itismadness 5759035277 pin pprp to 0.2.6
pprp 0.2.7 was a breaking change in that it refactored the package
structure. While the new structure is an improvement, it also made
nose and coverage runtime dependencies and so we stay on 0.2.6 for
now until a new version comes out that fixes that.
2020-09-20 23:29:12 +01:00
itismadness 0d31170f0d Bump to 0.8.0 release 2019-06-09 05:44:50 +04:00
itismadness a4760b83c1 Fix for logs manually appended to each other 2019-06-08 19:23:54 -03:00
itismadness 0cf27512ed Bump to v0.7.1 2018-11-22 11:05:51 -05:00
itismadness 05b97cd447 Fix bug detecting EAC version if it's not on the first line 2018-11-22 08:32:05 -05:00
11 changed files with 213 additions and 125 deletions

No files matched your search

+3 -1
View File
@@ -2,4 +2,6 @@
build/
dist/
*.egg-info/
.eggs/
.eggs/
.pytest_cache/
__pycache__/
+10 -10
View File
@@ -1,16 +1,16 @@
language: python
dist: trusty
sudo: true
python:
- "3.5"
- "3.6"
- "3.7"
- "3.8"
jobs:
include:
- python: '3.5'
- python: '3.6'
- python: '3.7'
dist: xenial
install:
- python3 -m pip install pytest
- python3 -m pip install -e .
script:
- python3 setup.py test
- pytest
deploy:
provider: pypi
@@ -20,4 +20,4 @@ deploy:
secure: rGBkzH4DOs8Y4AY/J5yJNVGzEabV/cnKhZmh5bN5GxR6H0H/he3/J3+0pXb+omLkXyvcIiQQ/paoIs/TMQ2Ujgvkp1iSVB10OrPmhCZZv6sw3+wkQO2tuiCggHbuYZbOwZFFjaapqIW1RONHCl+qE2hvN7aVfDxbohAyVnsVUYoEGl6kKuPVuMmSBikIyWFO/NuIPYaKUSqTCORty/qfUNTOSp2nQUqg1jQb7WDb3YuBLMVPUSU7XvdswkN2nrlcJwM75AbX7B8SVfkyoVQvkgwhdVLFwTjJmXyHqfpS10CKQt9fM1442yxrI8G7+ss/fvXgaaUN+zZzu8GlPs6mpDhYNnHZ1SOHqznTag87fJFhRxhrjoxomv0m5wPX1iB9eIzqjygfn5bsatNOCPUGEhOPysF7FcchmqTqR02XXOfVhw1+OAHExadGk5ySaWazG3ajNwWQjFXu9mYh5oLZSH7ZsWjB4TiwDsta9P7Db24Y086vF/zHOHNoNf81y9G+ArUUpfZuK4QZSfTHw61+CRT4W1unAGaixDN3THcx6B5rsGuFkN6bXyWtWNT6i1X1p4wQSdIGba+2vp8VMSGENoPDpbBEd2vMl5DDqJd3mA7+nywFnQCfb89lh1Hd/ihY+Qcxag6+L82bf23Y8xULB/d0sbeZUcHf1WfK6HOTLFo=
on:
python: '3.6'
tags: true
tags: true
+37 -9
View File
@@ -1,15 +1,20 @@
# EAC Logchecker
This is a transparent implementation of the Exact Audio Copy log checksum algorithm in Python 3.4+.
![Travis-CI Status](https://img.shields.io/travis/com/OPSnet/eac_logchecker.py/master.svg)
![PyPI](https://img.shields.io/pypi/v/eac_logchecker.svg)
This is a transparent implementation of the Exact Audio Copy log checksum algorithm in Python 3.5+.
This is a fork of https://github.com/puddly/eac_logsigner, with modifications to have it
better match the output of the actual EAC Logchecker to be used in downstream applications.
better match the output of the actual EAC Logchecker to be used in downstream applications. All
credit goes to puddly for reverse-engineering the closed source EAC to develop the base.
# Requirements
## Requirements
* Python 3.5+
* [pprp](http://pypi.org/project/pprp)==0.2.6
# Installation
## Installation
From PyPI:
@@ -21,20 +26,43 @@ From source:
$ cd eac_logchecker.py
$ python setup.py install
# Usage
## Usage
usage: eac_logchecker [-h] [--json] files [files ...]
usage: eac_logchecker.py [-h] [--json] file
Verifies and resigns EAC logs
positional arguments:
files input log file(s)
file input log file
optional arguments:
-h, --help show this help message and exit
--json Output as JSON
## Example
# Overview
$ eac_logchecker logs/01.log
Log Integrity Checker (C) 2010 by Andre Wiethoff
The algorithm internally uses UTF-16 strings and XORs a refilling 32-byte buffer of characters with the internal state of what looks to be part of AES-256. The code is pretty short, go read it for more info. Open a pull request if you can figure out a way to simplify it.
1. Log entry is fine!
$ eac_logchecker logs/01.log
$ eac_logchecker logs/05.log
Log Integrity Checker (C) 2010 by Andre Wiethoff
1. Log entry is fine!
2. Log entry is fine!
$ eac_logchecker --json logs/05.log
[{"message": "Log entry is fine!", "status": "OK"}, {"message": "Log entry is fine!", "status": "OK"}]
## Algorithm
1. Strip the log file of newlines and BOMs.
2. Cut off the existing signature block and (re-)encode the log text back into little-endian UTF-16
3. Encrypt the log file with Rijndael-256:
* in CBC mode
* with a 256-bit block size (most AES implementations hard-code a 128-bit block size)
* all-zeroes IV
* zero-padding
* the hex key `9378716cf13e4265ae55338e940b376184da389e50647726b35f6f341ee3efd9`
4. XOR together all of the resulting 256-bit ciphertext blocks. You can do it byte-by-byte, it doesn't matter in the end.
5. Output the little-endian representation of the above number, in uppercase hex.
+89 -40
View File
@@ -1,16 +1,29 @@
#!/usr/bin/env python3
import argparse
import contextlib
import json
from pathlib import Path
import re
import pprp
CHECKSUM_MIN_VERSION = ('V1.0', 'beta', '1')
EAC_KEY = '9378716cf13e4265ae55338e940b376184da389e50647726b35f6f341ee3efd9'
__version__ = '0.8.1'
def eac_checksum(text):
class Log:
def __init__(self, text):
self.text = text
self.unsigned_text = self.text
self.version = None
self.modified = False
self.old_checksum = None
self.checksum = None
def eac_checksum(log):
text = log.unsigned_text
# Ignore newlines
text = text.replace('\r', '').replace('\n', '')
@@ -19,8 +32,9 @@ def eac_checksum(text):
# 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'),
# Probably SHA256('super secret password') but it doesn't
# actually matter
key=bytes.fromhex(EAC_KEY),
block_size=256 // 8
)
@@ -28,7 +42,7 @@ def eac_checksum(text):
plaintext = text.encode('utf-16-le')
# The IV is all zeroes so we don't have to handle it
signature = b'\x00' * 32
checksum = b'\x00' * 32
# Process it block-by-block
for i in range(0, len(plaintext), 32):
@@ -36,39 +50,37 @@ def eac_checksum(text):
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))
cbc_plaintext = bytes(
a ^ b for a, b in zip(checksum, plaintext_block)
)
# New signature is the ciphertext.
signature = cipher.encrypt(cbc_plaintext)
# New checksum is the ciphertext.
checksum = cipher.encrypt(cbc_plaintext)
# Textual signature is just the hex representation
return signature.hex().upper()
# Textual checksum is just the hex representation
log.checksum = checksum.hex().upper()
def extract_info(text):
if len(text) == 0:
return text, None, None
def extract_info(log):
if len(log.text) == 0:
return log
version = text.splitlines()[0]
for line in log.text.splitlines():
if line.startswith('Exact Audio Copy'):
log.version = tuple(line.split()[3:6])
elif re.match(r'[a-zA-Z]', line):
break
if not version.startswith('Exact Audio Copy'):
version = None
else:
version = tuple(version.split()[3:6])
match = re.search('\n\n==== (.*) ([A-Z0-9]+) ====', text)
match = re.search('\n\n==== (.*) ([A-Z0-9]+) ====', log.text)
if match:
text, signature_parts = re.split('\n\n==== {}'.format(match.group(1)), text)
signature = signature_parts.split()[0].strip()
else:
signature = None
return text, version, signature
search = '\n\n==== {}'.format(match.group(1))
log.unsigned_text, checksum_parts = re.split(search, log.text)
log.old_checksum = checksum_parts.split()[0].strip()
def eac_verify(text):
unsigned_text, version, old_signature = extract_info(text)
return unsigned_text, version, old_signature, eac_checksum(unsigned_text)
def eac_verify(log):
extract_info(log)
eac_checksum(log)
def get_logs(data):
@@ -85,35 +97,64 @@ def get_logs(data):
# 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')
return [x.strip() for x in re.split(r'[^-]-{60}[^-]', text)]
splits = re.split('(\n\n==== .* [A-Z0-9]+ ====)', text)
logs = []
for split in splits:
if split.strip() != '':
logs.append(split)
if len(logs) > 1:
length = len(logs) - 1 if len(logs) % 2 == 1 else len(logs)
return_logs = []
for i in range(0, length, 2):
log = Log(logs[i] + logs[i+1])
if i > 0:
(log.text, matches) = re.subn(
r'[^-]-{60}[^-]',
'',
log.text,
1
)
if matches == 0:
log.modified = True
return_logs.append(log)
for i in range(length, len(logs)):
return_logs.append(Log(logs[i]))
else:
return_logs = [Log(logs[0])]
return return_logs
def check_checksum(arg_file, arg_json):
def check_checksum(arg_file):
if not isinstance(arg_file, Path):
arg_file = Path(arg_file)
output = []
if not arg_file.exists():
if not arg_json:
print('Could not find logfile to examine.')
output.append({
'status': 'ERROR',
'message': 'Could not find logfile to examine.'
})
return output
try:
with arg_file.open('rb') as open_file:
logs = get_logs(open_file.read())
for log in logs:
data, version, old_signature, actual_signature = eac_verify(log)
eac_verify(log)
if version is None or old_signature is None:
if log.version is None or log.old_checksum is None:
message = 'Log entry has no checksum!'
status = "NO"
elif old_signature != actual_signature:
elif log.modified or log.old_checksum != log.checksum:
message = 'Log entry was modified, checksum incorrect!'
status = "BAD"
else:
@@ -135,7 +176,14 @@ def check_checksum(arg_file, arg_json):
def main():
parser = argparse.ArgumentParser(description='Verifies and resigns EAC logs')
parser = argparse.ArgumentParser(
description='Verifies and resigns EAC logs'
)
parser.add_argument(
'--version',
action='version',
version='%(prog)s ' + __version__
)
parser.add_argument('--json', action='store_true', help='Output as JSON')
parser.add_argument('file', type=Path, help='input log file')
@@ -145,12 +193,13 @@ def main():
if not args.json:
print('Log Integrity Checker (C) 2010 by Andre Wiethoff')
print('')
output = check_checksum(args.file, args.json)
output = check_checksum(args.file)
if args.json:
print(json.dumps(output))
else:
for i in range(len(output)):
print('{:d}. {:s}'.format(i+1, output[i]['message']))
if __name__ == '__main__':
main()
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+5
View File
@@ -0,0 +1,5 @@
[aliases]
test=pytest
[tool:pytest]
addopts = --verbose
+22 -21
View File
@@ -1,15 +1,15 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#!/usr/bin/env python3
# Note: To use the 'upload' functionality of this file, you must:
# $ pip install twine
import io
import os
from pathlib import Path
import re
import sys
from shutil import rmtree
from setuptools import find_packages, setup, Command
from setuptools import setup, Command
# Package meta-data.
NAME = 'eac_logchecker'
@@ -18,17 +18,15 @@ URL = 'https://github.com/OPSnet/eac_logchecker.py'
EMAIL = '[email protected]'
AUTHOR = 'OPS'
REQUIRES_PYTHON = '>=3.5.0'
VERSION = '0.7.0'
here = os.path.abspath(os.path.dirname(__file__))
# Import the README and use it as the long-description.
# Note: this will only work if 'README.md' is present in your MANIFEST.in file!
try:
with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as f:
long_description = '\n' + f.read()
except FileNotFoundError:
long_description = DESCRIPTION
setup_dir = Path(__file__).resolve().parent
version = re.search(
r"__version__ = '(.*)'",
Path(setup_dir, 'eac_logchecker.py').read_text()
)
if version is None:
raise SystemExit("Could not determine version to use")
VERSION = version.group(1)
class UploadCommand(Command):
@@ -51,12 +49,14 @@ class UploadCommand(Command):
def run(self):
try:
self.status('Removing previous builds…')
rmtree(os.path.join(here, 'dist'))
rmtree(Path(setup_dir, 'dist'))
except OSError:
pass
self.status('Building Source and Wheel (universal) distribution…')
os.system('{0} setup.py sdist bdist_wheel --universal'.format(sys.executable))
os.system(
'{0} setup.py sdist bdist_wheel --universal'.format(sys.executable)
)
self.status('Uploading the package to PyPI via Twine…')
os.system('twine upload dist/*')
@@ -64,12 +64,11 @@ class UploadCommand(Command):
sys.exit()
# Where the magic happens:
setup(
name=NAME,
version=VERSION,
description=DESCRIPTION,
long_description=long_description,
long_description=Path(setup_dir, 'README.md').read_text(),
long_description_content_type='text/markdown',
author=AUTHOR,
author_email=EMAIL,
@@ -81,16 +80,18 @@ setup(
'eac_logchecker = eac_logchecker:main'
]
},
install_requires=['pprp'],
install_requires=['pprp==0.2.6'],
tests_require=['pytest'],
license='MIT',
classifiers=[
'Development Status :: 4 - Beta'
'Development Status :: 5 - Production/Stable',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7'
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
],
# $ setup.py publish support.
cmdclass={
-44
View File
@@ -1,44 +0,0 @@
#!/usr/bin/env python3
from pathlib import Path
import unittest
import eac_logchecker
TESTS = [
(Path('logs/01.log'), [{'message': 'Log entry is fine!', 'status': 'OK'}, {'message': 'Log entry is fine!', 'status': 'OK'}]),
(Path('logs/02.log'), [{'message': 'Log entry is fine!', 'status': 'OK'}]),
(Path('logs/03.log'), [{'message': 'Log entry has no checksum!', 'status': 'NO'}]),
(Path('logs/04.log'), [{'message': 'Log entry is fine!', 'status': 'OK'}, {'message': 'Log entry has no checksum!', 'status': 'NO'}]),
(Path('logs/05.log'), [{'message': 'Log entry is fine!', 'status': 'OK'}, {'message': 'Log entry is fine!', 'status': 'OK'}]),
(Path('logs/06.log'), [{'message': 'Log entry has no checksum!', 'status': 'NO'}]),
(Path('logs/07.log'), [{'message': 'Log entry has no checksum!', 'status': 'NO'}]),
(Path('logs/08.log'), [{'message': 'Log entry was modified, checksum incorrect!', 'status': 'BAD'}]),
(Path('logs/09.log'), [{'message': 'Log entry has no checksum!', 'status': 'NO'}]),
(Path('logs/10.log'), [{'message': 'Log entry has no checksum!', 'status': 'NO'}]),
(Path('logs/11.log'), [{'message': 'Log entry has no checksum!', 'status': 'NO'}]),
(Path('logs/12.log'), [{'message': 'Log entry is fine!', 'status': 'OK'}]),
(Path('logs/13.log'), [{'message': 'Log entry is fine!', 'status': 'OK'}]),
(Path('logs/14.log'), [{'message': 'Log entry has no checksum!', 'status': 'NO'}]),
(Path('logs/15.log'), [{'message': 'Log entry was modified, checksum incorrect!', 'status': 'BAD'}]),
(Path('logs/16.log'), [{'message': 'Log entry was modified, checksum incorrect!', 'status': 'BAD'}]),
(Path('logs/17.log'), [{'message': 'Log entry has no checksum!', 'status': 'NO'}]),
(Path('logs/18.log'), [{'message': 'Log entry is fine!', 'status': 'OK'}]),
(Path('logs/19.log'), [{'message': 'Log entry has no checksum!', 'status': 'NO'}]),
(Path('logs/20.log'), [{'message': 'Log entry has no checksum!', 'status': 'NO'}]),
(Path('logs/21.log'), [{'message': 'Log entry has no checksum!', 'status': 'NO'}]),
(Path('logs/22.log'), [{'message': 'Log entry has no checksum!', 'status': 'NO'}]),
(Path('logs/23.log'), [{'message': 'Log entry has no checksum!', 'status': 'NO'}]),
(Path('logs/24.log'), [{'message': 'Log entry has no checksum!', 'status': 'NO'}]),
]
class TestLogchecker(unittest.TestCase):
def test_logs(self):
for log_file, expected in TESTS:
with self.subTest(log=str(log_file)):
actual = eac_logchecker.check_checksum(log_file, True)
self.assertEqual(expected, actual)
if __name__ == "__main__":
unittest.main()
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
from pathlib import Path
import pytest
import eac_logchecker
LOG_GOOD = {'message': 'Log entry is fine!', 'status': 'OK'}
LOG_NO = {'message': 'Log entry has no checksum!', 'status': 'NO'}
LOG_BAD = {
'message': 'Log entry was modified, checksum incorrect!',
'status': 'BAD'
}
@pytest.mark.parametrize("log_path, log_statuses", [
(Path('logs/01.log'), [LOG_GOOD, LOG_GOOD]),
(Path('logs/02.log'), [LOG_GOOD]),
(Path('logs/03.log'), [LOG_NO]),
(Path('logs/04.log'), [LOG_GOOD, LOG_NO]),
(Path('logs/05.log'), [LOG_GOOD, LOG_GOOD]),
(Path('logs/06.log'), [LOG_NO]),
(Path('logs/07.log'), [LOG_NO]),
(Path('logs/08.log'), [LOG_BAD]),
(Path('logs/09.log'), [LOG_NO]),
(Path('logs/10.log'), [LOG_NO]),
(Path('logs/11.log'), [LOG_NO]),
(Path('logs/12.log'), [LOG_GOOD]),
(Path('logs/13.log'), [LOG_GOOD]),
(Path('logs/14.log'), [LOG_NO]),
(Path('logs/15.log'), [LOG_BAD]),
(Path('logs/16.log'), [LOG_BAD]),
(Path('logs/17.log'), [LOG_NO]),
(Path('logs/18.log'), [LOG_GOOD]),
(Path('logs/19.log'), [LOG_NO]),
(Path('logs/20.log'), [LOG_NO]),
(Path('logs/21.log'), [LOG_NO]),
(Path('logs/22.log'), [LOG_NO]),
(Path('logs/23.log'), [LOG_NO]),
(Path('logs/24.log'), [LOG_NO]),
(Path('logs/25.log'), [LOG_GOOD, LOG_NO]),
(Path('logs/26.log'), [LOG_BAD]),
(Path('logs/27.log'), [LOG_GOOD, LOG_BAD, LOG_BAD, LOG_BAD])
])
def test_log(log_path, log_statuses):
actual = eac_logchecker.check_checksum(log_path)
assert log_statuses == actual