From 8bd66911da8c4c2631297f4ec09ccbd4d9359cd7 Mon Sep 17 00:00:00 2001 From: Dmytro Meleshko Date: Wed, 26 May 2021 14:11:39 +0300 Subject: [PATCH] [scripts] add a little tool for working with CC saves --- scripts/crosscode-saved | 127 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100755 scripts/crosscode-saved diff --git a/scripts/crosscode-saved b/scripts/crosscode-saved new file mode 100755 index 0000000..8e64e33 --- /dev/null +++ b/scripts/crosscode-saved @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 + +import base64 +import argparse +from hashlib import md5 +from typing import IO +from Crypto.Cipher import AES +from Crypto import Random +import sys + + +CC_ENCRYPTION_MARKER_BYTES = b"[-!_0_!-]" +CC_ENCRYPTION_PASSPHRASE = b":_.NaN0" + + +def main(): + parser = argparse.ArgumentParser() + # NOTE: Empty help strings are necessary for subparsers to show up in help. + subparsers = parser.add_subparsers(required=True, metavar="COMMAND") + + subparser = subparsers.add_parser("pipe-decrypt", help="") + subparser.set_defaults(func=cmd_pipe_decrypt) + subparser.add_argument("input_file", nargs="?", default="-") + subparser.add_argument("output_file", nargs="?", default="-") + + subparser = subparsers.add_parser("pipe-encrypt", help="") + subparser.set_defaults(func=cmd_pipe_encrypt) + subparser.add_argument("input_file", nargs="?", default="-") + subparser.add_argument("output_file", nargs="?", default="-") + + args = parser.parse_args() + args.func(args) + + +def cmd_pipe_decrypt(args: argparse.Namespace) -> None: + input_file: IO[bytes] = ( + sys.stdin.buffer if args.input_file == "-" else open(args.input_file, 'rb') + ) + output_file: IO[bytes] = ( + sys.stdout.buffer if args.output_file == "-" else open(args.output_file, 'wb') + ) + + encrypted = input_file.read() + assert encrypted.startswith(CC_ENCRYPTION_MARKER_BYTES) + encrypted = encrypted[len(CC_ENCRYPTION_MARKER_BYTES):] + decrypted = CryptoJsBridge.decrypt(encrypted, CC_ENCRYPTION_PASSPHRASE) + output_file.write(decrypted) + + +def cmd_pipe_encrypt(args: argparse.Namespace) -> None: + input_file: IO[bytes] = ( + sys.stdin.buffer if args.input_file == "-" else open(args.input_file, 'rb') + ) + output_file: IO[bytes] = ( + sys.stdout.buffer if args.output_file == "-" else open(args.output_file, 'wb') + ) + + decrypted = input_file.read() + encrypted = CryptoJsBridge.encrypt(decrypted, CC_ENCRYPTION_PASSPHRASE) + output_file.write(CC_ENCRYPTION_MARKER_BYTES) + output_file.write(encrypted) + + +class CryptoJsBridge: + """ + Taken from . + Also see . + """ + + BLOCK_SIZE = 16 + SALTED_MARKER = b"Salted__" + SALT_SIZE = 8 + KEY_SIZE = 32 + IV_SIZE = 16 + + @classmethod + def pad(cls, data: bytes) -> bytes: + length = cls.BLOCK_SIZE - (len(data) % cls.BLOCK_SIZE) + return data + bytes([length]) * length + + @classmethod + def unpad(cls, data: bytes) -> bytes: + return data[:-data[-1]] + + @classmethod + def bytes_to_key(cls, data: bytes, salt: bytes, output: int) -> bytes: + """ + Extended from . + """ + assert len(salt) == cls.SALT_SIZE + data += salt + key = md5(data).digest() + final_key = key + while len(final_key) < output: + key = md5(key + data).digest() + final_key += key + return final_key[:output] + + @classmethod + def encrypt(cls, message: bytes, passphrase: bytes) -> bytes: + """ + Equivalent to `CryptoJS.AES.encrypt(message, passphrase).toString()`. + """ + salt = Random.new().read(cls.SALT_SIZE) + key_iv = cls.bytes_to_key(passphrase, salt, cls.KEY_SIZE + cls.IV_SIZE) + key, iv = key_iv[:cls.KEY_SIZE], key_iv[cls.KEY_SIZE:] + aes = AES.new(key, AES.MODE_CBC, iv) + ciphertext = aes.encrypt(cls.pad(message)) + return base64.b64encode(cls.SALTED_MARKER + salt + ciphertext) + + @classmethod + def decrypt(cls, encrypted: bytes, passphrase: bytes) -> bytes: + """ + Equivalent to `CryptoJS.AES.decrypt(encrypted, passphrase).toString(CryptoJS.enc.Utf8)`. + """ + encrypted = base64.b64decode(encrypted) + assert encrypted.startswith(cls.SALTED_MARKER) + encrypted = encrypted[len(cls.SALTED_MARKER):] + salt, ciphertext = encrypted[:cls.SALT_SIZE], encrypted[cls.SALT_SIZE:] + key_iv = cls.bytes_to_key(passphrase, salt, cls.KEY_SIZE + cls.IV_SIZE) + key, iv = key_iv[:cls.KEY_SIZE], key_iv[cls.KEY_SIZE:] + aes = AES.new(key, AES.MODE_CBC, iv) + return cls.unpad(aes.decrypt(ciphertext)) + + +if __name__ == "__main__": + main()