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:
- Obtain the public key for the repository2 in which you want to store the secret.
- Encrypt your secret value using that public key.
- 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)
}
-
libsodium-go and Sodium, from libsodium’s “Bindings for other languages” ↩︎
-
GitHub secrets can also be set at the organization level. The endpoints differ slightly but the workflow is the same. ↩︎