<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: HelperX</title>
    <description>The latest articles on DEV Community by HelperX (@helperx).</description>
    <link>https://dev.to/helperx</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3966739%2F3e15f448-cb0c-4b76-84a8-c95e8a275a25.png</url>
      <title>DEV Community: HelperX</title>
      <link>https://dev.to/helperx</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/helperx"/>
    <language>en</language>
    <item>
      <title>How We Encrypt X Auth Tokens: AES-256-GCM in Practice</title>
      <dc:creator>HelperX</dc:creator>
      <pubDate>Wed, 03 Jun 2026 19:27:49 +0000</pubDate>
      <link>https://dev.to/helperx/how-we-encrypt-x-auth-tokens-aes-256-gcm-in-practice-4g86</link>
      <guid>https://dev.to/helperx/how-we-encrypt-x-auth-tokens-aes-256-gcm-in-practice-4g86</guid>
      <description>&lt;p&gt;When you build a tool that stores authentication tokens for other people's social media accounts, you have exactly one job before anything else: &lt;strong&gt;make sure a database leak doesn't compromise every account you manage.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is how we handle it at &lt;a href="https://helperx.app" rel="noopener noreferrer"&gt;HelperX&lt;/a&gt; — an X automation platform where every slot stores an auth token and proxy credentials.&lt;/p&gt;

&lt;h2&gt;
  
  
  The threat model
&lt;/h2&gt;

&lt;p&gt;Let's be honest about what application-level encryption protects against — and what it doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it covers:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database dump stolen via SQL injection or backup leak&lt;/li&gt;
&lt;li&gt;Casual disk access (stolen server, improper decommission)&lt;/li&gt;
&lt;li&gt;Insider access to the database without code access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What it doesn't cover:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An attacker with code execution on the server (they can read the key from environment)&lt;/li&gt;
&lt;li&gt;A compromised application process (it decrypts at runtime)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the standard threat model for SaaS applications. If someone owns your process, encryption at rest won't save you — but that's what defense in depth, access controls, and monitoring are for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Our goal:&lt;/strong&gt; even if the database leaks, tokens are unreadable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AES-256-GCM
&lt;/h2&gt;

&lt;p&gt;There are three realistic choices for symmetric encryption in Node.js:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AES-256-CBC&lt;/strong&gt; — works, but no built-in authentication. You need a separate HMAC to detect tampering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AES-256-GCM&lt;/strong&gt; — authenticated encryption. Encryption + integrity check in one operation. If anyone modifies the ciphertext, decryption fails. This is what we use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ChaCha20-Poly1305&lt;/strong&gt; — excellent algorithm, but less hardware acceleration on x86 servers compared to AES-GCM.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GCM wins because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One operation for encryption + authentication (no separate HMAC step)&lt;/li&gt;
&lt;li&gt;Hardware-accelerated AES-NI on virtually all modern servers&lt;/li&gt;
&lt;li&gt;Battle-tested in TLS 1.3, widely reviewed&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The encryption flow
&lt;/h2&gt;

&lt;p&gt;Here's the conceptual flow — not our exact code, but the pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;plaintext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;masterKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Every value gets its own random IV — never reuse&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cipher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createCipheriv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aes-256-gcm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;masterKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;encrypted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;plaintext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;encrypted&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;final&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// GCM produces an auth tag — store it alongside the ciphertext&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authTag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAuthTag&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Return IV + authTag + ciphertext as a single string&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;authTag&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;encrypted&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;decrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;masterKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ivHex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authTagHex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ciphertext&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ivHex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authTag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authTagHex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decipher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDecipheriv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aes-256-gcm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;masterKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;decipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAuthTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authTag&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;decrypted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;decipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ciphertext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;decrypted&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;decipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;final&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;decrypted&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key details:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Random IV per value&lt;/strong&gt; — the same token encrypted twice produces different ciphertext. This prevents an attacker from detecting duplicate tokens across slots.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth tag stored with ciphertext&lt;/strong&gt; — if anyone tampers with the stored value, &lt;code&gt;decipher.final()&lt;/code&gt; throws. No silent corruption.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single string format&lt;/strong&gt; — &lt;code&gt;iv:authTag:ciphertext&lt;/code&gt; keeps everything in one database column. No schema changes needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key management
&lt;/h2&gt;

