initluyhgulednj

This commit is contained in:
ZOMBIIIIIII
2026-05-29 01:03:03 +03:00
parent 77a0f3d107
commit 860a22eb4a

View File

@@ -8,12 +8,33 @@ import { logger } from '../lib/logger';
* Token: <b64url_payload>.<b64url_timestamp>.<b64url_signature>
*
* Vault: read-only. `secret_key` обязателен; `salt`/`digest` опциональны (дефолты при отсутствии).
*
* itsdangerous 2.x по умолчанию: key_derivation=django-concat, digest=sha1.
* Старый Node-код использовал legacy-hmac-signer — из-за этого prod 403 при верном secret_key.
*/
const ITSDANGEROUS_EPOCH = 1293840000;
const DEFAULT_SALT = 'itsdangerous.Signer';
const LEGACY_VERIFY_SALTS = ['csrf-salt', 'csrf', 'csrf-token', 'itsdangerous.Signer'] as const;
const LEGACY_VERIFY_SALTS = [
'csrf-salt',
'csrf',
'csrf-token',
'wtf',
'wtf-csrf',
'itsdangerous.Signer',
] as const;
/** Порядок: сначала то, что реально ставит itsdangerous 2.x / Flask-WTF. */
const KEY_DERIVATIONS = [
'django-concat',
'legacy-hmac-signer',
'hmac',
'concat',
'none',
] as const;
export type CsrfKeyDerivation = (typeof KEY_DERIVATIONS)[number];
export interface CsrfConfig {
secret: string;
@@ -70,7 +91,7 @@ export function logCsrfConfigLoaded(): void {
`CSRF config loaded: mount=${summary.mount} path=${summary.path} ` +
`salt="${summary.salt}" digest=${summary.digest} maxAgeSec=${summary.maxAgeSec} ` +
`salt_source=${summary.saltSource} digest_source=${summary.digestSource} ` +
`secret_fp=${summary.secretFp}`,
`secret_fp=${summary.secretFp} verify_key_derivations=${KEY_DERIVATIONS.join(',')}`,
);
if (summary.saltSource === 'default') {
logger.warn(
@@ -125,10 +146,54 @@ function b64urlDecode(s: string): Buffer {
return Buffer.from(padded.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
}
function deriveKey(secret: string, salt: string, digest: string): Buffer {
/** itsdangerous 2.x default: hash(salt + b"signer" + secret_key). */
function deriveKeyDjangoConcat(secret: string, salt: string, digest: string): Buffer {
const data = Buffer.concat([
Buffer.from(salt, 'utf8'),
Buffer.from('signer', 'utf8'),
Buffer.from(secret, 'utf8'),
]);
return crypto.createHash(digest).update(data).digest();
}
/** itsdangerous key_derivation="hmac": HMAC(secret, salt). */
function deriveKeyHmac(secret: string, salt: string, digest: string): Buffer {
return crypto.createHmac(digest, secret).update(salt, 'utf8').digest();
}
/** itsdangerous key_derivation="concat": hash(salt + secret). */
function deriveKeyConcat(secret: string, salt: string, digest: string): Buffer {
const data = Buffer.concat([Buffer.from(salt, 'utf8'), Buffer.from(secret, 'utf8')]);
return crypto.createHash(digest).update(data).digest();
}
/** Старый wallet / часть 1.x: HMAC(secret, salt + "signer"). */
function deriveKeyLegacyHmacSigner(secret: string, salt: string, digest: string): Buffer {
return crypto.createHmac(digest, secret).update(salt + 'signer').digest();
}
export function deriveSigningKey(
secret: string,
salt: string,
digest: string,
mode: CsrfKeyDerivation,
): Buffer {
switch (mode) {
case 'django-concat':
return deriveKeyDjangoConcat(secret, salt, digest);
case 'hmac':
return deriveKeyHmac(secret, salt, digest);
case 'concat':
return deriveKeyConcat(secret, salt, digest);
case 'legacy-hmac-signer':
return deriveKeyLegacyHmacSigner(secret, salt, digest);
case 'none':
return Buffer.from(secret, 'utf8');
default:
return deriveKeyDjangoConcat(secret, salt, digest);
}
}
function decodeTimestamp(encoded: string): number {
const raw = b64urlDecode(encoded);
let ts = 0;
@@ -159,6 +224,7 @@ function verifyCsrfTokenWithParams(
cfg: CsrfConfig,
salt: string,
digest: CsrfDigest,
keyDerivation: CsrfKeyDerivation,
token: string,
): CsrfVerifyResult {
if (!token || typeof token !== 'string') return { valid: false, reason: 'Empty token' };
@@ -174,7 +240,7 @@ function verifyCsrfTokenWithParams(
const tsStr = payloadTs.slice(prevDot + 1);
const derived = deriveKey(cfg.secret, salt, digest);
const derived = deriveSigningKey(cfg.secret, salt, digest, keyDerivation);
const expectedSig = crypto.createHmac(digest, derived).update(payloadTs).digest();
let actualSig: Buffer;
@@ -232,53 +298,79 @@ function digestsToTry(primary: CsrfVerifyResult, vaultDigest: CsrfDigest): CsrfD
return order;
}
function derivationsToTry(primary: CsrfVerifyResult): CsrfKeyDerivation[] {
const order: CsrfKeyDerivation[] = [...KEY_DERIVATIONS];
if (primary.actualSigLen === 20) {
// Prod auth: itsdangerous 2.x + sha1 → django-concat первым.
return ['django-concat', ...order.filter((d) => d !== 'django-concat')];
}
return order;
}
function isRetryableVerifyFailure(reason?: string): boolean {
return reason === 'Signature length mismatch' || reason === 'Signature mismatch';
}
/**
* Verify: Vault salt+digest first, then legacy matrix (same secret_key from Vault, read-only).
* Verify: django-concat (itsdangerous 2.x) + legacy matrix (salt × digest × key_derivation).
*/
function inferSigLenFromToken(token: string): number | undefined {
const lastDot = token.lastIndexOf('.');
if (lastDot < 0) return undefined;
try {
return b64urlDecode(token.slice(lastDot + 1)).length;
} catch {
return undefined;
}
}
export function verifyCsrfToken(token: string): CsrfVerifyResult {
if (!current) return { valid: false, reason: 'CSRF secret not loaded' };
const vaultSalt = current.salt;
const vaultDigest = current.digest as CsrfDigest;
const salts = saltsToTry(vaultSalt);
const sigProbe: CsrfVerifyResult = { valid: false, actualSigLen: inferSigLenFromToken(token) };
const derivations = derivationsToTry(sigProbe);
const primary = verifyCsrfTokenWithParams(current, vaultSalt, vaultDigest, token);
if (primary.valid) return primary;
let lastMismatch: CsrfVerifyResult = { valid: false, reason: 'Signature mismatch' };
if (!isRetryableVerifyFailure(primary.reason)) {
return primary;
}
const digests = digestsToTry(primary, vaultDigest);
let lastMismatch: CsrfVerifyResult = primary;
for (const salt of salts) {
for (const digest of digests) {
if (salt === vaultSalt && digest === vaultDigest) continue;
const attempt = verifyCsrfTokenWithParams(current, salt, digest, token);
if (attempt.valid) {
logger.warn(
`CSRF verified with fallback digest=${digest} salt="${salt}" ` +
`(config digest=${vaultDigest} salt="${vaultSalt}"). ` +
'Align auth signing with Vault read config.',
for (const keyDerivation of derivations) {
for (const salt of salts) {
for (const digest of digestsToTry(sigProbe.actualSigLen !== undefined ? sigProbe : lastMismatch, vaultDigest)) {
const attempt = verifyCsrfTokenWithParams(
current,
salt,
digest,
keyDerivation,
token,
);
return { valid: true };
}
if (attempt.reason === 'Signature mismatch' || attempt.reason === 'Signature length mismatch') {
lastMismatch = attempt;
if (attempt.valid) {
const isPrimary =
keyDerivation === 'django-concat' &&
salt === vaultSalt &&
digest === vaultDigest;
if (!isPrimary) {
logger.warn(
`CSRF verified with fallback key_derivation=${keyDerivation} digest=${digest} salt="${salt}" ` +
`(config digest=${vaultDigest} salt="${vaultSalt}"). Align auth with Vault metadata when possible.`,
);
}
return { valid: true };
}
if (isRetryableVerifyFailure(attempt.reason)) {
lastMismatch = attempt;
} else if (attempt.reason && attempt.reason !== 'Signature mismatch') {
return attempt;
}
}
}
}
return {
valid: false,
reason: 'Signature mismatch (all digest/salt fallbacks failed)',
actualSigLen: primary.actualSigLen ?? lastMismatch.actualSigLen,
expectedSigLen: primary.expectedSigLen ?? lastMismatch.expectedSigLen,
reason: 'Signature mismatch (all digest/salt/key_derivation fallbacks failed)',
actualSigLen: lastMismatch.actualSigLen,
expectedSigLen: lastMismatch.expectedSigLen,
};
}