peoj

peoj

Porting node.js encryption function to Elixir

Hi folks,

I’m attempting to port this JS code into its Erlang/Elixir equivalent:

const key = "mysecretkey";

function decryptString(encryptedString) {
    const algorithm = 'aes256';
    const decipher = crypto.createDecipher(algorithm, key);
    return decipher.update(encryptedString, 'hex', 'utf8') +
      decipher.final('utf8');
  }

This is as far as I have gotten so far in E land:

iex> key = "mysecretkey"
iex> encrypted_text = "myencryptedtext-changed-for-this-example"
iex> :crypto.crypto_one_time(:aes_256_ecb, key, encrypted_text, encrypt: false)
** (ErlangError) Erlang error: {:badarg, {'api_ng.c', 143}, 'Bad key size'}
    (crypto 4.8.3) :crypto.ng_crypto_one_time_nif(:aes_256_ecb, "mysecretkey", "", "myencryptedtext-changed-for-this-example", false, :undefined)

Clearly the precise example I’ve given will never work since I’ve redacted both the key and encrypted text. However the error message I’ve included above is the real thing.

Does anyone know what I’m missing?

Marked As Solved

mattbaker

mattbaker

I like crypto! And I like puzzles, and I had a suspicion Node was doing something silly, so I chased this a bit. I’ll say two things right off the bat:

First, this would be confusing to anyone. Node made some choices that favored ease of use, and sacrificed clarity (and security!) in the process. Node is doing things hidden from you that you couldn’t possibly have known about without knowing exactly what to search for. With a little experience with cryptography concepts it’s not bad, but if you’re new to this stuff (and you mentioned you are) this is really hard to solve without help.

Second, @hauleth is right that a revamp of how you encrypt things is warranted, but we don’t always have the luxury of doing that with legacy data! Best practices here do not solve the problem in front of you. For what it’s worth, I don’t think you’re encrypting in ECB in Node, I think you’re actually using CBC.

With all that said, to make any progress there are some things to figure out before we can decrypt the data Node is producing in Elixir.

Question 1: what block cipher mode is Node using?

You specified aes256 in your JS code and aes_256_ecb in Elixir. The string “aes256” doesn’t really tell us the whole story, because encryption is not just about which encryption algorithm is being used, it’s also about how each block of data is operated on during the encryption or decryption process. You’ve selected “ECB” (electronic code book) but there are others, like “CBC” (cipher block chaining). You really need to know which one is being used if you want to decrypt your data.

If you head down the rabbit hole far enough with the Node docs you’ll see that aes256 value you’re passing is from a list of ciphers provided by OpenSSL. Unfortunately, the string aes256 doesn’t tell us the block mode.

I couldn’t find a clear explanation of what aes256 truly maps to so I just ran openssl enc -aes256 to see what would happen. The first thing you’ll see is

enter aes-256-cbc encryption password:

So, that makes me think when you say “aes256” in your code in Node it’s really a shortcut for aes-256-cbc.

That means the first thing you have wrong is the algorithm, instead of ECB you should be using :aes_256_cbc.

Question 2: But if it’s CBC, what’s the initialization vector?

This wikipedia page talks a bit about block cipher modes. The primary thing to notice is the difference between ECB and CBC mode. You could also google other resources explaining block cipher modes that might be more clear than that wikipedia page.

If you look at the pictures describing how ECB works, you’ll see decryption needs two inputs: the key and some ciphertext (aka encrypted data).

You’ll also see that CBC expects three inputs. The key, the ciphertext, and something called an “initialization vector” (IV) to get things started.

Since you are (apparently) using CBC but not supplying an IV in your JS code, the only conclusion is that Node is generating an IV for you, and it must be doing it deterministically (not a great thing in the crypto world, one of the reasons the Node docs push people toward createCipheriv).

Luckily you’re not only one dealing with this. Here’s someone trying to solve the same problem in Ruby.

Summarizing the Stack Overflow post above: Node takes the “password” you provide (of any length) and uses that to produce an encryption key and an IV. So it turns out that not only is Node secretely making an IV for you, it’s also creating an encryption key that’s based on your “password” but not literally your password. In your example, the decryption key is not actually “mysecretkey” (no surprise, since it’s too short to be an encryption key!)