&lt;p&gt;The master key lives in an environment variable. Not in the database, not in the codebase, not in a config file that gets committed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;X_TOKEN_ENC_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;64-char hex string&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key derivation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Derive a 32-byte key from the environment variable&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;masterKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;X_TOKEN_ENC_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We use SHA-256 to derive a fixed-length key from the environment variable. This means the env var can be any string — a passphrase, a hex string, a UUID — and we always get a valid 256-bit key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not use the env var directly?&lt;/strong&gt;&lt;br&gt;
Environment variables are strings. AES-256 needs exactly 32 bytes. Hashing normalizes any input to the right length.&lt;/p&gt;
&lt;h2&gt;
  
  
  What gets encrypted
&lt;/h2&gt;

&lt;p&gt;Not everything in the database is encrypted — only credentials:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;X auth tokens&lt;/strong&gt; — the primary target&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proxy credentials&lt;/strong&gt; — username/password for residential proxies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Settings, daily counters, audit logs, module configuration — these are stored in plaintext. They're not sensitive, and encrypting them would add latency to every operation for no security benefit.&lt;/p&gt;
&lt;h2&gt;
  
  
  Decryption at runtime
&lt;/h2&gt;

&lt;p&gt;Tokens are only decrypted when a module needs to make an API call on behalf of the user. The decrypted value lives in memory for the duration of the request, then gets garbage collected.&lt;/p&gt;

&lt;p&gt;We don't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cache decrypted tokens&lt;/li&gt;
&lt;li&gt;Write decrypted values to logs&lt;/li&gt;
&lt;li&gt;Pass decrypted tokens between processes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each module cycle: read encrypted token → decrypt → use → discard.&lt;/p&gt;
&lt;h2&gt;
  
  
  Legacy data migration
&lt;/h2&gt;

&lt;p&gt;When we added encryption, existing users already had plaintext tokens in the database. We handle this with a detection-and-upgrade pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;masterKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Encrypted values contain colons (iv:authTag:ciphertext)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;decrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;masterKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Legacy plaintext — encrypt it for next time&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;encrypted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;masterKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;saveEncryptedToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;encrypted&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// update in database&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On first access, plaintext tokens are automatically encrypted and saved back. No manual migration needed, no downtime, no batch job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance impact
&lt;/h2&gt;

&lt;p&gt;AES-256-GCM with AES-NI hardware acceleration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Encrypt:&lt;/strong&gt; ~0.02ms per token&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decrypt:&lt;/strong&gt; ~0.02ms per token&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For context, a single HTTP request to X's API takes 200–800ms. Encryption adds 0.02ms. It's unmeasurable in practice.&lt;/p&gt;

&lt;p&gt;We encrypt/decrypt ~50,000 tokens per day across all slots. Total CPU time for encryption: about 1 second per day. Not a bottleneck.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we'd do differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;If starting from scratch:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use envelope encryption&lt;/strong&gt; — encrypt each token with a unique data key, then encrypt the data key with the master key. This lets you rotate the master key without re-encrypting every token.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Consider a KMS&lt;/strong&gt; — AWS KMS or HashiCorp Vault for key management instead of a raw environment variable. Adds operational complexity but improves the key lifecycle.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Field-level encryption in the ORM&lt;/strong&gt; — encrypt/decrypt transparently at the model layer so developers never see plaintext tokens. We do this manually; a framework integration would be cleaner.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For our scale (thousands of slots, not millions), the current approach is sufficient. The improvements above are for teams that need to rotate keys frequently or operate under stricter compliance requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways for your project
&lt;/h2&gt;

&lt;p&gt;If you're storing third-party credentials in your SaaS:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Use AES-256-GCM&lt;/strong&gt;, not CBC — you get authentication for free&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Random IV per value&lt;/strong&gt; — never reuse IVs with the same key&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store IV + authTag + ciphertext together&lt;/strong&gt; — one column, no schema overhead&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key in environment, not in code&lt;/strong&gt; — the simplest separation that works&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encrypt only secrets&lt;/strong&gt; — don't waste cycles on non-sensitive data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handle legacy data gracefully&lt;/strong&gt; — detect-and-upgrade beats batch migration&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The code is straightforward. The hard part is making it automatic so developers on your team can't accidentally skip it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://helperx.app" rel="noopener noreferrer"&gt;HelperX&lt;/a&gt; encrypts every auth token and proxy credential with AES-256-GCM before database storage. If you manage X accounts, we handle the security so you can focus on growth.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>encryption</category>
      <category>node</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
