One-off encryption?

Hey all,
I am looking for a way to encode and then encrypt a payload that will later be passed to a webhook in the web app.

We’re talking something like “put these two options in your config/config.exs and then call these two functions”.

What’s a very quick and low-friction way to encrypt a binary (and subsequently decrypt it)? I am not looking for the best security here; I am looking for something to discourage a potential attacker that might be able to sniff an HTTP request with an encoded parameter in it.

Hi! I see two scenarios (sorry if I’m misreading your post):

  1. You do not care about the encoded information being read but just want to make sure it was not tempered with. In this case signing might be sufficient and you can use Phoenix.Token — Phoenix v1.6.2 or similar which is low-friciton if you’re already using phoenix.
  2. If you want the encoded data to be safe from prying eyes use something like GitHub - danielberkompas/cloak: Elixir encryption library designed for Ecto which implements best practices around erlang crypto so you don’t need to worry about the details (like IVs). This is very close to "put these two options in your config/config.exs and then call these two functions”
3 Likes

I was hoping for something without an external dependency. Basically a way to call :crypto which is a bit confusing to me.

BTW I want the data secret. I assume encrypting it well also prevents tampering.

But if I can’t do that I’ll definitely use Cloak.

Well you can certainly choose to use :crypto directly but you’ll have to take care of IVs and padding yourself. We ended up doing something like this (note that this has a hardcoded IV size of 16):

defmodule CryptoOneTime do
  require Logger 
  
  def encrypt_binary(data) when is_binary(data) do
    initialization_vector = :crypto.strong_rand_bytes(16)
    plaintext = pad(data, 16)
    encrypted_text = :crypto.crypto_one_time(:aes_128_cbc, secret_key(), initialization_vector, plaintext, true)

    :base64.encode(initialization_vector <> encrypted_text)
  end

  def decrypt_binary(ciphertext) when is_binary(ciphertext) do
    <<initialization_vector::binary-16, ciphertext::binary>> = Base.decode64!(ciphertext)

    plaintext =
      :aes_128_cbc
      |> :crypto.crypto_one_time(secret_key(), initialization_vector, ciphertext, false)
      |> unpad()

    {:ok, plaintext}
  rescue
    error in ArgumentError ->
      Logger.error(Exception.format(:error, error, __STACKTRACE__))
      :error
  end

  def decrypt_binary(_non_binary_value), do: :error

  defp secret_key do
    # this needs to be 128 bits of class-a randomness
  end

  defp unpad(data) do
    :binary.part(data, 0, byte_size(data) - :binary.last(data))
  end

  defp pad(data, block_size) do
    padding = block_size - rem(byte_size(data), block_size)
    data <> :binary.copy(<<padding>>, padding)
  end
end
1 Like

The best way to keep something secret is not to transmit it, encrypted or not. I would put said payload in a database, get the sequence id and just send the id with hashids

This way you send a very short string regardless how large is the payload.

1 Like

That’s what I want to do. Everything is in the DB, I just want the ID sent back securely. How secure is hashids though?

That looks good. I assume I’ll have to store the IV in config/config.exs and not only the secret key?

The IV is computed for every encrypt so that encrypting the same payload twice yields different results (similar idea as a salt). It’s prepended to the encrypted payload, so that it can be read and used for decryption. So you only need to keep the secret key in your config.exs (or runtime.exs more likely :slight_smile:)

1 Like

Thank you. Let me try it and I’ll comment back!

I just keep forgetting those encryption primitives, hence my question here. :smiley:

Hashid is not very secure. to add extra protection on tempering, you can append a sha3 hash.

1 Like

I see. But this seems to become a homegrown cryptography solution, which is something I want to avoid.

for inputs that lack entropy such as short integers, any encryption is not going to survive serious crpto attack. A breakable integer id and a secure hash of the original and un-transmitted payload still offer quite some protection.

EDIT: On a second thought, you can just send the secure hash, and use the hash as the primary key of your table to fetch the original payload. hashids is not needed here.

I get what you are saying. We’re not expecting serious attacks. I just don’t want that integer ID flying around naked on the net (even though it goes through HTTPS).

I still might go with hashids btw. I’ll take an hour or so Soon™ to evaluate both suggested approaches. Your has the advantage of being super simple while offering adequate protection.

We’re no crypto experts either, so I feel obligated to reference this SO post :smiley: the chosen cypher and padding seemed a good fit for our use-case where we’re encrypting a user ID that goes through a 3rd-party service and comes back via webhook as well.

1 Like

Why not just generate a random string, store it in the database and send that ? (or a hash like @derek-zhou said.)

So even if someone sniffs it, it is meaningless. Is there a fundamental problem to send a value that is also stored in the database, it the value is just a one-time key for fetching?

1 Like

If you already use Phoenix, you can use Phoenix.Token.encrypt/4 to encrypt a token with a given secret.

1 Like

That… I haven’t thought of that. That actually might be the best solution.

Thanks for showing me that I am dumb today. I needed it. :003:

1 Like

Nope, a very plain Plug project so Phoenix is off limits for now.

One caveat: In our case it was important that the same db record (a user) was not mapped to the same hash/encrypted text, because that would allow the external system to essentially track activity. If that’s no concern in your use case, some random bytes in your database are your best bet, indeed!

1 Like

We use several systems and our app is the glue + the persistent layer keeping track of it all. Your point is valid and well understood – but happily not critical in this case.

Worst case scenario is that one of those external systems will know we’re sending some numbers back to our webhook. And those numbers expire and are unusable minutes later.