Add byte-compatible Auth & SSO core primitives
Pure, unit-tested primitives the AtomCMS->Next.js login must reproduce exactly
(verified now with round-trip + known vectors; full end-to-end check deferred
until a real DB + APP_KEY + live emulator are available):
- password.ts: argon2id (m=65536,t=4,p=1 via hash-wasm) + bcrypt ($2y$ accepted)
verify, and the md5->argon2id on-login upgrade gated by convert_passwords
(mirrors RedirectIfTwoFactorAuthenticatable).
- sso-ticket.ts: '{hotel_name without spaces}-{uuidv4}' written to auth_ticket +
ip_current (mirrors User::ssoTicket()).
- laravel-encrypter.ts: AES-256-CBC + HMAC-SHA256 payload compatible with
Laravel encrypt()/encryptString (for existing 2FA secrets) incl. PHP string
(de)serialization.
- totp.ts: otplib Google2FA-compatible TOTP verify (SHA1/6/30).
Libs: hash-wasm + bcryptjs + otplib (pure JS/WASM, no native build). 28 tests.
This commit is contained in:
1 parent
5fd40feaa6
commit
ec2d46e583
11 files changed
+511
No files matched your search
@@ -0,0 +1,55 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
LaravelEncrypter,
|
||||
phpSerializeString,
|
||||
phpUnserializeString,
|
||||
} from "./laravel-encrypter";
|
||||
|
||||
// A deterministic 32-byte key in Laravel's "base64:" form.
|
||||
const APP_KEY = `base64:${Buffer.from("0123456789abcdef0123456789abcdef").toString("base64")}`;
|
||||
|
||||
describe("LaravelEncrypter", () => {
|
||||
it("rejects a key that is not 32 bytes", () => {
|
||||
expect(() => new LaravelEncrypter("base64:c2hvcnQ=")).toThrow(/32 bytes/);
|
||||
});
|
||||
|
||||
it("round-trips encrypt/decrypt (serialize=true, like Laravel encrypt())", () => {
|
||||
const enc = new LaravelEncrypter(APP_KEY);
|
||||
const secret = "JBSWY3DPEHPK3PXP"; // a TOTP secret
|
||||
const payload = enc.encrypt(secret);
|
||||
expect(payload).not.toContain(secret);
|
||||
expect(enc.decrypt(payload)).toBe(secret);
|
||||
});
|
||||
|
||||
it("round-trips encryptString/decryptString (serialize=false)", () => {
|
||||
const enc = new LaravelEncrypter(APP_KEY);
|
||||
const payload = enc.encryptString("hello world");
|
||||
expect(enc.decryptString(payload)).toBe("hello world");
|
||||
});
|
||||
|
||||
it("fails closed when the MAC is tampered", () => {
|
||||
const enc = new LaravelEncrypter(APP_KEY);
|
||||
const payload = enc.encrypt("x");
|
||||
const json = JSON.parse(Buffer.from(payload, "base64").toString("utf8"));
|
||||
json.mac = "00".repeat(32);
|
||||
const tampered = Buffer.from(JSON.stringify(json), "utf8").toString("base64");
|
||||
expect(() => enc.decrypt(tampered)).toThrow(/MAC is invalid/);
|
||||
});
|
||||
|
||||
it("decrypts a payload produced with a fresh instance of the same key", () => {
|
||||
const payload = new LaravelEncrypter(APP_KEY).encrypt("shared");
|
||||
expect(new LaravelEncrypter(APP_KEY).decrypt(payload)).toBe("shared");
|
||||
});
|
||||
});
|
||||
|
||||
describe("php string (de)serialization", () => {
|
||||
it("serializes by byte length", () => {
|
||||
expect(phpSerializeString("hello")).toBe('s:5:"hello";');
|
||||
expect(phpSerializeString("café")).toBe('s:5:"café";'); // é is 2 bytes
|
||||
});
|
||||
|
||||
it("round-trips including multibyte", () => {
|
||||
expect(phpUnserializeString(phpSerializeString("café"))).toBe("café");
|
||||
expect(phpUnserializeString('s:5:"hello";')).toBe("hello");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user