def get_likely_xor_keysizes(ciphertext, number_of_keysizes=1): """Return the most likely repeating keysize for a ciphertext encrypted with a repeating key XOR. This is a helper function for break_repeating_key_xor used further down the list of key sizes we need to try. :param ciphertext: The ciphertext to find likely key sizes for :param number_of_keysizes: An optional variable to set the number of likely key sizes to return :return: A list of likely key sizes with the most likely key sizes at smaller indexes. """ normalized_distances = [] for keysize in xrange(2, 40): key_size_chunks = list(chunks(ciphertext, keysize)) total_distance = 0 max_blocks = len(key_size_chunks)-1 if len(key_size_chunks) < 40 else 40 number_of_blocks = 0 # If the number of blocks in the average distance calculation is too low # we may end up with the wrong keysize. Using 40 blocks seems to give # results. for i in xrange(0, max_blocks, 2): total_distance += get_hamming_distance(key_size_chunks[i], key_size_chunks[i + 1]) number_of_blocks += 1 avg_distance = float(total_distance) / (number_of_blocks / 2) normalized_distances.append((keysize, avg_distance / keysize)) # The most likely keysizes will have the lowest hamming weight normalized_distances.sort(key=lambda x: x[1]) return [i[0] for i in normalized_distances[:number_of_keysizes]]
def aes_ctr_encrypt(plaintext, key, nonce_generator): ciphertext = '' for pt_block in chunks(plaintext, 16): nonce = nonce_generator.next() key_steam = aes_ecb_encrypt(nonce, key) ciphertext += string_xor(pt_block, key_steam) return ciphertext
def aes_ctr_decrypt(ciphertext, key, nonce_generator): plaintext = '' for ct_block in chunks(ciphertext, 16): nonce = nonce_generator.next() key_steam = aes_ecb_encrypt(nonce, key) plaintext += string_xor(ct_block, key_steam) return plaintext
def cbc_padding_oracle(ciphertext, iv, padding_valid_fn): """ Decrypt a CBC encrypted ciphertext using a padding oracle :param ciphertext: Ciphertext to decrypt :param padding_valid_fn: Function that decrypts the ciphertext, checks the padding, and returns the status of the padding (True or False) :return: Recovered plaintext """ block_size = 16 ct_blocks = list(chunks(iv + ciphertext, block_size)) pt = '' for two_blocks in chunks(ct_blocks[::-1], 2, adv=1): try: pt = cbc_padding_attack_block(two_blocks[1], two_blocks[0], padding_valid_fn) + pt except IndexError: break return remove_pkcs7_padding(pt)
def break_repeating_nonce_ctr(ciphertexts): """ Decrypt CTR encrypted ciphertexts that were encrypted with a fixed nonce :param ciphertextx: A list of ciphertexts encrypted with a fixed nonce :return: A list of decrypted plaintexts """ block_size = 16 ciphertext = ''.join(ct[:block_size] for ct in ciphertexts) guess = list(chunks(break_repeating_key_xor(ciphertext, keysize=block_size), block_size)) block_keystream = string_xor(guess[0], ciphertexts[0]) plaintexts = [] for ct in ciphertexts: plaintext = '' for block in chunks(ct, block_size): plaintext += string_xor(block, block_keystream) plaintexts.append(plaintext) return plaintexts
def aes_cbc_decrypt(ciphertext, key, iv): """ Perform AES CBC decryption This is the solution to Set 2, Challenge 10 :param ciphertext: The data to or decrypt :param key: The AES key to use :param iv: The initialization vector :return: The resulting plaintext """ xor_block = iv plaintext = '' for ct_block in chunks(ciphertext, 16): plaintext += string_xor(aes_ecb_decrypt(ct_block, key), xor_block) xor_block = ct_block return plaintext
def break_repeating_key_xor(ciphertext, keysize=None): """Try to break a ciphertext encrypted with a repeating key XOR :param ciphertext: Attempt to decrypt this ciphertext :return: The recovered plaintext or None on failure """ best_guess = None best_guess_unprintable = None # Get the 4 most likely keysizes. The first most likely keysize it probably the # correct keysize, but if it is not we can try the next couple keysizes = get_likely_xor_keysizes(ciphertext, 4) if keysize is None else [keysize] for keysize in keysizes: ct_chunks = list(chunks(ciphertext, keysize)) ct_chunks = [[ord(c) for c in chunk] for chunk in ct_chunks] # Transpose the blocks so we get lists of the byte at position 0, 1, etc transposed_blocks = [[chunk[i] for chunk in ct_chunks if len(chunk) > i] for i in xrange(len(ct_chunks[0]))] # Brute force the XOR key one byte at a time block_hexstrings = [''.join('%02x' % byte for byte in block) for block in transposed_blocks] key = [single_byte_xor_break(hexstring)[0] for hexstring in block_hexstrings] key_string = ''.join(chr(c) for c in key) plaintext = repeating_key_xor_encrypt(ciphertext, key_string).decode('hex') # If everything is a printable ASCII character we definetly have the right # key and can stop here unprintable_chars = sum(1 for c in plaintext if c not in string.printable) if not unprintable_chars: return plaintext else: if best_guess is None: best_guess = plaintext best_guess_unprintable = unprintable_chars else: if unprintable_chars < best_guess_unprintable: best_guess = plaintext best_guess_unprintable = unprintable_chars # Unable to find a perfect decryption return the best guess return best_guess
def get_no_prefix_oracle(oracle, block_size): # We may have a random number of bytes prepended to our string. We need to add bytes until we start a new block # that we have full control of for i in xrange(0, 16): blocks = list(chunks(oracle('A' * i + 'B' * 32), block_size)) for j, block in enumerate(blocks): try: if block == blocks[j+1]: index_of_first_controlled_block = j * block_size number_of_padding_bytes = i except IndexError: break def no_prefix_oracle(plaintext): return oracle('B' * number_of_padding_bytes + plaintext)[index_of_first_controlled_block:] return no_prefix_oracle
def aes_cbc_encrypt(plaintext, key, iv=None): """ Perform AES CBC encryption, adding PKCS 7 padding as needed :param plaintext: The data to encrypt :param key: The AES key to use :param iv: The initialization vector :return: The resulting ciphertext """ iv = os.urandom(16) if iv is None else iv xor_block = iv ciphertext = '' for pt_block in chunks(plaintext, 16): ct_block = aes_ecb_encrypt(string_xor(pt_block, xor_block), key) ciphertext += ct_block xor_block = ct_block return ciphertext, iv
def detect_aes_ecb(hexstring_list): """Determine if any hexstrings provided have been encrypted with AES 128 ECB :param hexstring_list: A list of strings to search for AES 128 ECB encrypted blocks :return: A list of indices of strings that are likely encrypted with AES 128 ECB """ indexes_that_are_aes_ecb = [] for i, hexstring in enumerate(hexstring_list): blocks = list(chunks(hexstring, 2 * 16)) # Count the number of occurrence of each element in the block list # if the count list is shorter than the block list that means there # are duplicate blocks. EX: # ['a','b','c'] -> [1,1,1] # ['a','b','a'] -> [2,1] block_frequency = {block: blocks.count(block) for block in blocks} if len(block_frequency.values()) != len(blocks): indexes_that_are_aes_ecb.append(i) return indexes_that_are_aes_ecb
def test_chunks(): assert list(cc_util.chunks('aa00bb00cc00', 2, 4)) == ['aa', 'bb', 'cc']