Sterling has too many projects Blogging about programming, microcontrollers & electronics, 3D printing, and whatever else...

Do not use libsodium with Go

| 676 words | 4 minutes | golang github garotate
Photo of a cell tower pretending to be a palm tree.

Updated 2023-01-11: This update is looooooong overdue. I had it dated as of May of last year and then never published it. My mistake. However, thanks to a reader, c0nscience, I am coming back around and geetting this correction made. He also suggested another implementation for reference by Jeff Linse on Github. After writing this post, I started working on some other projects and also started on another branch. Apparently, my testing of this change was very inadequate. I discovered, I’d actually been using the wrong method to perform the box sealing. I could make the Github change, but Github could not decrypt it. I have corrected the code below.

This is going to be a quick post from a lesson I learned recently: github gives bad advice (sort of1) on how to work with it’s API via Go. Certain of the calls require an additional bit of encryption in order to send some of the data. Specifically, if you are updating an action secret for a repository, you need to encrypt the secret. This is technically redundant since the entire API is transmitted with TLS in place. However, I think it’s reasonable given the number of odd setups there are out there for intercepting and logging traffic. However, I digress…

So, here’s some code that does what is apparently suggested by github for performing this encryption:

import (
    "encoding/base64"
    "github.com/jamesruan/sodium"
)

func encryptSecret(pk, secret string) (string, error) {
    pkgBox := sodium.BoxPublicKey{
        Bytes: sodium.Bytes([]byte(pk)),
    }

    keyBox := sodium.Bytes([]byte(secret))
    keySealed := keyBox.SealedBox(pkBox)
    keyEncSealed := base64.StdEncoding.EncodeToSstring(keySealed)

    return keyEncSealed, nil
}

However, this has a major drawback: It needs cgo. That in itself is not generally a major problem, but it can present some difficulties. For the project I’m working on, I want to generate several binaries for different operating systems and architectures as part of the release process. This requires a certain amount of cross compiling, which Go does pretty well as long as your program is entirely in Go. In theory, cross-compiling with cgo is possible, but is not something many Gophers do and so there’s a lot of debate on how to get it done. Even If I could, cross compiling the C-based LibSodium is even less straightforward. That’s a major pain.

However, there’s no reason at all to use this sodium library, and as pkg.go.dev only shows something like 2 or 3 uses (including my project) for the most popular libsodium wrapper library, I’m late to the game on learning this.

Instead…

LibSodium is just a fork of an older library named nacl, which hasn’t had a release in quite some time. However, the encryption algorithm used by the github API was implemented in the old library too, it’s just not maintained. Maintainance of the old original library is what LibSodium provides. As such, we don’t need LibSodium, but an implementation of the encryption algorithm that nacl provided that LibSodium now maintains. There just so happens to be one in Go’s extended library ecosystem.

Instead of LibSodium, you should use the nacl/box library that is part of golang.org/x/crypto. Here’s the code equivalent to the LibSodium code above, which is very slightly more complicated, but much better all around:

import (
    "crypto/rand"
    "encoding/base64"
    "golang.org/x/crypto/nacl/box"
)

func encryptSecret(pk, secret string) (string, error) {
    var pkBytes [32]byte
    copy(pkBytes[:], pk)
    secretBytes := secret

    out := make([]byte, 0,
        len(secretBytes)+
        box.Overhead+
        len(pkBytes))

    enc, err := box.SealAnonymous(
        out, secretBytes, &pkBytes, rand.Reader,
    )
    if err != nil {
        return "", err
    }

    encEnc := base64.StdEncoding.EncodeToString(enc)

    return encEnc, nil
}

Anyway, I share in the interest of maybe saving someone else the trouble, though I hardly believe my blog is popular enough to ever actually appear in a Google search, but here it is in any case.

Cheers.


  1. The chain of bad advice goes like this. The API reference for how to create or update a secret is documented. On that page, it tells you to use LibSodium to do the encryption. It specifically links to this page listing other bindings, which includes the golang binding that I don’t think you should use. ↩︎

The content of this site is licensed under Attribution 4.0 International (CC BY 4.0).