Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add `areUint8ArraysEqual` function to compare two `Uint8Array` types ([#268](https://github.com/MetaMask/utils/pull/268))

## [11.7.0]

### Added
Expand Down
101 changes: 101 additions & 0 deletions src/bytes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
UTF_8_BYTES_FIXTURES,
} from './__fixtures__';
import {
areUint8ArraysEqual,
assertIsBytes,
base64ToBytes,
bigIntToBytes,
Expand Down Expand Up @@ -537,3 +538,103 @@ describe('createDataView', () => {
});
});
});

describe('areUint8ArraysEqual', () => {
it('returns true if the Uint8Arrays are equal', () => {
expect(
areUint8ArraysEqual(
new Uint8Array(32).fill(1),
new Uint8Array(32).fill(1),
),
).toBe(true);
});

it('returns false if the Uint8Arrays are not equal', () => {
expect(
areUint8ArraysEqual(
new Uint8Array(32).fill(1),
new Uint8Array(32).fill(2),
),
).toBe(false);
});

it('returns false if the Uint8Arrays length is different', () => {
expect(
areUint8ArraysEqual(
new Uint8Array(32).fill(1),
new Uint8Array(31).fill(1),
),
).toBe(false);
});

it('returns false if the first Uint8Array is shorter than the second', () => {
expect(
areUint8ArraysEqual(
new Uint8Array(31).fill(1),
new Uint8Array(32).fill(1),
),
).toBe(false);
});

it('returns true if the Uint8Arrays are both empty', () => {
expect(areUint8ArraysEqual(new Uint8Array(), new Uint8Array())).toBe(true);
});

it('returns false if there is a subtle difference in the Uint8Arrays', () => {
expect(
areUint8ArraysEqual(
new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
new Uint8Array([1, 2, 3, 4, 5, 6, 2, 8, 9, 10, 11, 12]),
),
).toBe(false);
});

it('has similar runtime for early vs late differences on large arrays', () => {
const LENGTH = 100_000;
const ITERATIONS = 200;

const base = new Uint8Array(LENGTH).fill(7);
const early = base.slice();
const late = base.slice();
early[0] = 6; // first element differs
late[LENGTH - 1] = 6; // last element differs

// Warm up JIT
for (let i = 0; i < 20; i++) {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
areUint8ArraysEqual(base, base);
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
areUint8ArraysEqual(early, base);
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
areUint8ArraysEqual(late, base);
}

const now = () => Number(process.hrtime.bigint());

let earlyTotal = 0;
let lateTotal = 0;

// Measure early difference
const startEarly = now();
for (let i = 0; i < ITERATIONS; i++) {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
areUint8ArraysEqual(early, base);
}
earlyTotal = now() - startEarly;

// Measure late difference
const startLate = now();
for (let i = 0; i < ITERATIONS; i++) {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
areUint8ArraysEqual(late, base);
}
lateTotal = now() - startLate;

// Ratio ≈ 1.0 ⇒ similar runtimes regardless of diff position.
// The threshold enforces the same order of magnitude while allowing normal system jitter.
// It's an empirical upper bound (~p95). To tune: run multiple trials, take a high percentile, and set slightly above it.
const ratio =
earlyTotal > lateTotal ? earlyTotal / lateTotal : lateTotal / earlyTotal;
expect(ratio).toBeLessThan(1.1);
});
});
29 changes: 29 additions & 0 deletions src/bytes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,3 +459,32 @@ export function createDataView(bytes: Uint8Array): DataView {

return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
}

/**
* Compare two Uint8Arrays using a constant-time style loop to reduce timing
* side-channels when comparing sensitive data (e.g., mnemonic bytes, keys,
* authentication tags). Does not early-return on the first difference:
* work done depends only on the input lengths, so byte content does not affect timing.
*
* When to use:
* - Use for secret or security-sensitive byte comparisons to avoid content-based timing leaks.
* - Prefer when inputs are fixed-length (or validated to equal length) at the API boundary.
*
* @param a - The first Uint8Array to compare.
* @param b - The second Uint8Array to compare.
* @returns Whether the Uint8Arrays are equal.
*/
export function areUint8ArraysEqual(a: Uint8Array, b: Uint8Array): boolean {
// eslint-disable-next-line no-bitwise
let diff = a.byteLength ^ b.byteLength;
const len = Math.max(a.byteLength, b.byteLength);

for (let i = 0; i < len; i++) {
const aByte = a[i] ?? 0;
const bByte = b[i] ?? 0;
// eslint-disable-next-line no-bitwise
diff |= aByte ^ bByte;
}

return diff === 0;
}
1 change: 1 addition & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ describe('index', () => {
"VersionRangeStruct",
"VersionStruct",
"add0x",
"areUint8ArraysEqual",
"assert",
"assertExhaustive",
"assertIsBytes",
Expand Down
1 change: 1 addition & 0 deletions src/node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ describe('node', () => {
"VersionRangeStruct",
"VersionStruct",
"add0x",
"areUint8ArraysEqual",
"assert",
"assertExhaustive",
"assertIsBytes",
Expand Down
Loading