fix: decode legacy v6 sealed sessions on unseal#479
Conversation
Upgrading from the `encryptor` gem (v6) to the in-tree AES-GCM sealer (v7) changed the on-the-wire payload: v7 prepends a version byte and derives the key via SHA-256, while v6 used the raw passphrase as the key with no version prefix. Without a fallback, every existing browser cookie would fail to unseal after upgrade, silently logging users out. Unseal now tries v7 first and falls back to the v6 layout on failure, re-raising the original v7 error if both fail so error messages stay accurate for genuinely malformed payloads.
The v6 decoding fix in 126a713 had no regression coverage. This test exercises the full path — loading a v6-sealed cookie, refreshing against the API, and verifying the resealed session uses the current format — so future changes to the seal/unseal logic cannot silently break v6 compatibility.
Greptile SummaryThis PR adds a v6 legacy fallback path to
Confidence Score: 4/5Safe to merge; the fallback is well-isolated and existing error contracts are preserved. The core logic change is small and well-tested. The fallback correctly tries v6 decryption on any v7 failure and re-raises the original error when both attempts fail. The only open question is whether the v6 encryptor gem silently truncated passphrases longer than 32 bytes — if it did, decode_old would fail to decrypt those valid v6 cookies with no hint to the caller. lib/workos/encryptors/aes_gcm.rb — specifically the raw-passphrase key assignment in decode_old and its behaviour when the passphrase length differs from 32 bytes. Important Files Changed
|
| tag = encrypted.byteslice(encrypted.bytesize - 16, 16) | ||
|
|
||
| cipher = OpenSSL::Cipher.new("aes-256-gcm").decrypt | ||
| cipher.key = key.to_s |
There was a problem hiding this comment.
decode_old passes the raw passphrase string directly to OpenSSL without checking its length. AES-256-GCM requires exactly 32 bytes; if the cookie password is longer or shorter, OpenSSL raises OpenSSL::Cipher::CipherError, which unseal's rescue silently swaps for the original v7 error message. If the v6 encryptor gem silently truncated longer passphrases to 32 bytes, valid v6 cookies sealed with a longer password would fail to decrypt here with no useful diagnostic. Adding an explicit comment (or guard) clarifying that key.to_s must already be 32 bytes would make this constraint visible.
| def legacy_v6_seal(data, key) | ||
| cipher = OpenSSL::Cipher.new("aes-256-gcm").encrypt | ||
| iv = SecureRandom.random_bytes(12) | ||
| cipher.key = key | ||
| cipher.iv = iv | ||
| ciphertext = cipher.update(JSON.generate(data)) + cipher.final | ||
|
|
||
| Base64.encode64(iv + ciphertext + cipher.auth_tag) | ||
| end | ||
| end |
There was a problem hiding this comment.
The
legacy_v6_seal helper is defined identically in both test_encryptors_aes_gcm.rb and test_session.rb. Duplicating it means any future change to the v6 seal logic (e.g., to match a newly discovered variant) must be made in two places. Consider extracting it to a shared test helper module (e.g., test/support/legacy_seal_helpers.rb) and including it in both test classes.
| def legacy_v6_seal(data, key) | |
| cipher = OpenSSL::Cipher.new("aes-256-gcm").encrypt | |
| iv = SecureRandom.random_bytes(12) | |
| cipher.key = key | |
| cipher.iv = iv | |
| ciphertext = cipher.update(JSON.generate(data)) + cipher.final | |
| Base64.encode64(iv + ciphertext + cipher.auth_tag) | |
| end | |
| end | |
| # Consider moving this to a shared test helper module (e.g. test/support/legacy_seal_helpers.rb) | |
| # so it stays in sync with the identical copy in test_encryptors_aes_gcm.rb. | |
| def legacy_v6_seal(data, key) | |
| cipher = OpenSSL::Cipher.new("aes-256-gcm").encrypt | |
| iv = SecureRandom.random_bytes(12) | |
| cipher.key = key | |
| cipher.iv = iv | |
| ciphertext = cipher.update(JSON.generate(data)) + cipher.final | |
| Base64.encode64(iv + ciphertext + cipher.auth_tag) | |
| end | |
| end |
Summary
WorkOS::Encryptors::AesGcm#unsealso cookies sealed by the oldencryptor-gem format still decrypt after the v7 upgrade.test_encryptors_aes_gcm.rband an end-to-endSessionManager#authenticatetest exercising a v6-sealed cookie.Test plan
bundle exec rake test TEST=test/workos/test_encryptors_aes_gcm.rbbundle exec rake test TEST=test/workos/test_session.rb