Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Cryptopals Set 2

set2.livemd

Cryptopals Set 2

Setup

Mix.install([
  {:kino, "~> 0.5.2"}
])
:ok

9. Implement PKCS#7 padding

A block cipher transforms a fixed-sized block (usually 8 or 16 bytes) of plaintext into ciphertext. But we almost never want to transform a single block; we encrypt irregularly-sized messages.

One way we account for irregularly-sized messages is by padding, creating a plaintext that is an even multiple of the blocksize. The most popular padding scheme is called PKCS#7.

So: pad any block to a specific block length, by appending the number of bytes of padding to the end of the block. For instance,

"YELLOW SUBMARINE"

… padded to 20 bytes would be:

"YELLOW SUBMARINE\x04\x04\x04\x04"
defmodule Cryptopals.Set2.Challenge9 do
  def pkcs7(plaintext, blocksize) do
    padding = blocksize - rem(byte_size(plaintext), blocksize)
    plaintext <> :binary.copy(<>, padding)
  end
end
{:module, Cryptopals.Set2.Challenge9, <<70, 79, 82, 49, 0, 0, 6, ...>>, {:pkcs7, 2}}

10. Implement CBC mode

CBC mode is a block cipher mode that allows us to encrypt irregularly-sized messages, despite the fact that a block cipher natively only transforms individual blocks.

In CBC mode, each ciphertext block is added to the next plaintext block before the next call to the cipher core.

The first plaintext block, which has no associated previous ciphertext block, is added to a “fake 0th ciphertext block” called the initialization vector, or IV.

Implement CBC mode by hand by taking the ECB function you wrote earlier, making it encrypt instead of decrypt (verify this by decrypting whatever you encrypt to test), and using your XOR function from the previous exercise to combine them.

The file here is intelligible (somewhat) when CBC decrypted against “YELLOW SUBMARINE” with an IV of all ASCII 0 (\x00\x00\x00 &c)


Don’t cheat. Do not use OpenSSL’s CBC code to do CBC mode, even to verify your results. What’s the point of even doing this stuff if you aren’t going to learn from it?

defmodule Cryptopals.Set2.Challenge10 do
  import Cryptopals.Set2.Challenge9, only: [pkcs7: 2]
  use Bitwise

  @blocksize 16

  def xor(a, b) when is_bitstring(a) and is_bitstring(b) do
    bxor(:binary.decode_unsigned(a), :binary.decode_unsigned(b))
    |> :binary.encode_unsigned()
  end

  def aes_cbc_encrypt(plaintext, key, iv) do
    aes_cbc_encrypt_helper(pkcs7(plaintext, @blocksize), key, iv)
  end

  def aes_cbc_encrypt_helper(<<>>, _key, _prev_block) do
    <<>>
  end

  def aes_cbc_encrypt_helper(
        <>,
        key,
        prev_block
      ) do
    block =
      plaintext_block
      |> xor(prev_block)

    ciphertext_block = :crypto.crypto_one_time(:aes_128_ecb, key, block, encrypt: true)

    ciphertext_block <> aes_cbc_encrypt_helper(rst, key, ciphertext_block)
  end

  def aes_cbc_decrypt(<<>>, _key, _prev_block) do
    <<>>
  end

  # Note we're using strip_padding from a future challenge
  import Cryptopals.Set2.Challenge15, only: [strip_padding: 1]

  def aes_cbc_decrypt(
        <>,
        key,
        prev_block
      ) do
    :crypto.crypto_one_time(:aes_128_ecb, key, ciphertext_block, encrypt: false)
    |> xor(prev_block)
    |> strip_padding()
  end

  def aes_cbc_decrypt(
        <>,
        key,
        prev_block
      ) do
    plaintext_block =
      :crypto.crypto_one_time(:aes_128_ecb, key, ciphertext_block, encrypt: false)
      |> xor(prev_block)

    plaintext_block <> aes_cbc_decrypt(rst, key, ciphertext_block)
  end
end
{:module, Cryptopals.Set2.Challenge10, <<70, 79, 82, 49, 0, 0, 12, ...>>, {:aes_cbc_decrypt, 3}}
challenge10_input = Kino.Input.textarea("Challenge 10")
challenge10_ciphertext =
  Kino.Input.read(challenge10_input)
  |> String.replace("\n", "")
  |> Base.decode64!()

