mirror of
https://github.com/HeyPuter/puter.git
synced 2026-10-11 22:32:26 +00:00
refactor(util): add secretsEqual for constant-time secret comparison
One HMAC-then-timingSafeEqual helper, safe for inputs of any length, so callers stop hand-rolling the length guard. File signature verification uses it.
This commit is contained in:
1 parent
6f0dbfd46c
commit
0d6b8859b2
3 files changed
+90
-12
No files matched your search
@@ -17,9 +17,10 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { createHash, createHmac, timingSafeEqual } from 'node:crypto';
|
||||
import { createHash, createHmac } from 'node:crypto';
|
||||
import { HttpError } from '../core/http/HttpError.js';
|
||||
import type { FSEntry } from '../stores/fs/FSEntry.js';
|
||||
import { secretsEqual } from './secureCompare.js';
|
||||
|
||||
/**
|
||||
* File URL signing. A signed URL carries `uid`, `expires` and `signature`; that
|
||||
@@ -91,20 +92,15 @@ function computeOwnerBoundSignature(
|
||||
}
|
||||
|
||||
/**
|
||||
* Constant-time equality for the hex signature strings. A plain `===`
|
||||
* short-circuits on the first differing character, leaking a byte-by-byte
|
||||
* timing oracle on the one value an attacker controls and submits repeatedly.
|
||||
* Length mismatch (or non-hex input) returns false without a timing-variable
|
||||
* compare.
|
||||
* Constant-time equality for the hex signature strings; compares the decoded
|
||||
* bytes, so hex case doesn't matter.
|
||||
*/
|
||||
function signaturesEqual(provided: string, expected: string): boolean {
|
||||
if (provided.length !== expected.length) return false;
|
||||
const a = Buffer.from(provided, 'hex');
|
||||
const b = Buffer.from(expected, 'hex');
|
||||
// Malformed hex yields a shorter buffer than the hex length implies;
|
||||
// timingSafeEqual requires equal lengths, so guard before comparing.
|
||||
if (a.length !== b.length) return false;
|
||||
return timingSafeEqual(a, b);
|
||||
return secretsEqual(
|
||||
Buffer.from(provided, 'hex'),
|
||||
Buffer.from(expected, 'hex'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (C) 2024-present Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { secretsEqual } from './secureCompare.ts';
|
||||
|
||||
describe('secretsEqual', () => {
|
||||
it('is true for equal strings and equal buffers', () => {
|
||||
expect(secretsEqual('s3cret', 's3cret')).toBe(true);
|
||||
expect(
|
||||
secretsEqual(Buffer.from([1, 2, 3]), Buffer.from([1, 2, 3])),
|
||||
).toBe(true);
|
||||
expect(secretsEqual('', '')).toBe(true);
|
||||
});
|
||||
|
||||
it('is false for values that differ anywhere', () => {
|
||||
expect(secretsEqual('s3cret', 's3creT')).toBe(false);
|
||||
expect(secretsEqual('s3cret', 'X3cret')).toBe(false);
|
||||
});
|
||||
|
||||
it('is false for different lengths without throwing', () => {
|
||||
expect(secretsEqual('short', 'much longer secret')).toBe(false);
|
||||
expect(secretsEqual('', 'x')).toBe(false);
|
||||
expect(secretsEqual(Buffer.alloc(16), Buffer.alloc(32))).toBe(false);
|
||||
});
|
||||
|
||||
it('compares a string with its utf8 bytes as equal', () => {
|
||||
expect(secretsEqual('héllo', Buffer.from('héllo', 'utf8'))).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) 2024-present Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
|
||||
|
||||
// Only has to stay unknown to callers for the life of the process.
|
||||
const COMPARE_KEY = randomBytes(32);
|
||||
|
||||
const digest = (value: string | Buffer): Buffer =>
|
||||
createHmac('sha256', COMPARE_KEY).update(value).digest();
|
||||
|
||||
/**
|
||||
* Constant-time equality for secrets, tokens and signatures. Both sides are
|
||||
* HMACed to a fixed-length digest first, so neither the inputs' lengths nor
|
||||
* where they first differ shows in the timing, and unequal lengths need no
|
||||
* early return.
|
||||
*/
|
||||
export function secretsEqual(a: string | Buffer, b: string | Buffer): boolean {
|
||||
return timingSafeEqual(digest(a), digest(b));
|
||||
}
|
||||
Reference in new issue
Block a user