Beispiel #1
0
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
Beispiel #2
0
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]
Beispiel #3
0
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
Beispiel #4
0
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")
Beispiel #5
0
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
Beispiel #6
0
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")
Beispiel #7
0
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
Beispiel #8
0
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