Skip to content

Instantly share code, notes, and snippets.

@felipecsl
Created January 25, 2024 13:03
Show Gist options
  • Save felipecsl/a6959e54caf2e53238306e2167e90ba2 to your computer and use it in GitHub Desktop.
Save felipecsl/a6959e54caf2e53238306e2167e90ba2 to your computer and use it in GitHub Desktop.
Typescript/Node function to decrypt Rails 7.1 encrypted session cookies
import crypto from 'crypto';
import { promisify } from "node:util";
const pbkdf2 = promisify(crypto.pbkdf2);
// Works with Rails 7.1.3
// secretKeyBase is Rails.application.secret_key_base
async function decryptCookie(encryptedCookie: string, cookieName: string, secretKeyBase: string) {
const cookie = decodeURIComponent(encryptedCookie);
const [data, iv, authTag] = cookie.split("--").map(v => Buffer.from(v, 'base64'));
if (!authTag || authTag.length !== 16) {
throw new Error('InvalidMessage');
}
const salt = "authenticated encrypted cookie";
const iterations = 1000;
const keyLength = 32; // for AES-256
// Generate secret key using PBKDF2
const secret = await pbkdf2(secretKeyBase, salt, iterations, keyLength, 'sha256');
// Setup cipher for decryption
const decipher = crypto.createDecipheriv('aes-256-gcm', secret, iv);
decipher.setAuthTag(authTag);
decipher.setAAD(Buffer.from(''));
try {
// Perform decryption
let decrypted = decipher.update(data, undefined, 'utf8');
decrypted += decipher.final('utf8');
const cookiePayload = JSON.parse(decrypted);
const pur = cookiePayload["_rails"]['pur'];
if (pur !== `cookie.${cookieName}`) {
throw new Error('InvalidMessage');
}
const message = cookiePayload["_rails"]["message"];
return JSON.parse(Buffer.from(message, 'base64').toString());
} catch (err) {
throw new Error('DecryptionFailed');
}
}
function userIdFromDecryptedCookie(decryptedCookie: any) {
return decryptedCookie["warden.user.user.key"][0][0];
}
decryptCookie(encryptedCookie, cookieName, secretKeyBase)
.then(userIdFromDecryptedCookie)
.then(console.log);
@Eric-Guo
Copy link

Eric-Guo commented Oct 14, 2025

Recent Rails 7.1.5.2 cookie crypt changed as below. (8.0.3, 7.2.2.2 is right at 👆)

   const keyLength = 32; // for AES-256
+  const hashDigest = 'sha1'; // Rails 7.1 derives cookie keys with SHA1 PBKDF2
   // Generate secret key using PBKDF2
-  const secret = await pbkdf2(secretKeyBase, salt, iterations, keyLength, 'sha256');
+  const secret = await pbkdf2(secretKeyBase, salt, iterations, keyLength, hashDigest);
   // Setup cipher for decryption

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment