Skip to content

Commit

Permalink
docs: add blog post about libsodium (#400)
Browse files Browse the repository at this point in the history
  • Loading branch information
atinux authored Dec 19, 2024
1 parent 8671152 commit d6e5a41
Show file tree
Hide file tree
Showing 2 changed files with 148 additions and 0 deletions.
148 changes: 148 additions & 0 deletions docs/content/5.blog/4.libsodium-cloudflare-workers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
---
title: How to use Libsodium in Cloudflare Workers
description: Learn how to leverage NaCL to crypto box seal values, useful when
working with GitHub repository secrets.
image: '/images/blog/libsodium-cloudflare-workers.png'
authors:
- name: Sebastien Chopin
avatar:
src: https://avatars.githubusercontent.com/u/904724?v=4
to: https://x.com/atinux
username: atinux
date: 2024-12-19
category: Tutorial
---

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](https://docs.github.com/en/rest/guides/encrypting-secrets-for-the-rest-api?apiVersion=2022-11-28).

```ts
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](https://github.com/jedisct1/libsodium.js) NPM library, we quickly hit the [TypeError: Cannot read properties of undefined (reading 'href')](https://github.com/jedisct1/libsodium.js/issues/323) error when awaiting `.ready` .

We decided to not hack around it like [suggested here](https://github.com/jedisct1/libsodium.js/issues/212#issuecomment-1181238495).

## 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](https://nacl.cr.yp.to/), 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](https://tweetnacl.js.org/) 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](https://libsodium.gitbook.io/doc/public-key_cryptography/sealed_boxes) concept.

::callout
Sealed boxes are designed to anonymously send messages to a recipient given their public key. :br :br

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. :br :br

A message is encrypted using an ephemeral key pair, with the secret key being erased right after the encryption process.:br :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:

```text
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:

1. Generate an ephemeral key pair using `nacl.box.keyPair()`
2. Derive the nonce using Blake2b cryptographic hash function
3. Encrypt the message with `nacl.box()` by combining the ephemeral secret key and the repository public key
4. 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:

```bash [Terminal]
npm i tweetnacl blakejs
```

Then create the `cryptoBoxSeal` and `deviceNonce` methods in a Nuxt server util:

```ts [server/utils/crypto.ts]
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:

```diff [example.ts]
- // 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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit d6e5a41

Please sign in to comment.