Today I Was Confused — AES/CBC/PKCS5PADDING
Tags: node, php, json, encryption
Published: 2019-09-18 05:02:26

Date: 6 September 2019
Location: Mumbai, India
Weather: Rainy

I do not intend for this article to be educational in any capacity. But hey thanks for reading.

<hr>

One of our clients has a website and wants to use a third part API — I can’t say which — and so I’m given charge of the project. Sweet.

I get my hands on some example PHP code, the API docs and test credentials. Our story begins here, about how I got to a working implementation and how the innards of this implementation confused me.

<hr>

Let’s start with the fact that I know jack about encryption. All I know is that you plug in some text and a key, and get gibberish text out the other end. That’s about all I know about encryption.

And today I had to write code that would encrypt some text. Using AES/CBC/PKCS5PADDING.

I started by reading the example code and the API docs in tandem. They were pretty short so it didn’t take too long to understand how the code worked.

Essentially you had a JSON object, serialized the JSON to string, encrypted that string, and base64 encoded the ciphertext. Easy.

{% gist https://gist.github.com/vlence/091edf2cec96c977f4ddb8c288bfc726 %}

I proceeded to test my code and it worked — the API accepted my request and promptly sent me an error saying one of the fields was incorrect. In other words the API could correctly decrypt the encrypted data and tell me something was wrong with it. Noice.

<hr>

Life would be simple if you could just fix that field and call it a day. Well I couldn’t call it a day. Because I couldn’t fix that field. Because that field is supposed to be already correct…

Now the API docs say that this field expects only one, very specific, default value. Of course I was using that one, very specific, default value. And the API told me it was invalid. You wot m8?!

After taking a deep breath and a 30 minute break I decided it was time to binge on YouTube. During my binge a thought crossed my mind, “Maybe it’s my code. Maybe I should run the PHP code and see what’s what.” And so I ran the PHP code. And it ran just like mine — The API returned the same error for the same input. OK yay it’s not my code.

<hr>

Along the way I got curious about what the encrypted text looked like. And what do you know, my code and the PHP code produced different ciphertext. Time to play detective.

First rule to playing detective is that you don’t play detective with production code. Or staging code. Or dev code.

{% gist https://gist.github.com/vlence/5ffd810aec08658abcbb8e46c44865d0 %}

A quick glance™ and it’s obvious both tests do basically the same thing. But completely different results. How? Why? We need a second quick glance™.

Notice that in the JavaScript test file the data object has field1 defined before field2 and the opposite is true in the PHP test file. At this point I remembered something I had once read about chaos theory— A small change in the input brings about a large change in the output. So I made sure that in both test files the fields appeared in the same order.

The outputs were still different. But they were very similar. We’re getting closer to the truth. The inputs were the same, right? Both fields were in the exact same order and had the same values. Time to play detective v2. Let’s log the JSON encoding.

{% gist https://gist.github.com/vlence/0eea5ccd45cddc5573f682de838f1d6f %}

And there we have it. An extra backslash. Looks very similar to… Wait. Is `json_encode` escaping a forward slash? Yes. Yes it is. 🤯

You can disable this behavior by passing `JSON_UNESCAPED_SLASHES` to `json_encode`. Peace has returned to the valley. But here’s the thing — why is this default behavior? JSON doesn’t even require you to escape a forward slash AFAIK.

Why did I waste 5 hours on this?