-
Notifications
You must be signed in to change notification settings - Fork 60
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs: add blog post about libsodium (#400)
- Loading branch information
Showing
2 changed files
with
148 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.