既存のRails 6のサービスをストラングラーフィグパターンで徐々にNode.jsに移行していこうかなと思ったけど結果的にやらない方向にしたのでお焚き上げしておく。
そもそもRubyでやるにはどうするか。下記を参考に。
- Decrypt Rails 6.0 beta session cookies · GitHub
- Demystifying cookie security in Rails 6 - DEV Community 👩💻👨💻
require "base64" require "openssl" require "json" require "uri" def decode(secret_key_base, cookie) data, iv, auth_tag = URI.decode_uri_component(cookie).split("--").map(&Base64.method(:strict_decode64)) cipher = OpenSSL::Cipher.new("aes-256-gcm") secret = OpenSSL::PKCS5.pbkdf2_hmac_sha1(secret_key_base, "authenticated encrypted cookie", 1000, cipher.key_len) cipher.decrypt cipher.key = secret cipher.iv = iv cipher.auth_tag = auth_tag cipher.auth_data = "" value = cipher.update(data) + cipher.final payload = JSON.parse(value) message = Base64.strict_decode64(payload["_rails"]["message"]) JSON.parse(message) end puts decode("a444a9...", "4HmNs...")
これをNode.jsに移植すればよい。
const crypto = require("crypto"); function decode(secretKeyBase, cookie) { const [data, iv, authTag] = decodeURIComponent(cookie).split("--").map(v => Buffer.from(v, "base64")); const secret = crypto.pbkdf2Sync(secretKeyBase, "authenticated encrypted cookie", 1000, 32, "sha1"); const cipher = crypto.createDecipheriv("aes-256-gcm", secret, iv); cipher.setAuthTag(authTag); const value = cipher.update(data, "base64", "utf8") + cipher.final("utf8"); const payload = JSON.parse(value); const message = Buffer.from(payload._rails.message, "base64"); return JSON.parse(message); } console.log(decode("a444a9...", "4HmNs..."));
ついでに逆もやってみる。上記の逆をやればいいのでRubyだとこうなる。
require "base64" require "openssl" require "json" require "uri" def encode(secret_key_base, session) message = Base64.strict_encode64(session.to_json) payload = { _rails: { message:, exp: nil, pur: "cookie._hoge_session" } } value = payload.to_json cipher = OpenSSL::Cipher.new("aes-256-gcm") secret = OpenSSL::PKCS5.pbkdf2_hmac_sha1(secret_key_base, "authenticated encrypted cookie", 1000, cipher.key_len) iv = cipher.random_iv cipher.encrypt cipher.key = secret cipher.iv = iv cipher.auth_data = "" data = cipher.update(value) + cipher.final auth_tag = cipher.auth_tag cookie = [data, iv, auth_tag].map(&Base64.method(:strict_encode64)).join("--") URI.encode_uri_component(cookie) end puts encode("a444a9...", session_id: "621be...", ...)
同様にNode.jsはこうなる。
const crypto = require("crypto"); function encode(secretKeyBase, session) { const message = Buffer.from(JSON.stringify(session)).toString("base64"); const payload = { _rails: { message, exp: null, pur: "cookie._hoge_session" } }; const value = JSON.stringify(payload); const secret = crypto.pbkdf2Sync(secretKeyBase, "authenticated encrypted cookie", 1000, 32, "sha1"); const iv = crypto.randomFillSync(Buffer.alloc(12)); const cipher = crypto.createCipheriv("aes-256-gcm", secret, iv); const data = cipher.update(value, "utf8", "base64") + cipher.final("base64"); const authTag = cipher.getAuthTag(); const cookie = [data, iv.toString("base64"), authTag.toString("base64")].join("--"); return encodeURIComponent(cookie); } console.log(encode("a444a9...", { session_id: "621be...", ... }));
Rails 7だとデフォルトの暗号化方式が変わるので、それに合わせた変更をするか、当座は引き続きこれまでのを使うようにするか設定する。実際どうするかは先のリンクのどこかに書いてたような気がする。ちなみに上記ではRuby 3.2の記法などを使ってるので3.1以下の場合は各自よろしくやってくれ。
検証としてRemixのミドルウェアっぽい使い方で期待通りに動いてるように見えた。今回は簡単のためNode.jsのcryptoモジュールは同期APIを利用したが、運用コードは非同期APIの方がいいだろう。