Skip to content

Instantly share code, notes, and snippets.

@karanlyons
Last active May 1, 2020 08:15
Show Gist options
  • Save karanlyons/67a437402f6e2e75522352553f2506a8 to your computer and use it in GitHub Desktop.
Save karanlyons/67a437402f6e2e75522352553f2506a8 to your computer and use it in GitHub Desktop.
pngen: Makes ECB PNGuins

Plaintext:

Plaintext Penguin

Duplicate Plaintext Blocks:
	Total Blocks:                         358414
	Total Duplicates:                     349474

Ciphertext (Uncompressed and Encrypted):

$ pngen plaintext.png -k 'deceptivepenguin' --uncompressed -o ciphertext_uncompressed.png Encrypted then Compressed Penguin

Duplicate Ciphertext (Uncompressed and Encrypted) Blocks:
	Total Blocks:                         358414
	Total Duplicates:                     349473

Ciphertext (Compressed then Encrypted):

$ pngen plaintext.png -k 'deceptivepenguin' -o ciphertext.png Compressed then Encrypted Penguin

Duplicate Ciphertext (Compressed then Encrypted) Blocks:
	Total Blocks:                           4828
	Total Duplicates:                         24
#!/usr/bin/env python3
import struct
import zlib
from collections import defaultdict, namedtuple
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
CRYPTO_BACKEND = default_backend()
def ecb_encrypt(data, key):
if len(key) <= 16: key += b'\x00' * (16 - len(key))
elif len(key) <= 32: key += b'\x00' * (32 - len(key))
else: key = key[:32]
if len(data) < 16: data += '\x00' * (16 - (len(data) % 16))
cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=CRYPTO_BACKEND)
encryptor = cipher.encryptor()
tail_len = len(data) % 16
data, tail = data[:-tail_len], data[-tail_len:]
ciphertext = encryptor.update(data)
ciphertext, penultimate_block = ciphertext[:-16], ciphertext[-16:]
final_block = encryptor.update(tail + penultimate_block[:-tail_len])
return ciphertext + final_block + penultimate_block[-tail_len:]
PNG_HEADER = b'\x89PNG\r\n\x1a\n'
class Chunk(namedtuple('Chunk', ('name', 'data', 'length', 'crc'))):
def __new__(cls, name, data, length=None, crc=None):
if length is None: length = len(data)
if crc is None: crc = zlib.crc32(name + data) & 0xffffffff
return super().__new__(cls, name, data, length, crc)
def parse_png(file):
if file.read(8) != PNG_HEADER:
raise Exception("Missing PNG header")
chunks = []
while file.peek(1) != b'':
length = struct.unpack('!I', file.read(4))[0]
name = file.read(4)
data = file.read(length)
crc = struct.unpack('!I', file.read(4))[0]
chunks.append(Chunk(
name,
data,
length,
crc,
))
return chunks
def build_png(chunks):
return PNG_HEADER + b''.join(
struct.pack(
f'!I4s{c.length}sI',
c.length, c.name, c.data, c.crc,
)
for c in chunks
)
def find_duplicate_blocks(chunks):
blocks = defaultdict(int)
total_blocks = 0
for chunk in chunks:
if chunk.name == b'IDAT':
for i in range(0, len(chunk.data), 16):
blocks[chunk.data[i:i + 16]] += 1
total_blocks += 1
blocks = {
block: count - 1
for block, count in blocks.items()
if count > 1
}
return total_blocks, sorted(blocks.items(), key=lambda bc: (bc[1], bc[0]))
def print_duplicate_blocks(title, chunks, print_chunks=False):
total_blocks, duplicates = find_duplicate_blocks(chunks)
print(title)
if print_chunks:
for block, count in duplicates: print(f'\t{block.hex()}: {count: >10}')
print(f'\tTotal Blocks: {total_blocks: >10}')
print(
f'\tTotal Duplicates: '
+ f'{sum(count for block, count in duplicates): >10}'
)
if __name__ == '__main__':
import argparse
import sys
from pathlib import Path
parser = argparse.ArgumentParser(
prog='pngen',
description="makes ECB PNGuins",
)
parser.add_argument(
'file',
nargs='?',
type=argparse.FileType('rb'),
default=sys.stdin.buffer,
)
parser.add_argument('-k', '--key', default=b'\x00' * 16)
parser.add_argument(
'-o',
'--out',
nargs='?',
type=argparse.FileType('wb'),
default=sys.stdout.buffer,
)
parser.add_argument(
'--uncompressed',
help="encrypt uncompressed data",
action='store_true',
)
parser.add_argument(
'--duplicates',
help="print information about duplicate blocks",
action='store_true',
)
args = parser.parse_args()
if args.file.raw == sys.stdin.buffer.raw and sys.stdin.isatty():
parser.print_help()
sys.exit()
chunks = parse_png(args.file)
chunks = [
Chunk(chunk.name, zlib.decompress(chunk.data))
if chunk.name == b'IDAT'
else chunk
for chunk in chunks
]
if args.duplicates and args.out.raw != sys.stdout.buffer.raw:
print_duplicate_blocks('Duplicate Plaintext Blocks:', chunks)
key = args.key if type(args.key) is bytes else args.key.encode()
chunks = [
Chunk(
chunk.name,
ecb_encrypt(
chunk.data if args.uncompressed else zlib.compress(chunk.data, 9),
key,
),
)
if chunk.name == b'IDAT'
else chunk
for chunk in chunks
]
if args.duplicates and args.out.raw != sys.stdout.buffer.raw:
style = (
'Uncompressed and Encrypted'
if args.uncompressed
else 'Compressed then Encrypted'
)
print_duplicate_blocks(
f'Duplicate Ciphertext ({style}) Blocks:',
chunks
)
# Compress again even the "Compressed then Encrypted" data: This is to allow the PNG
# to still render *something* so that the differences can be visualized, as renderers
# are expecting zlib'd data. We don't adjust the dimensions of the image, and PNG's
# IDAT format isn't just a straight run of pixels, so this'll still choke various
# renderers in various ways.
args.out.write(build_png([
Chunk(
chunk.name,
zlib.compress(chunk.data, 9),
)
if chunk.name == b'IDAT'
else chunk
for chunk in chunks
]))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment