カレーはおやつに入りますか?

気の向くままに紹介記事やプログラミング技術について書いていきます

RailsのCookieStoreをnodeで復号する

背景

既に動いているRailsアプリケーションとfrontendの間にBFF用のnodeサーバを立てて、認証情報(currentUserIdの取得)はBFFで吸収させたい。

※ Session管理にCookie Storeを使っているRailsアプリケーションを想定しています。

書いた

RubyMarshal.loadRuby独自の機構なのでnode-marshalを利用した。

 

  import * as crypto from "crypto";
  import Marshal from "marshal";
   
  function decrypt(cookie: string): Object {
  const unescapedCookie = unescape(cookie);
   
  if (!process.env.RAILS_SECRET_KEY_BASE) {
  throw new Error("No Exists process.env.RAILS_SECRET_KEY_BASE");
  }
  const secretKeyBase = process.env.RAILS_SECRET_KEY_BASE;
  const encryptedCookieSalt = "encrypted cookie";
  const encryptedSignedCookieSalt = "signed encrypted cookie";
  const iterations = 1000;
  const keySize = 64;
  const secret = crypto.pbkdf2Sync(secretKeyBase, encryptedCookieSalt, iterations, keySize / 2, "SHA1");
  const signedSecret = crypto.pbkdf2Sync(secretKeyBase, encryptedSignedCookieSalt, iterations, keySize, "SHA1");
   
  const [data, digest] = unescapedCookie.split("--");
   
  // raise 'invalid message' unless digest == OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, sign_secret, data)
  // # you better use secure compare instead of `==` to prevent time based attact,
  // # ref: ActiveSupport::SecurityUtils.secure_compare
   
  const encryptedMessage = Buffer.from(data, "base64").toString();
  const [encryptedData, iv] = encryptedMessage.split("--").map(message => {
  return Buffer.from(message, "base64");
  });
   
  const decipher = crypto.createDecipheriv("aes-256-cbc", secret, iv);
  let dec = decipher.update(encryptedData);
  dec = Buffer.concat([dec, decipher.final()]);
   
  const marshal = new Marshal();
  const parsed = marshal.load(dec).parsed;
   
  return parsed;
  }
   
  export const setDecryptCookies = (cookieName: string) => {
  return (req, _, next) => {
  if (req.cookies && req.cookies[cookieName]) {
  // Base64 encode & 暗号化 された文字列をオブジェクトへ変換
  req.cookies[cookieName] = decrypt(req.cookies[cookieName]);
  next();
  } else {
  next();
  }
  };
  };

 

環境変数Railsアプリケーションで使っているSECRET_KEY_BASEを設定する必要がある。
expressのmiddlewareとして書いたので使うときは下のような感じになる。

import cookieParser from "cookie-parser";
import express from "express";

const COOKIE_NAME = "_sample_app_session_development"

// middleware
const app = express();
app.use(cookieParser()).use(setDecryptCookies(COOKIE_NAME));

参考にしました

encryption - How to decrypt a Rails 5 session cookie manually? - Stack Overflow
rails-cookie-parser/index.js at master · instore/rails-cookie-parser · GitHub