key = "YELLOW SUBMARINE"
iv = :binary.copy("\x00", 16)

Cryptopals.Set2.Challenge10.aes_cbc_decrypt(challenge10_ciphertext, key, iv)
|> IO.puts()
I'm back and I'm ringin' the bell 
A rockin' on the mike while the fly girls yell 
In ecstasy in the back of me 
Well that's my DJ Deshay cuttin' all them Z's 
Hittin' hard and the girlies goin' crazy 
Vanilla's on the mike, man I'm not lazy. 

I'm lettin' my drug kick in 
It controls my mouth and I begin 
To just let it flow, let my concepts go 
My posse's to the side yellin', Go Vanilla Go! 

Smooth 'cause that's the way I will be 
And if you don't give a damn, then 
Why you starin' at me 
So get off 'cause I control the stage 
There's no dissin' allowed 
I'm in my own phase 
The girlies sa y they love me and that is ok 
And I can dance better than any kid n' play 

Stage 2 -- Yea the one ya' wanna listen to 
It's off my head so let the beat play through 
So I can funk it up and make it sound good 
1-2-3 Yo -- Knock on some wood 
For good luck, I like my rhymes atrocious 
Supercalafragilisticexpialidocious 
I'm an effect and that you can bet 
I can take a fly girl and make her wet. 

I'm like Samson -- Samson to Delilah 
There's no denyin', You can try to hang 
But you'll keep tryin' to get my style 
Over and over, practice makes perfect 
But not if you're a loafer. 

You'll get nowhere, no place, no time, no girls 
Soon -- Oh my God, homebody, you probably eat 
Spaghetti with a spoon! Come on and say it! 

VIP. Vanilla Ice yep, yep, I'm comin' hard like a rhino 
Intoxicating so you stagger like a wino 
So punks stop trying and girl stop cryin' 
Vanilla Ice is sellin' and you people are buyin' 
'Cause why the freaks are jockin' like Crazy Glue 
Movin' and groovin' trying to sing along 
All through the ghetto groovin' this here song 
Now you're amazed by the VIP posse. 

Steppin' so hard like a German Nazi 
Startled by the bases hittin' ground 
There's no trippin' on mine, I'm just gettin' down 
Sparkamatic, I'm hangin' tight like a fanatic 
You trapped me once and I thought that 
You might have it 
So step down and lend me your ear 
'89 in my time! You, '90 is my year. 

You're weakenin' fast, YO! and I can tell it 
Your body's gettin' hot, so, so I can smell it 
So don't be mad and don't be sad 
'Cause the lyrics belong to ICE, You can call me Dad 
You're pitchin' a fit, so step back and endure 
Let the witch doctor, Ice, do the dance to cure 
So come up close and don't be square 
You wanna battle me -- Anytime, anywhere 

You thought that I was weak, Boy, you're dead wrong 
So come on, everybody and sing this song 

Say -- Play that funky music Say, go white boy, go white boy go 
play that funky music Go white boy, go white boy, go 
Lay down and boogie and play that funky music till you die. 

Play that funky music Come on, Come on, let me hear 
Play that funky music white boy you say it, say it 
Play that funky music A little louder now 
Play that funky music, white boy Come on, Come on, Come on 
Play that funky music 
:ok

11. An ECB/CBC detection oracle

Now that you have ECB and CBC working:

Write a function to generate a random AES key; that’s just 16 random bytes.

Write a function that encrypts data under an unknown key — that is, a function that generates a random key and encrypts under it.

The function should look like:

encryption_oracle(your-input)
=> [MEANINGLESS JIBBER JABBER]

Under the hood, have the function append 5-10 bytes (count chosen randomly) before the plaintext and 5-10 bytes after the plaintext.

Now, have the function choose to encrypt under ECB 1/2 the time, and under CBC the other half (just use random IVs each time for CBC). Use rand(2) to decide which to use.

Detect the block cipher mode the function is using each time. You should end up with a piece of code that, pointed at a block box that might be encrypting ECB or CBC, tells you which one is happening.

defmodule Cryptopals.Set2.Challenge11 do
  import Cryptopals.Set2.Challenge10
  @blocksize 16

  def encryption_oracle(input) do
    padded_input = :rand.bytes(Enum.random(5..10)) <> input <> :rand.bytes(Enum.random(5..10))

    case Enum.random([:cbc, :ecb]) do
      :cbc ->
        encrypted =
          aes_cbc_encrypt(padded_input, :rand.bytes(@blocksize), :rand.bytes(@blocksize))

        {:cbc, encrypted}

      :ecb ->
        encrypted =
          :crypto.crypto_one_time(:aes_128_ecb, :rand.bytes(@blocksize), padded_input,
            encrypt: true
          )

        {:ecb, encrypted}
    end
  end

  # We just generate a text with lots of repeating 16-byte blocks and compare to see if two 16-byte sequences (after we drop some padding) are the same
  # input = :binary.copy("YELLOW SUBMARINE", 20)
  def detect_mode(output) do
    <<_::binary-size(20), a::binary-size(16), b::binary-size(16), _::binary>> = output

    if a == b do
      :ecb
    else
      :cbc
    end
  end
end
{:module, Cryptopals.Set2.Challenge11, <<70, 79, 82, 49, 0, 0, 10, ...>>, {:detect_mode, 1}}

12. Byte-at-a-time ECB decryption (Simple)

Copy your oracle function to a new function that encrypts buffers under ECB mode using a consistent but unknown key (for instance, assign a single random key, once, to a global variable).

Now take that same function and have it append to the plaintext, BEFORE ENCRYPTING, the following string:

Um9sbGluJyBpbiBteSA1LjAKV2l0aCBteSByYWctdG9wIGRvd24gc28gbXkg
aGFpciBjYW4gYmxvdwpUaGUgZ2lybGllcyBvbiBzdGFuZGJ5IHdhdmluZyBq
dXN0IHRvIHNheSBoaQpEaWQgeW91IHN0b3A/IE5vLCBJIGp1c3QgZHJvdmUg
YnkK

Spoiler alert.

Do not decode this string now. Don’t do it.


Base64 decode the string before appending it. Do not base64 decode the string by hand; make your code do it. The point is that you don’t know its contents.

What you have now is a function that produces:

AES-128-ECB(your-string || unknown-string, random-key)

It turns out: you can decrypt “unknown-string” with repeated calls to the oracle function!

Here’s roughly how:

  1. Feed identical bytes of your-string to the function 1 at a time — start with 1 byte (“A”), then “AA”, then “AAA” and so on. Discover the block size of the cipher. You know it, but do this step anyway.
  2. Detect that the function is using ECB. You already know, but do this step anyways.
  3. Knowing the block size, craft an input block that is exactly 1 byte short (for instance, if the block size is 8 bytes, make “AAAAAAA”). Think about what the oracle function is going to put in that last byte position.
  4. Make a dictionary of every possible last byte by feeding different strings to the oracle; for instance, “AAAAAAAA”, “AAAAAAAB”, “AAAAAAAC”, remembering the first block of each invocation.
  5. Match the output of the one-byte-short input to one of the entries in your dictionary. You’ve now discovered the first byte of unknown-string.
  6. Repeat for the next byte.

Congratulations.

This is the first challenge we’ve given you whose solution will break real crypto. Lots of people know that when you encrypt something in ECB mode, you can see penguins through it. Not so many of them can decrypt the contents of those ciphertexts, and now you can. If our experience is any guideline, this attack will get you code execution in security tests about once a year.

defmodule Cryptopals.Set2.Challenge12 do
  @unknown_string """
                  Um9sbGluJyBpbiBteSA1LjAKV2l0aCBteSByYWctdG9wIGRvd24gc28gbXkg
                  aGFpciBjYW4gYmxvdwpUaGUgZ2lybGllcyBvbiBzdGFuZGJ5IHdhdmluZyBq
                  dXN0IHRvIHNheSBoaQpEaWQgeW91IHN0b3A/IE5vLCBJIGp1c3QgZHJvdmUg
                  YnkK
                  """
                  |> String.replace("\n", "")
                  |> Base.decode64!()

  @random_key :rand.bytes(16)

  def oracle(input) do
    :crypto.crypto_one_time(:aes_128_ecb, @random_key, input <> @unknown_string,
      encrypt: true,
      padding: :pkcs_padding
    )
  end
end
{:module, Cryptopals.Set2.Challenge12, <<70, 79, 82, 49, 0, 0, 7, ...>>, {:oracle, 1}}
# 1) Find the blocksize

byte_size(Cryptopals.Set2.Challenge12.oracle("AAAAAA")) -
  byte_size(Cryptopals.Set2.Challenge12.oracle("AAAAA"))
16
# 2) Confirm ECB
# pattern match on two identical blocks
<> =
  Cryptopals.Set2.Challenge12.oracle(:binary.copy("A", 32))
<<123, 54, 65, 89, 78, 108, 121, 165, 146, 141, 39, 105, 139, 42, 72, 98, 123, 54, 65, 89, 78, 108,
  121, 165, 146, 141, 39, 105, 139, 42, 72, 98, 119, 85, 159, 29, 147, 23, 72, 118, 244, 122, 223,
  142, 62, 71, 163, 39, 238, 39, ...>>
defmodule Cryptopals.Set2.Challenge12Solution do
  import Cryptopals.Set2.Challenge12

  @blocksize 16

  def decrypt(block_num \\ 0, prev_block \\ :binary.copy("A", @blocksize), plaintext \\ "") do
    block_start = @blocksize * block_num

    plaintext_block =
      Enum.reduce(1..@blocksize, "", fn len, decrypted ->
        prefix = binary_part(prev_block, len, @blocksize - len) <> decrypted

        candidates =
          for byte <- 0..255, into: %{} do
            <> = oracle(prefix <> <>)

            {block, <>}
          end

        <<_::binary-size(block_start), block::binary-size(@blocksize), _::binary>> =
          oracle(:binary.copy("A", @blocksize - len))

        # TODO: I'm not getting the padding out of my dictionary (hence the ""). Something wonky
        decrypted <> Map.get(candidates, block, "")
      end)

    if byte_size(oracle("")) == block_start + @blocksize do
      plaintext <> plaintext_block
    else
      decrypt(block_num + 1, plaintext_block, plaintext <> plaintext_block)
    end
  end
end

#  

IO.puts(Cryptopals.Set2.Challenge12Solution.decrypt())
Rollin' in my 5.0
With my rag-top down so my hair can blow
The girlies on standby waving just to say hi
Did you stop? No, I just drove by

:ok

13. ECB cut-and-paste

Write a k=v parsing routine, as if for a structured cookie. The routine should take:

foo=bar&baz=qux&zap=zazzle

… and produce:

{
  foo: 'bar',
  baz: 'qux',
  zap: 'zazzle'
}

(you know, the object; I don’t care if you convert it to JSON).

Now write a function that encodes a user profile in that format, given an email address. You should have something like:

profile_for("foo@bar.com")

… and it should produce:

{
  email: 'foo@bar.com',
  uid: 10,
  role: 'user'
}

… encoded as:

email=foo@bar.com&uid=10&role=user

Your “profile_for” function should not allow encoding metacharacters (& and =). Eat them, quote them, whatever you want to do, but don’t let people set their email address to “foo@bar.com&role=admin”.

Now, two more easy functions. Generate a random AES key, then:

  1. Encrypt the encoded user profile under the key; “provide” that to the “attacker”.
  2. Decrypt the encoded user profile and parse it.

Using only the user input to profile_for() (as an oracle to generate “valid” ciphertexts) and the ciphertexts themselves, make a role=admin profile.

defmodule Cryptopals.Set2.Challenge13 do
  @random_key :rand.bytes(16)

  def profile_for(email) do
    # Note that Elixir's map is ordering the keys for us
    # The instructions want the order a certain way, which makes this problem much easier
    # So I'm using a keyword list to get it (and kind of cheat)
    [email: email, uid: 10, role: "user"]
    |> URI.encode_query()
  end

  def encrypted_profile_for(email) do
    profile = profile_for(email)

    :crypto.crypto_one_time(:aes_128_ecb, @random_key, profile,
      encrypt: true,
      padding: :pkcs_padding
    )
  end

  def decrypt_profile(encrypted_profile) do
    :crypto.crypto_one_time(:aes_128_ecb, @random_key, encrypted_profile,
      encrypt: false,
      padding: :pkcs_padding
    )
    |> URI.decode_query()
  end
end
{:module, Cryptopals.Set2.Challenge13, <<70, 79, 82, 49, 0, 0, 8, ...>>, {:decrypt_profile, 1}}
# 1. Get a block that ends in com&role=

email1 = :binary.copy("A", 10) <> "com"

<<_::binary-size(16), role_block::binary-size(16), rst::binary>> =
  Cryptopals.Set2.Challenge13.encrypted_profile_for(email1)

# 2. Get a block that starts with admin&, and make the email valid

email2 = "foo@bar.admin"

<> =
  Cryptopals.Set2.Challenge13.encrypted_profile_for(email2)

# 3. Put them together

admin_profile =
  Cryptopals.Set2.Challenge13.decrypt_profile(a <> role_block <> admin_block <> rst)
  |> IO.inspect()

admin_profile["role"] == "admin"
%{"email" => "foo@bar.com", "role" => "admin", "roluser" => "", "uid" => "10"}
true

14. Byte-at-a-time ECB decryption (Harder)

Take your oracle function from #12. Now generate a random count of random bytes and prepend this string to every plaintext. You are now doing:

AES-128-ECB(random-prefix || attacker-controlled || target-bytes, random-key)

Same goal: decrypt the target-bytes.


Stop and think for a second. What’s harder than challenge #12 about doing this? How would you overcome that obstacle? The hint is: you’re using all the tools you already have; no crazy math is required.

Think “STIMULUS” and “RESPONSE”.

defmodule Cryptopals.Set2.Challenge14 do
  @unknown_string """
                  Um9sbGluJyBpbiBteSA1LjAKV2l0aCBteSByYWctdG9wIGRvd24gc28gbXkg
                  aGFpciBjYW4gYmxvdwpUaGUgZ2lybGllcyBvbiBzdGFuZGJ5IHdhdmluZyBq
                  dXN0IHRvIHNheSBoaQpEaWQgeW91IHN0b3A/IE5vLCBJIGp1c3QgZHJvdmUg
                  YnkK
                  """
                  |> String.replace("\n", "")
                  |> Base.decode64!()

  @random_key :rand.bytes(16)
  @random_prefix :rand.bytes(Enum.random(5..40))

  def oracle(input) do
    :crypto.crypto_one_time(:aes_128_ecb, @random_key, @random_prefix <> input <> @unknown_string,
      encrypt: true,
      padding: :pkcs_padding
    )
  end
end
{:module, Cryptopals.Set2.Challenge14, <<70, 79, 82, 49, 0, 0, 7, ...>>, {:oracle, 1}}
defmodule Cryptopals.Set2.Challenge14Solution do
  import Cryptopals.Set2.Challenge14

  @blocksize 16

  def find_ciphrertext_prefix_size() do
    find_ciphrertext_prefix_size(oracle(:binary.copy("A", 200)), 0)
  end

  def find_ciphrertext_prefix_size(ciphertext, block_num) do
    case ciphertext do
      <> ->
        block_num * @blocksize

      <<_::binary-size(@blocksize), rst::binary>> ->
        find_ciphrertext_prefix_size(rst, block_num + 1)
    end
  end

  def find_plaintext_prefix_size(0), do: 0

  def find_plaintext_prefix_size(ciphertext_prefix_size, num \\ 16) when num > 0 do
    case oracle(:binary.copy("_", num) <> :binary.copy("A", 200)) do
      <<_::binary-size(ciphertext_prefix_size), a::binary-size(@blocksize),
        a::binary-size(@blocksize), _::binary>> ->
        num

      _ ->
        find_plaintext_prefix_size(ciphertext_prefix_size, num - 1)
    end
  end

  def decrypt() do
    ciphertext_prefix_size = find_ciphrertext_prefix_size()
    plaintext_prefix_size = find_plaintext_prefix_size(ciphertext_prefix_size)
    plaintext_prefix = :binary.copy("_", plaintext_prefix_size)
    decrypt_helper(plaintext_prefix, ciphertext_prefix_size)
  end

  def decrypt_helper(
        plaintext_prefix,
        ciphertext_prefix_size,
        block_num \\ 0,
        prev_block \\ :binary.copy("A", @blocksize),
        plaintext \\ ""
      ) do
    block_start = ciphertext_prefix_size + @blocksize * block_num

    plaintext_block =
      Enum.reduce(1..@blocksize, "", fn len, decrypted ->
        prefix = plaintext_prefix <> binary_part(prev_block, len, @blocksize - len) <> decrypted

        candidates =
          for byte <- 0..255, into: %{} do
            <<_::binary-size(ciphertext_prefix_size), block::binary-size(@blocksize), _::binary>> =
              oracle(prefix <> <>)

            {block, <>}
          end

        <<_::binary-size(block_start), block::binary-size(@blocksize), _::binary>> =
          oracle(plaintext_prefix <> :binary.copy("A", @blocksize - len))

        # TODO: I'm not getting the padding out of my dictionary (hence the ""). Something wonky
        decrypted <> Map.get(candidates, block, "")
      end)

    if byte_size(oracle(plaintext_prefix)) == block_start + @blocksize do
      plaintext <> plaintext_block
    else
      decrypt_helper(
        plaintext_prefix,
        ciphertext_prefix_size,
        block_num + 1,
        plaintext_block,
        plaintext <> plaintext_block
      )
    end
  end
end

Cryptopals.Set2.Challenge14Solution.decrypt()
|> IO.puts()
Rollin' in my 5.0
With my rag-top down so my hair can blow
The girlies on standby waving just to say hi
Did you stop? No, I just drove by

:ok

15. PKCS#7 padding validation

Write a function that takes a plaintext, determines if it has valid PKCS#7 padding, and strips the padding off.

The string:

"ICE ICE BABY\x04\x04\x04\x04"

… has valid padding, and produces the result “ICE ICE BABY”.

The string:

"ICE ICE BABY\x05\x05\x05\x05"

… does not have valid padding, nor does:

"ICE ICE BABY\x01\x02\x03\x04"

If you are writing in a language with exceptions, like Python or Ruby, make your function throw an exception on bad padding.

Crypto nerds know where we’re going with this. Bear with us.

defmodule Cryptopals.Set2.Challenge15 do
  @blocksize 16

  def strip_padding(""), do: ""

  def strip_padding(<>) do
    # cheating by knowning the blocksize
    # We get a free exception from the cases not matching
    case block do
      <<16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16>> -> ""
      <> -> c
      <> -> c
      <> -> c
      <> -> c
      <> -> c
      <> -> c
      <> -> c
      <> -> c
      <> -> c
      <> -> c
      <> -> c
      <> -> c
      <> -> c
      <> -> c
      <> -> c
    end
  end

  def strip_padding(plaintext) when byte_size(plaintext) > @blocksize do
    <> = plaintext
    <>
  end
end
warning: variable "rst" is unused (if the variable is not meant to be used, prefix it with an underscore)
  set2.livemd#cell:30: Cryptopals.Set2.Challenge15.strip_padding/1
{:module, Cryptopals.Set2.Challenge15, <<70, 79, 82, 49, 0, 0, 12, ...>>, {:strip_padding, 1}}

16. CBC bitflipping attacks

Generate a random AES key.

Combine your padding code and CBC code to write two functions.

The first function should take an arbitrary input string, prepend the string:

"comment1=cooking%20MCs;userdata="

.. and append the string:

";comment2=%20like%20a%20pound%20of%20bacon"

The function should quote out the “;” and “=” characters.

The function should then pad out the input to the 16-byte AES block length and encrypt it under the random AES key.

The second function should decrypt the string and look for the characters “;admin=true;” (or, equivalently, decrypt, split the string on “;”, convert each resulting string into 2-tuples, and look for the “admin” tuple).

Return true or false based on whether the string exists.

If you’ve written the first function properly, it should not be possible to provide user input to it that will generate the string the second function is looking for. We’ll have to break the crypto to do that.

Instead, modify the ciphertext (without knowledge of the AES key) to accomplish this.

You’re relying on the fact that in CBC mode, a 1-bit error in a ciphertext block:

