def count_matching_prefix_blocks(a: bytes, b: bytes, block_size: int) -> bytes: i = 0 for a_block, b_block in zip(chunks(a, block_size), chunks(b, block_size)): if a_block == b_block: i += 1 else: return i
def detect_block_cipher_mode(cipher: Callable[[bytes], bytes]) -> str: pt = bytes(100) # Chosen to produce repeated blocks in ECB ciphertext ct = cipher(pt) counter = Counter() for block in utils.chunks(ct, AES_BLOCK_SIZE_BYTES): counter[block] += 1 most_common = counter.most_common(1)[0] return 'ECB' if most_common[1] > 1 else 'CBC'
def detect_aes_ecb(ciphertexts: Sequence[bytes]): candidates = [] for ciphertext in ciphertexts: counter = Counter() for block in chunks(ciphertext, AES_BLOCK_SIZE_BYTES): counter[block] += 1 candidates.append((ciphertext, counter.most_common(3))) candidates.sort(key=lambda t: t[1][0][1], reverse=True) return candidates[0][0]
def encrypt(self, plaintext: bytes) -> bytes: previous = self.iv ciphertext_bytes = [] for block in chunks(plaintext, AES_BLOCK_SIZE_BYTES): combined = fixed_xor(block, previous) ciphertext_block = self.aes_ecb.encrypt(combined) previous = ciphertext_block ciphertext_bytes.extend(ciphertext_block) return bytes(ciphertext_bytes)
def decrypt(self, ciphertext: bytes) -> bytes: previous = self.iv plaintext_bytes = [] for block in chunks(ciphertext, AES_BLOCK_SIZE_BYTES): combined = self.aes_ecb.decrypt(block) plaintext_block = fixed_xor(combined, previous) previous = block plaintext_bytes.extend(plaintext_block) return bytes(plaintext_bytes)
def break_repeating_xor(ciphertext: bytes) -> DecryptionCandidate: size_distance_pairs = [] for key_size in range(2, 41): first_n_blocks = list(chunks(ciphertext, key_size))[:N] pairwise = combinations(first_n_blocks, 2) normalized_distances = [ hamming_distance(a, b) / key_size for a, b in pairwise ] average_normalized = sum(normalized_distances) / len( normalized_distances) size_distance_pairs.append((key_size, average_normalized)) key_size_candidates = sorted(size_distance_pairs, key=lambda p: p[1]) candidates = [] for key_size, _ in key_size_candidates[:N]: # First element is the concatenation of the first byte in every block. # Second is the concatenation of the second byte in each block. # etc... transposed = [bytearray() for _ in range(key_size)] for block in chunks(ciphertext, key_size): for i, b in enumerate(block): transposed[i].append(b) key_bytes = [] for i, block in enumerate(transposed): part, _, _ = break_single_xor(block) key_bytes.extend(part) key = bytes(key_bytes) plaintext = repeating_xor(ciphertext, key) candidates.append( DecryptionCandidate( key=key, plaintext=plaintext, score=english_language_score(plaintext), )) return max(candidates, key=lambda c: c.score)
def decrypt_using_oracle( iv: bytes, ciphertext: bytes, oracle: Callable[[bytes, bytes], bool], ) -> bytes: known = bytearray() previous = iv for ct_block in chunks(ciphertext, AES_BLOCK_SIZE_BYTES): # We decrypt the ciphertext blocks from head to tail but the order # doesn't matter. You could even do it in parallel. pt_block = _decrypt_block(previous, ct_block, oracle) known.extend(pt_block) previous = ct_block plaintext = _padder.unpad(known) return bytes(plaintext)