That’s all outlined in the Stack Overflow answer I linked to, which I recommend you read.

If we copy what they did in Ruby in Elixir, that process could look something like this:

a = :crypto.hash(:md5, password)
b = :crypto.hash(:md5, a <> password)
c = :crypto.hash(:md5, b <> password)

iv = a <> b
key = c

And finally, encoding

Last but not least, your JS code makes me think that your encrypted data is probably being passed around encoded as base 16 (hex), for example are you providing something like “f07258ace89d16d847cb0ec520b19438” as input when you try to decrypt data?

If so, that means you need to decode the hex string before you try to decrypt it. You can use Base.decode16!(encrypted_text, case: :lower) for this.

Putting it all together

So, if I had to summarize, it’d be this: Node is trying to be helpful by hiding a bunch of things from you, and that works if you never have to decrypt something outside the Node ecosystem, but it’s a crappy design if you do. In their defense, the createCipher function you’re using there has been deprecated in favor of a function that takes a true key (not a “password”) and an explicit IV value (you must generate it, they will not do it for you). I’m very happy they’ve deprecated the function you’re using.

The good news is you can definitely re-create what Node did behind the scenes so that you can decrypt your data in Elixir.

First, here’s an example of some JS code that I’m guessing is close to your own. We can use it to generate test data.

const crypto = require('crypto')

function decryptString(key, encryptedString) {
  const algorithm = 'aes256';
  const decipher = crypto.createDecipher(algorithm, key);
  return decipher.update(encryptedString, 'hex', 'utf8') +
    decipher.final('utf8');
}

function encryptString(key, plaintext) {
  const algorithm = 'aes256';
  const encipher = crypto.createCipher(algorithm, key);
  const encrypted = Buffer.concat([encipher.update(plaintext), encipher.final()]);
  return encrypted.toString("hex")
}


const password = "mysecretkey"
const plaintext = "hello world"

console.log("Plaintext: ", plaintext)

console.log("Password: ", password)

var encrypted = encryptString(password, plaintext)

console.log("Encrypted: ", encrypted)

var decrypted = decryptString(password, encrypted)

console.log("Decrypted: ", decrypted)

That produces the following output:

Plaintext:  hello world
Password:  mysecretkey
Encrypted:  686ca8793bf5e7317cfd451aa81b72cf
Decrypted:  hello world

So in Elixir our goal is to decrypt the string “686ca8793bf5e7317cfd451aa81b72cf” by mimicking Node. If we succeed then we know we’ve copied Node’s approach.

# As we discovered earlier, you want CBC:
algo = :aes_256_cbc

# Not literally your encryption key, just a value Node uses to generate a key:
password = "mysecretkey"

# Calling this "encoded" ciphertext because it looks like you're using hex strings:
encoded_ciphertext = "686ca8793bf5e7317cfd451aa81b72cf"

# Your ciphertext hex-decoded
ciphertext = Base.decode16!(encoded_ciphertext, case: :lower)

# Re-creating how Node generates the IV and key values from a "password" string
a = :crypto.hash(:md5, password)
b = :crypto.hash(:md5, a <> password)
c = :crypto.hash(:md5, b <> password)

iv = a <> b
key = c

# Decrypting the data. Notice that we're also specifying the padding so it's stripped.
:crypto.crypto_one_time(algo, iv, key, ciphertext, [encrypt: false, padding: :pkcs_padding])
|> IO.inspect()

That prints out “hello world” on my machine.

I hope that helps you get unstuck and away from Node :slight_smile:

If I had to suggest a new approach once you’re able to migrate off your legacy Node code:

  • Use a full randomly generated key, don’t generate one like Node does based on a short “password” string
  • Don’t feel tempted to use aes_256_ecb because it’s easier. Something like aes_256_cbc is fine as far as I know, and there are others.
  • Erlang’s built-in crypto stuff is, to my knowledge, perfectly fine to use
  • We didn’t find Cloak’s abstraction particularly useful, but it’s possible you might, certainly worth reading up on it once you get there.
