Encrypting GitHub Secrets using Go

Want to start encrypting right away? Skip to GitHub or install the package.

When programatically storing a secret value in a repository for use in a GitHub Actions workflow, you must first encrypt the value using libsodium. If you want to do this in Golang, there are basically two choices: bind the libsodium C library, for which there are Go packages available1, or use APIs provided by the golang.org/x/crypto package.

In this article I’ll demonstrate how to encrypt your GitHub secrets in Go using the latter approach, without the need to bind libsodium.

The high-level steps for programatically storing a GitHub secret are:

  1. Obtain the public key for the repository2 in which you want to store the secret.
  2. Encrypt your secret value using that public key.
  3. Set the encrypted secret in your repository.

This article assumes you already have the necessary access permissions for creating secrets on your repository and that you’re comfortable calling REST endpoints from Go, so focus will be on the encryption. However, a full end-to-end example of this workflow is available on GitHub.

Obtaining The Public Key

Before we can do anything, we need to get the repository’s public key, which we’ll use when encrypting secret values.

GitHub returns a JSON response containing the key and key_id. Hang on to both of these – we’ll need the former for encrypting secret values and the latter for storing them.

Encrypting The Secret

GitHub uses “sealed box” encryption from libsodium to encrypt and decrypt secrets. The corresponding functionality we need for encrypting secrets is in two Go packages:

go get golang.org/x/crypto/blake2b
go get golang.org/x/crypto/nacl/box

The blake2b package contains cryptographic hashing functionality needed for combining public keys into a nonce, and the nacl/box package contains the NaCl sealed box APIs we’ll use to encrypt our secret values.

On to the encryption function. We want a function, let’s call it naclEncrypt(), that will take the repo’s public key and secret value to be encrypted and return the encrypted content.

func naclEncrypt(recipientPublicKey string, content string) (string, error) {

}

Let’s implement it piece by piece. For clarity, I’ll omit most error handling code as we examine each step in detail.

1. Decode The Public Key

The public key we got from GitHub is base64-encoded and we must decode it. We’ll also need its contents as an array (not a slice), thus we copy the bytes into a [32]byte:

// decode the provided public key from base64
b, _ := base64.StdEncoding.DecodeString(recipientPublicKey)
recipientKey := new([32]byte)
copy(recipientKey[:], b)

2. Generate A Key Pair

Next, we need to create a 24-byte nonce – a one-time value that’s unique for each message we encrypt. We can’t just use any random value here; the nonce needs to be a hash of our repository’s public key and a another public key we generate randomly. We already have the former, so our next step is to generate a random key pair. The box package provides this mechanism:

// create an ephemeral key pair
pubKey, privKey, _ := box.GenerateKey(rand.Reader)

3. Create A Nonce

With our newly generated public key we can create our nonce. For this we use the blake2b package to hash together the two public keys. The nonce must be a an array, so just as with our public key, we copy the bytes from the hash’s resulting slice into a [24]byte:

// create the nonce by hashing together the two public keys
nonceHash, _ := blake2b.New(24, nil)
nonceHash.Write(pubKey[:])
nonceHash.Write(recipientKey[:])
nonce := new([24]byte)
copy(nonce[:], nonceHash.Sum(nil))

4. Encrypt The Content

Now we’re ready to encrypt our secret.

The first argument to box.Seal() specifies a slice that the encrypted content will be appended to. Protocol requires us to prepend the ephemeral public key to our encrypted secret data, so we’ll pass it as the first argument.

// begin the output with the ephemeral public key and append the encrypted content
out := box.Seal(pubKey[:], []byte(content), nonce, recipientKey, privKey)

The remaining arguments are the secret content to be encrypted, the nonce, the recipient public key, and the generated private key.

Finally, we just need to base64-encode the output:

// base64-encode the final output
return base64.StdEncoding.EncodeToString(out), nil

Done. The complete implementation can be found here.

Storing The Secret

Now that we’ve encrypted our secret, we’re ready to store it. In the request body, specify the encrypted_value and the key_id of the public key we obtained from the repository.

Package

This code is available as a Go package on GitHub.

go get github.com/jefflinse/githubsecret
import (
    "fmt"
    "log"
    "github.com/jefflinse/githubsecret"
)

func main() {
    publicKey := "jpEqkF3JrT+00cEQdpTAUnVWYdJJpkbghDSTndgRJKg="
    secret := "my super sensitive value"
    encrypted, err := githubsecret.Encrypt(publicKey, secret)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(encrypted)
}

  1. libsodium-go and Sodium, from libsodium’s “Bindings for other languages” ↩︎

  2. GitHub secrets can also be set at the organization level. The endpoints differ slightly but the workflow is the same. ↩︎