Using Libsodium in Cloudflare Workers and the Easter Egg I Found

While working on a bounty for Ubiquity, I needed to port the permit generation features of the bot to a separate module for use in the kernel. This module would be used in a Cloudflare Worker, which is a serverless environment for running JavaScript on Cloudflare's edge network. The module I was porting made use of Libsodium for decrypting secrets that allow the payments to be made. This is the story of how I got it working and the easter egg that made it all worth it.


The Problem

Despite the fact that Cloudflare Workers are built on top of V8, the same JavaScript engine that powers Node.js, there are some differences. One of these differences is that Cloudflare Workers do not have access to the Node.js standard library. This means that any native modules that are not included in the Cloudflare Worker runtime must be compiled from source and included in the worker's code.

I was to avoid changing the kernel code as much as possible and so I could never invoke the permit module from the kernel, so my "do what needs done to get it operational" approach was to fire the module up as it's own worker. This was because I had to explicity undefine the globals, now what I mean by that is:

globalThis.window = undefined;
globalThis.importScripts = undefined;

Without this little trick you are bound for failure, but without knowing the knock-on effects this would have on the rest of the kernel, I had to make the decision to run it as it's own worker. This meant that I had to find a way to use Libsodium in a Cloudflare Worker.

Traditionally, when using Libsodium you have to await the sodium.ready promise before you can use the library. This is because Libsodium is compiled to WebAssembly and must be loaded before it can be used. Heavy lifting functions such as scalarMult, crypto_box_seal_open etc are not available, luckily some of the smaller helper functions are available which came in handy. However, when I tried to use the library in my worker, that promise would never resolve. I tried everything I could think of to get it to work, but nothing I did would make the promise resolve.

The Solution

After a lot of trial and error, I finally found a solution but it meant making use of another library called tweetnacl.

The most striking difference between the two is that Libsodium handles nonce management for you, while tweetnacl does not. This means that you have to manage the nonce yourself when using tweetnacl. This is a small price to pay for the ability to use a library that actually works but it meant having to reverse engineer how libsodium handles encryption, nonce generation and decryption.

The tl;dr of it is this:

  • Libsodium generates a nonce by hashing the ephemeral key and the public key of the recipient. That nonce is then with the recipient's public key and the ephemeral seceret key to encrypt the message.

  • The epk is then prepended to the encrypted message, which you can extract and use to decrypt the message using tweetnacl.

  • First derive the nonce by hashing the recipient's public key and the ephemeral key ensuring you match the length of the nonce to the length of the nonce used by libsodium (24 bytes).

  • Then use the box.open function of tweetnacl to decrypt the message using the derived nonce, the epk and the recipient's private key.

As the only functions I had to replace were scalarMult and crypto_box_seal_open I was able to use the rest of the helper functions from Libsodium. This was a huge relief as it meant I didn't have to reverse engineer the entire library but it stands to reason that if you are tied to having to use Libsodium in a Cloudflare Worker and experience the same issue I did, you can likely apply the same solution (accounting for differences in the functions you need to replace).

The Easter Egg

While I was still in the "read the bundled code" phase, I found a little easter egg in the code. The code for the libsodium-wrappers relies on this function to load the wasm module:

try {
    a();
    var _ = new Uint8Array([98, 97, 108, 108, 115])
    , n = e.randombytes_buf(e.crypto_secretbox_NONCEBYTES)
    , s = e.randombytes_buf(e.crypto_secretbox_KEYBYTES)
    , c = e.crypto_secretbox_easy(_, n, s)
    , o = e.crypto_secretbox_open_easy(c, n, s);
    if (e.memcmp(_, o))
    return
} catch (e) {
    if (null == t.useBackupModule)
    throw new Error("Both wasm and asm failed to load" + e)
}
    t.useBackupModule(),
    a()

a()
here is the initialize function for the wasm module. The easter egg is in the line
var _ = new Uint8Array([98, 97, 108, 108, 115])

So the simple check is that if the wasm module is loaded, the function will encrypt the string and then decrypt it. If the decrypted string is not what it should be, then the wasm module is not loaded and the backup module is used. This is a very simple way to check if the wasm module is loaded and working as expected.

Now imagine it, you've been there. You are balls deep in the bundled source code, your tracking consonants and vowels, eyes are bloodshot and losing depth perception on the verge of giving up for the day and then you see it.

B A L L S

I laughed for a good 5 minutes, train of thought completely derailed. It was a good laugh and it made the whole debugging debacle worth it.


Conclusion

In the end, I was able to hack together a solution that allowed me to 'use Libsodium' in a Cloudflare Worker. Though it meant having to reverse engineer a little piece of it while still relying on it's lighter helper functions and use tweetnacl for those heavier functions.

I was able to generate those Permit2 signatures from my worker, admittely it was being invoked first through my ChatGPT plugin in response to a comment on a GitHub issue but has since had to undergo changes as to which context it needs to run in regards to the kernel.

The easter egg was a nice touch and I hope to see more of these out there in the wild. If ever I get the chance to myself I'll take it.

I hope this post has been helpful to you. If you have any questions or comments, feel free to reach out to me on Twitter or GitHub.