41
Post #6

Also Liked

emeryotopalik

emeryotopalik

Aren’t you supposed to be on vacation?

mindok

mindok

This has to be answer of the year!!! Awesome work.

hauleth

hauleth

AES256 require key to be exactly 32 byte long, your key is 11 bytes, 21 bytes too short. JS code probably does some padding to achieve what you want.

However:

For gods sake, do not use this code. Burry it in the dessert, and wear gloves. After that burn all your clothes.

Jokes aside - this is terrible way to do cryptography and encrypt data. Just don’t do it that way. If you have “AES” written in your code, then you are probably vulnerable. It is even hard to list what is wrong there:

  • low entropy keys - just use KDF for dear gods
  • no padding for data
  • no authentication of data
  • using ECB mode for encryption

This is nowhere to be even considered to be a remotely reasonable implementation. It is just insecure.

Last Post!

chriscohoat

chriscohoat

I know I’m a bit late to the game here … but I spent all day trying to port this related (and reasonably-looking NodeJS call):

const decryptedData = CryptoJS.AES.decrypt(
     ciphertext,
     passphrase
).toString(CryptoJS.enc.Utf8);

to Elixir. In my case I was passing in a Base64 encoded string with a Salted prefix.

In crypto-js there’s a function called parse:

https://github.com/brix/crypto-js/blob/develop/src/cipher-core.js#L610

My implementation (thanks a lot @mattbaker for your help):

defmodule SSODecryption do
  def parse(openSSLStr) do
    {:ok, ciphertext_b64} = Base.decode64(openSSLStr)

    <<salted_indicator::binary-size(8), salt::binary-size(8), ciphertext::binary>> =
      ciphertext_b64

    case salted_indicator do
      "Salted__" ->
        {salt, ciphertext}

      _ ->
        {:error, "No salt found"}
    end
  end

  def parse_and_decrypt(openSSLStr, passphrase, algo \\ :aes_256_cbc) do
    case parse(openSSLStr) do
      {:error, reason} ->
        {:error, reason}

      {salt, ciphertext} ->
        {key, iv} = derive_key_and_iv(passphrase, salt)
        perform_decryption(ciphertext, iv, key, algo)
    end
  end

  defp derive_key_and_iv(passphrase, salt) do
    # Concatenate the passphrase and salt as the starting point for hash computations.
    passphrase_salt = passphrase <> salt

    # Compute the first hash using the passphrase and salt.
    a = :crypto.hash(:md5, passphrase_salt)

    # For subsequent hashes, concatenate the result of the previous hash with the original passphrase and salt.
    b = :crypto.hash(:md5, a <> passphrase_salt)
    c = :crypto.hash(:md5, b <> passphrase_salt)

    # The IV is derived from the first two hash outputs.
    iv = a <> b

    # The key is derived from the third hash output.
    key = c

    {key, iv}
  end

  defp perform_decryption(ciphertext, iv, key, algo) do
    try do
      case :crypto.crypto_one_time(algo, iv, key, ciphertext,
             encrypt: false,
             padding: :pkcs_padding
           ) do
        result when is_binary(result) ->
          case Jason.decode(result) do
            {:ok, decoded} -> {:ok, decoded}
            {:error, _reason} -> {:error, "Failed to decode JSON"}
          end

        _reason ->
          {:error, "Decryption failed for an unknown reason"}
      end
    rescue
      _exception ->
        {:error, "Decryption failed"}
    end
  end
end

I’m not positive about the error handling and it only works for the salted case right now, but it seems to let me move some NodeJS code away. Thanks again for the answers on this thread.

Where Next?

Popular in Questions Top

vegabook
I’m brand new to Phoenix and I have stripped one of the demo applications to the bone. I just want to get an svg up on the screen. Here i...
New
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New

Other popular topics Top

grych
Hi folks, Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
639 54092 488
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New
sergio
Kind of like when jquery came out, it was super necessary. Existing drag and drop libraries have a bunch of baggage to support old browse...
New

We're in Beta

About us Mission Statement