Completely scrambles the block the error occurs in Produces the identical 1-bit error(/edit) in the next ciphertext block. Stop and think for a second. Before you implement this attack, answer this question: why does CBC mode have this property?

defmodule Cryptopals.Set2.Challenge16 do
  import Cryptopals.Set2.Challenge10
  @blocksize 16
  @random_key :rand.bytes(@blocksize)
  @random_iv :rand.bytes(@blocksize)

  @prefix "comment1=cooking%20MCs;userdata="
  @postfix ";comment2=%20like%20a%20pound%20of%20bacon"

  def encrypt(input) do
    sanitized =
      input
      |> String.replace("=", "%3D")
      |> String.replace(";", "%3B")

    plaintext = @prefix <> sanitized <> @postfix
    aes_cbc_encrypt(plaintext, @random_key, @random_iv)

    #    :crypto.crypto_one_time(:aes_128_cbc, @random_key, plaintext, encrypt: true, padding: :pkcs_padding)
  end

  def decrypt(ciphertext) do
    aes_cbc_decrypt(ciphertext, @random_key, @random_iv)
  end

  def check_admin(ciphertext) do
    ciphertext
    |> decrypt()
    |> String.split(";")
    |> Enum.map(&amp;List.to_tuple(String.split(&amp;1, "=")))
    |> Enum.member?({"admin", "true"})
  end
end
{:module, Cryptopals.Set2.Challenge16, <<70, 79, 82, 49, 0, 0, 10, ...>>, {:check_admin, 1}}
# We need a binary xor again
defmodule Cryptopals.Utils do
  use Bitwise

  def xor(<<>>, _), do: <<>>

  def xor(<>, <>) do
    <> <> xor(a, b)
  end
end

# First we need to get a block to scramble
# Use 32 A's as input so we get two blocks of A's 
userdata = :binary.copy("A", 32)

# The prefix aligns nicely along blocks, so we get our first encrypted block of As 
<> =
  Cryptopals.Set2.Challenge16.encrypt(userdata)

# We want to bitflip our ciphertext so the next block is turned into the target
target =
  "AAAAA;admin=true"
  |> Cryptopals.Utils.xor(:binary.copy("A", 16))

scrambled_block = Cryptopals.Utils.xor(block, target)

# Now we sub in our scrambled block to get the result we want
new_ciphertext = prefix <> scrambled_block <> rst

# We're admin!
Cryptopals.Set2.Challenge16.check_admin(new_ciphertext)
true

Testing it all

ExUnit.start(autorun: false)

defmodule Set2Test do
  import Cryptopals.Set2.{Challenge9, Challenge11, Challenge13, Challenge15}
  use ExUnit.Case, async: true

  test "pkcs7 padding" do
    assert pkcs7("YELLOW SUBMARINE", 20) ==
             "YELLOW SUBMARINE\x04\x04\x04\x04"

    assert pkcs7("YELLOW SUBMARIN", 16) ==
             "YELLOW SUBMARIN\x01"

    assert pkcs7("YELLOW SUBMARINE", 16) ==
             "YELLOW SUBMARINE\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10"
  end

  test "ecb/cbc mode oracle" do
    input = :binary.copy("YELLOW SUBMARINE", 20)

    for _ <- 1..100 do
      {mode, output} = encryption_oracle(input)
      assert detect_mode(output) == mode
    end
  end

  test "profile_for" do
    assert profile_for("foo@bar.com") == "email=foo%40bar.com&uid=10&role=user"

    profile = profile_for("foo@bar.com&role=admin") |> URI.decode_query()
    assert profile["role"] == "user"
  end

  test "strip_padding" do
    assert strip_padding("ICE ICE BABY\x04\x04\x04\x04") == "ICE ICE BABY"

    assert_raise CaseClauseError, fn ->
      strip_padding("ICE ICE BABY\x05\x05\x05\x05")
    end

    assert_raise CaseClauseError, fn ->
      strip_padding("ICE ICE BABY\x01\x02\x03\x04")
    end
  end
end

ExUnit.run()
....

Finished in 0.00 seconds (0.00s async, 0.00s sync)
4 tests, 0 failures

Randomized with seed 849580
%{excluded: 0, failures: 0, skipped: 0, total: 4}