How to use Libsodium in Cloudflare Workers
While working on the NuxtHub's GitHub app and action, we decided to synchronise the environment variables and secrets to the repository so both the runtime and build environments would be similar.
The problem
When creating repository secrets with the GitHub REST API, you need to encrypt them using the Libsodium library.
import libsodium from 'libsodium-wrappers'
// Check if libsodium is ready and then proceed.
await sodium.ready
const publicKey = 'repositoryProductionPublicKeyAsBase64'
// Convert base64 key to Uint8Array
const binaryPublicKey = Uint8Array.from(Buffer.from(publicKey.data.key, 'base64'))
// Convert string value to Uint8Array
const binarySecretValue = new TextEncoder().encode('my-secret-value')
// Encrypt with libsodium
const encryptedBytes = libsodium.crypto_box_seal(binaryValue, binaryKey)
// Convert to base64
const encryptedBase64 = Buffer.from(encryptedBytes).toString('base64')
// Call GitHub API with the encryptedBase64 value
By trying the libsodium-wrappers NPM library, we quickly hit the TypeError: Cannot read properties of undefined (reading 'href') error when awaiting .ready
.
We decided to not hack around it like suggested here.
Inside Libsodium: NaCl
I decided to checkout the Libsodium documentation which mentions that the library is a portable, cross-compilable, installable, and package-able fork of NaCl, with a compatible but extended API to improve usability even further.
As we only use the crypto_box_seal
function from Libsodium, I believed that we could directly use a compatible NaCl library that would work on edge runtimes.
This is how I found the TweetNaCl library that works perfectly in Cloudflare Workers.
One problem: tweetnacl
does not have a crypto_box_seal
method, so what to do?
Learning about Sealed Boxes
Libsodium did a great job explaining the Sealed boxes concept.
Only the recipient can decrypt these messages using their private key. While the recipient can verify the integrity of the message, they cannot verify the identity of the sender.
A message is encrypted using an ephemeral key pair, with the secret key being erased right after the encryption process.:br
Without knowing the secret key used for a given message, the sender cannot decrypt the message later. Furthermore, without additional data, a message cannot be correlated with the identity of its sender.
The documentation also gives us the algorithm implementation details:
ephemeral_pk ‖ box(m, recipient_pk, ephemeral_sk, nonce=blake2b(ephemeral_pk ‖ recipient_pk))
We can re-create the crypto box seal algorithm with NaCL:
- Generate an ephemeral key pair using
nacl.box.keyPair()
- Derive the nonce using Blake2b cryptographic hash function
- Encrypt the message with
nacl.box()
by combining the ephemeral secret key and the repository public key - Combine the ephemeral public key and our encrypted message
The Solution
To create our edge-compatible crypto_box_seal
function using tweetnacl
and blakejs
.
First, we need to install the dependencies:
npm i tweetnacl blakejs
Then create the cryptoBoxSeal
and deviceNonce
methods in a Nuxt server util:
import nacl from 'tweetnacl'
import { blake2b } from 'blakejs'
// Function to derive the nonce using Blake2b
function deriveNonce(ephemeralPublicKey: Uint8Array, recipientPublicKey: Uint8Array) {
// Concatenate ephemeralPublicKey ‖ recipientPublicKey
const input = new Uint8Array(
ephemeralPublicKey.length + recipientPublicKey.length
)
input.set(ephemeralPublicKey, 0)
input.set(recipientPublicKey, ephemeralPublicKey.length)
// Derive the nonce using Blake2b
return blake2b(input, undefined, nacl.box.nonceLength)
}
export function cryptoBoxSeal(message: Uint8Array, recipientPublicKey: Uint8Array) {
// Generate ephemeral key pair
const ephemeralKeyPair = nacl.box.keyPair()
// Derive the nonce using Blake2b
const nonce = deriveNonce(ephemeralKeyPair.publicKey, recipientPublicKey)
// Encrypt the message using the ephemeral secret key and recipient's public key
const encryptedMessage = nacl.box(
message,
nonce,
recipientPublicKey,
ephemeralKeyPair.secretKey
)
// Combine the ephemeral public key and encrypted message
const sealedBox = new Uint8Array(
ephemeralKeyPair.publicKey.length + encryptedMessage.length
)
// Similar signature from libsodium
// ephemeral_pk ‖ box(m, recipient_pk, ephemeral_sk, nonce=blake2b(ephemeral_pk ‖ recipient_pk))
sealedBox.set(ephemeralKeyPair.publicKey, 0)
sealedBox.set(encryptedMessage, ephemeralKeyPair.publicKey.length)
return sealedBox
}
Finally, we can replace our example above to use our new helper:
- // Encrypt with libsodium
- const encryptedBytes = libsodium.crypto_box_seal(binaryValue, binaryKey)
+ // Encrypt using cryptoBoxSeal
+ const encryptedBytes = cryptoBoxSeal(binaryValue, binaryPublicKey)
That's it! This was a good opportunity to learn more on how encrypting secrets work for GitHub and not be afraid on diving into how librairies work under the hood.