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.
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.
I decided to checkout the 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?
Libsodium did a great job explaining the Sealed boxes concept.
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:
nacl.box.keyPair()nacl.box() by combining the ephemeral secret key and the repository public keyTo 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.