How we share secrets at a fully-remote startup

Appeared originally at https://www.getgrist.com/blog/how-we-share-secrets-at-a-fully-remote-startup/ on 2024-07-16.

Here’s the situation: a small, fully-remote software team works on a service (let’s say Grist), and to run it, they need a secret key (let’s say an API key for OpenAI(1)).

I, the co-CEO, signed up for the OpenAI account and generated an API key. Now I want to share it with my CTO, who is in charge of getting the feature out to production.

We work remotely. We talk on Slack, or Kumospace, or Zoom, or even email. Do I want the secret in any of these channels? Nope.

I like to think of myself as security conscious: careful about privacy, passwords, encryption. (Please don’t take that as an invitation to target me for hacking.) And on a few occasions — enough for this to feel like it’s becoming a theme — I’ve rolled my own security. Sort of.

Don’t roll your own security

Like many hard areas of human knowledge, security/cryptography is one where the more you know, the more you realize you don’t know.

I am not a security expert. But once upon a time, I did take a graduate level Randomized Algorithms class, which was mostly on cryptographic algorithms, from Michael Rabin — one of the pioneers of public-key cryptography. Another class was a graduate level seminar on Elliptic Curves (the math behind Elliptic-curve cryptography). Not that I remember much of either – I’m brandishing these credentials to give you at least a bit of confidence that I do in fact realize how much I don’t know.

Both security and cryptography are easier to mess up than to get right. If you mess up any part, everything breaks. And broken security is worse than useless, it’s dangerous. You’ll be less safe relying on it than if you didn’t rely on it at all. Don’t roll your own!

Encrypting secrets

And with that in mind, let’s move towards breaking this advice.

Are there clever tools built to share secrets in a completely encrypted safe way? Yes, there are.(2) But we are not in Estonia, and to rely on a tool, I’d have to trust it. Is that really different from just trusting Slack?

So I scratched my head, and decided that there are some tools I do trust already. I trust my computer, and my CTO’s computer, mostly because I have to. The task would be impossible if I didn’t. And I trust low-level tools already installed on our computers. One of them is Node.js. Node.js is JavaScript for the server, and runs happily from the command line in a terminal.

So I asked myself: if I just limit my trust to Node.js, is that enough to share a secret?

Yes! But I need a bit of software — some JavaScript code — that would run in Node.js to encrypt/decrypt my secret. I could look for something to download, or I could write my own. Either way, if I don’t want to trust it, I want to be able to read and understand it. Luckily, what Node.js has built-in (and what I’ve already decided I will trust) is enough that the extra code is small.

I wrote a small JS script run using Node.js from a terminal. Initially it was 46 lines, though now it’s 72 with support for larger secrets. It’s also on GitHub. You can read it and understand it.

Full code of the script
Pictured: all the code

Let’s walk through it

  1. The main workhorse is the Node crypto module. So no, I didn’t roll my own cryptography.
  2. Then we have the main() function. When run with no arguments in a terminal, it helpfully prints out all available commands:
    $ ./secrets.js
    Usage:
          ./secrets.js encrypt RECIPIENT_KEYFILE.pub < PLAINTEXT_FILE
          ./secrets.js decrypt MY_KEYFILE.priv < ENCRYPTED_FILE
          ./secrets.js genkey MY_KEYFILE
  3. Continuing with main(), we check if the command is encrypt. If so, read the public key and the plaintext file.
  4. Encryption is almost fully provided by the crypto module:
    crypto.publicEncrypt({key, padding}, plaintext).toString('base64')
    We just have to pick the parameters (padding), and encode the result into text that’s easy to copy/paste.
  5. But life isn’t so simple. This only works for very small inputs (OK for a short API key, not enough for an SSL certificate). If the input is too large, we do the typical thing public-key cryptography does:
    1. Generate a small key for symmetric encryption on the fly.
    2. Encrypt the long input using that key.
    3. Put together the symmetric key (encrypted using the recipient’s public key) and the input (encrypted with the symmetric key). You can see how having the recipient’s private key would unlock the symmetric key, which would in turn unlock the data.
  6. Now we’re getting dangerously close to rolling our own, even though this logic is just 7 lines of code. There are more parameters (size of symmetric key, symmetric encryption algorithm, initialization vector), and there’s the question of whether our approach is right. There are multiple options that would work fine, but it’s possible to get it wrong, which would be worse than skipping the project altogether. I won’t lie: the parameters are not obvious, or even well-documented (either in Node’s crypto, or in OpenSSL that it uses under the hood). I did research on this, however, and recommend you do your own if you have doubts.
  7. Continuing with main(), it then handles the decrypt command. It does all the steps of encrypt in reverse.
  8. The last bit is the genkey command for creating the key-pair: crypto once again provides the generateKeyPair function for that. We just pick suitable parameters to call it with. We save the files containing the public and private keys, and reduce permissions on the private key to make it readable only by you.

That’s it! Public-key cryptography for this simple use case:

  1. The recipient generates a key-pair (genkey), holds on to the private key, and shares freely the public key. The keys are just files on your computer.
  2. The sender uses the recipient’s public key (RECIPIENT_KEYFILE.pub above) to encrypt a secret (“plaintext” in crypto-speak). The encrypted block of text (ciphertext) is safe to share over an untrusted channel.
  3. The recipient uses their private key (MY_KEYFILE.priv above) to decrypt it. No one else has the private key, and no one can decrypt the ciphertext without it.
Sisiphus being crushed by his stone
Sisyphus “rolling his own”. (Image source: Wikimedia Commons)

Did we roll our own security?

No, not really. Our solution uses and trusts a lot of cryptography code from good sources: Node.js, its crypto module, and the OpenSSL library it’s based on. These are respected, well-maintained, commonly-used open-source tools, which get plenty of attention from security researchers.

The code we did write, which makes a number of choices about how this cryptography is used, is still sensitive security code. That’s risky. The main protection here is that it’s so small that any software engineer can read and understand it. To me what matters is that I can read and understand it.

I find this an interesting compromise. It feels as secure as I can imagine any software to be, and it allows Grist Labs to share technical secrets remotely. For non-technical secrets (like next month’s book club selection), Slack works fine. For now…


(1) We use OpenAI for Grist’s AI Formula Assistant, but without sending data from your document, only its structure.

(2) There are plenty of other opinions on this problem. But there’s also this (paraphrased) conversation I had with a colleague after showing them a draft of this post, which brings us back full circle:

“If we’re bothering with encryption and keypairs, why not just PGP?”

“That’s still software I have to trust and install. In fact, I just tried, and got lost immediately. I found OpenPGP which has a half-dozen email-related packages for MacOS (didn’t try: proprietary/paid, and all about specific email clients that I don’t need), and found GPG which has two installers for Mac. I tried one, and it needs root access to install itself. Why would I give some software root access just to encrypt a secret? It certainly doesn’t increase my trust in it.”

“I guess what this answer points out, once again, is that GPG has a usability problem… So bad that it’s easier to create a short script in Node.js to do one of its core features.”