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):
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.
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”
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
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.
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 )
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 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.
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?
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!
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.