def aes_cbc_encrypt( key: bytes, plaintext: bytes, iv: bytes, block_size: int = 16 ) -> bytes: plaintext = pkcs_7(plaintext, block_size) num_blocks = len(plaintext) // block_size blocks = [ plaintext[x * block_size : (x + 1) * block_size] for x in range(num_blocks) ] ciphertext = b"" aes_output = b"" for ind, block in enumerate(blocks): if ind == 0: block_to_xor = iv else: block_to_xor = aes_output # AES output from last block this_block = xor(block, block_to_xor) aes_output = aes_ecb_encrypt(key, this_block, padding=False) ciphertext = ciphertext + aes_output return ciphertext
def test_break_random_access_rw_ctr(): # Set 4, challenge 25: Break "random access read/write" AES CTR path_to_test_data = os.path.join( os.path.dirname(os.path.abspath(__file__)), "data/20.txt") with open(path_to_test_data, "r") as f: plaintexts_b64 = f.readlines() plaintexts = [base64_to_bytes(x) for x in plaintexts_b64] key = os.urandom(BLOCK_SIZE) nonce = 0 ciphertexts = [ aes_ctr_encrypt(key, x, nonce, BLOCK_SIZE) for x in plaintexts ] edited_ciphertexts = [] for ciphertext in ciphertexts: # Edit at position 0... why not! # And we'll edit to have all null bytes as the "new" plaintext. Allowed by the API! new_plaintext = b"\x00" * len(ciphertext) edited_ciphertext = aes_ctr_edit(key, ciphertext, 0, new_plaintext, nonce) edited_ciphertexts.append(edited_ciphertext) # This means that the "edited_ciphertexts" are really just the keystream. for ind, ciphertext in enumerate(ciphertexts): reconstructed_plaintext = xor(edited_ciphertexts[ind], ciphertext) assert reconstructed_plaintext == plaintexts[ind]
def aes_cbc_decrypt( key: bytes, ciphertext: bytes, iv: bytes, block_size: int = 16, remove_padding: bool = True, ) -> bytes: if len(ciphertext) % block_size != 0: raise ValueError("Ciphertext is not a multiple of the block size!") num_blocks = len(ciphertext) // block_size blocks = [ ciphertext[x * block_size : (x + 1) * block_size] for x in range(num_blocks) ] plaintext = b"" for ind, block in enumerate(reversed(blocks)): block_num = num_blocks - ind if block_num == 1: block_to_xor = iv else: block_to_xor = blocks[block_num - 2] # previous ciphertext block aes_output = aes_ecb_decrypt(key, block, remove_padding=False) this_block = xor(aes_output, block_to_xor) plaintext = this_block + plaintext if remove_padding: plaintext = remove_pkcs_7(plaintext) return plaintext
def test_single_character_xor(): # Set 1, challenge 3 (shift cipher) ciphertext = ( "1b37373331363f78151b7f2b783431333d78397828372d363c78373e783a393b3736" ) # type: str expected_plaintext = "Cooking MC's like a pound of bacon" # type: str bytes_ciphertext = hex_to_bytes(ciphertext) bytes_key, _ = break_single_char_xor(bytes_ciphertext) actual_plaintext = xor(bytes_ciphertext, bytes_key) assert expected_plaintext == actual_plaintext.decode("utf8")
def test_repeating_key_xor(): # Set 1, challenge 5 (repeating key XOR) plaintext = ( "Burning 'em, if you ain't quick and nimble\n" "I go crazy when I hear a cymbal" ) key = "ICE" expected_ciphertext = ( "0b3637272a2b2e63622c2e69692a23693a2a3c6324202d623d63343c2a26226324272765272" "a282b2f20430a652e2c652a3124333a653e2b2027630c692b20283165286326302e27282f" ) actual_ciphertext = bytes_to_hex(xor(plaintext.encode("utf8"), key.encode("utf8"))) assert expected_ciphertext == actual_ciphertext
def test_detect_single_character_xor(): # Set 1, challenge 4 (detect single character xor) path_to_data = os.path.join( os.path.dirname(os.path.abspath(__file__)), "data/4.txt" ) with open(path_to_data, "r") as f: ciphertexts = f.read().split("\n") metrics = [] for ciphertext in ciphertexts: _, metric = break_single_char_xor(hex_to_bytes(ciphertext)) metrics.append(metric) best_metric = min(metrics) argmin_metric = metrics.index(best_metric) xored_ciphertext = hex_to_bytes(ciphertexts[argmin_metric]) key, _ = break_single_char_xor(xored_ciphertext) plaintext = xor(xored_ciphertext, key) assert "Now that the party is jumping" in plaintext.decode("utf8")
def break_single_char_xor(ciphertext: bytes) -> Tuple[bytes, float]: potential_keys = [x.encode("utf8") for x in list(TEST_CHARACTERS)] best_key = b"" best_metric = 100.0 for key in potential_keys: result = xor(ciphertext, key) try: metric = score_english_text(result.decode("utf8")) except UnicodeDecodeError: # Not valid UTF-8 metric = 1000.0 if metric < best_metric: print( f"metric {metric!r} is better than the best {best_metric!r}, setting best key to {key!r}" ) best_metric = metric best_key = key print("best_key", best_key) print("best_metric", best_metric) return best_key, best_metric
def test_break_fixed_nonce_ctr(): # Set 3, challenge 19: Break fixed-nonce CTR mode using substitutions path_to_test_data = os.path.join( os.path.dirname(os.path.abspath(__file__)), "data/19.txt") with open(path_to_test_data, "r") as f: plaintexts_b64 = f.readlines() plaintexts = [base64_to_bytes(x) for x in plaintexts_b64] key = os.urandom(BLOCK_SIZE) nonce = 0 # Fixed nonce ciphertexts = [ aes_ctr_encrypt(key, x, nonce, BLOCK_SIZE) for x in plaintexts ] # Each ciphertext has been encrypted against the same keystream: # c_1 = p_1 xor k_1 # c_2 = p_2 xor k_2 # since k_1 = k_2 = k, we can xor ciphertexts to get: # c_1 xor c_2 = p_1 xor k xor p_2 xor k = p_1 xor p_2 C1_xor_C2 = namedtuple("C1_xor_C2", "c1_index c2_index value") xored = [] for ind_x, ciphertext_x in enumerate(ciphertexts): for ind_y, ciphertext_y in enumerate(ciphertexts): if ind_x != ind_y: result = [ bytes([x ^ y]) for x, y in zip(ciphertext_x, ciphertext_y) ] result_bytes = b"".join(result) result_tuple = C1_xor_C2(ind_x, ind_y, result_bytes) xored.append(result_tuple) xored = list(set(xored)) reconstructed_keystream = b"\x00" * 32 keystream_byte_guesses = {} # this will be a dict of lists # Crib dragging: # we have pairs of p_1 xor p_2 # if we xor with p_{test} = sp_1 = p, we'll get; p_1 xor p_{test} xor p_2 = p_2 only # so let's try some trigrams and see if we get any english text, this will be p_2 common_ngrams = [ b"the ", b" and ", b"ing ", b" her ", b" his ", b"this ", b"And ", b"This ", b"The ", b" in the ", b" in ", b" or ", b"Or ", b"What ", b"To ", b"When ", b" when ", b"All ", b"of the ", b" my ", b"and th", b"ation", b"There ", b" there ", b" I ", b" I had ", b"Her ", b"His ", b" which ", b"Which ", b"Their ", b" their ", b" would ", b"Would ", b"end", b"for ", b"ate", b"eth", b"all", b" said", b" will", b"I have ", ] # top_words = [x.encode('utf8') for x in top_n_english_words(10)] top_words_with_space = [(x + " ").encode("utf8") for x in top_n_english_words(10)] english_guesses = list(set(common_ngrams + top_words_with_space)) for pair in xored: for test_ngram in english_guesses: result = xor(pair.value, test_ngram) for found_ngram in english_guesses: if found_ngram != test_ngram and found_ngram in result: # if the area where we found a match in c1_xor_c2 = \x00, then skip # that's what we are doing with the found_ngram != test_ngram # Otherwise we found a few bytes of keystream starting_index = result.find(found_ngram) len_ngram = len(found_ngram) for index in range(len_ngram): # k = p xor c # but we don't know _which_ c to xor with # let's add both and then vote at the end ct_index = starting_index + index c1 = ciphertexts[pair.c1_index] c2 = ciphertexts[pair.c2_index] keystream_byte_guess_c1 = bytes( [found_ngram[index] ^ c1[ct_index]]) keystream_byte_guess_c2 = bytes( [found_ngram[index] ^ c2[ct_index]]) try: keystream_byte_guesses[ct_index].append( keystream_byte_guess_c1) keystream_byte_guesses[ct_index].append( keystream_byte_guess_c2) except KeyError: keystream_byte_guesses[ct_index] = [ keystream_byte_guess_c1, keystream_byte_guess_c2, ] for ct_index in keystream_byte_guesses.keys(): guesses = keystream_byte_guesses[ct_index] winning_guess = max(set(guesses), key=guesses.count) reconstructed_keystream = (reconstructed_keystream[:ct_index] + winning_guess + reconstructed_keystream[ct_index + 1:]) expected_keystream = aes_ctr_encrypt(key, b"\x00" * 32, nonce, BLOCK_SIZE) percent_correct = ( [x == y for x, y in zip(expected_keystream, reconstructed_keystream) ].count(True) / len(reconstructed_keystream) * 100) assert percent_correct > 80.0