DEV Community

shaojie gong
shaojie gong

Posted on

EmailKnow:I hold the keys to send email as my users. Here's how I made that as un-scary as I could.

To send a follow-up on your behalf, EmailKnow needs a Gmail refresh token — a long-lived key that lets my backend get permission to send as you, over and over, without you being present. Let that sink in: I'm holding something that can send email as you.

I did not want that token sitting in my database in plaintext. A database leak shouldn't equal "attacker can now email everyone's contacts as them." So a few decisions:

1/ Encrypt the refresh token at rest with AES-256-GCM. The database stores ciphertext, never the raw token. The encryption key lives in a secret, separate from the data. Leak the DB alone and you get noise.

2/ The client secret never leaves the server. The OAuth flow exchanges an authorization code for tokens, and that exchange needs Google's client secret. It's tempting to do more in the extension — but the extension is untrusted; anyone can unpack it. So the secret only ever exists on the backend. The extension never sees it.

3/ Access tokens (the short-lived ones) get cached in KV with their real expiry, so I'm not constantly re-minting them — but they're short-lived by design, so the blast radius is small.

4/ Revoke means revoke. When a user disconnects Gmail, I delete the stored refresh token AND clear the cached access token. No zombie access.

There's a broader principle I've started applying everywhere in this project: assume the database will leak someday, and design so that a leak is embarrassing, not catastrophic. Bodies never stored. IPs only as salted hashes. Tokens only as ciphertext. If the worst happens, the loot is worthless.

None of this is exotic crypto — it's AES-GCM and keeping a secret on the server. The hard part isn't the algorithm; it's deciding, before you ship, that "plaintext for now, encrypt later" is not acceptable when the thing you're storing can send email as another human.

For those who've built OAuth backends: do you encrypt refresh tokens at rest, or avoid storing them entirely? I went with encrypt — curious where others land.

— building EmailKnow in public, #4

Top comments (0)