Unverified Commit c6932dbf authored by Hong Minhee's avatar Hong Minhee
Browse files

Remove dependency on @std/crypto

parent e74c2df2
Loading
Loading
Loading
Loading
+0 −1
Original line number Diff line number Diff line
@@ -26,7 +26,6 @@
    "@opentelemetry/semantic-conventions": "npm:@opentelemetry/semantic-conventions@^1.27.0",
    "@phensley/language-tag": "npm:@phensley/language-tag@^1.9.0",
    "@std/assert": "jsr:@std/assert@^0.226.0",
    "@std/crypto": "jsr:@std/crypto@^1.0.4",
    "@std/encoding": "jsr:@std/encoding@1.0.7",
    "@std/http": "jsr:@std/http@^1.0.6",
    "@std/testing": "jsr:@std/testing@^0.224.0",
+119 −0
Original line number Diff line number Diff line
@@ -2,6 +2,7 @@ import {
  assert,
  assertEquals,
  assertExists,
  assertFalse,
  assertStringIncludes,
} from "@std/assert";
import { encodeBase64 } from "@std/encoding/base64";
@@ -25,6 +26,7 @@ import {
  parseRfc9421Signature,
  parseRfc9421SignatureInput,
  signRequest,
  timingSafeEqual,
  verifyRequest,
  type VerifyRequestOptions,
} from "./http.ts";
@@ -1689,3 +1691,120 @@ test("doubleKnock() async specDeterminer test", async () => {

  mf.uninstall();
});

test("timingSafeEqual()", async (t) => {
  await t.step("should return true for equal empty arrays", () => {
    const a = new Uint8Array([]);
    const b = new Uint8Array([]);
    assert(timingSafeEqual(a, b));
  });

  await t.step("should return true for equal non-empty arrays", async (t2) => {
    const testCases = [
      { a: [1, 2, 3], b: [1, 2, 3], name: "simple sequence" },
      { a: [0, 0, 0], b: [0, 0, 0], name: "sequence of zeros" },
      { a: [255, 128, 0, 42], b: [255, 128, 0, 42], name: "varied bytes" },
      {
        a: Array.from({ length: 100 }, (_, i) => i),
        b: Array.from({ length: 100 }, (_, i) => i),
        name: "longer sequence (0-99)",
      },
    ];

    for (const tc of testCases) {
      await t2.step(tc.name, () => {
        assert(timingSafeEqual(new Uint8Array(tc.a), new Uint8Array(tc.b)));
      });
    }
  });

  await t.step("should return true for reference equality", () => {
    const arr = new Uint8Array([10, 20, 30, 99, 100, 0]);
    assert(
      timingSafeEqual(arr, arr),
      "Array should be equal to itself by reference",
    );
  });

  await t.step(
    "should return false for arrays with same length but different content",
    async (t2) => {
      const testCases = [
        { a: [1, 2, 3], b: [0, 2, 3], name: "difference at start" },
        { a: [1, 2, 3], b: [1, 0, 3], name: "difference in middle" },
        { a: [1, 2, 3], b: [1, 2, 0], name: "difference at end" },
        { a: [0], b: [1], name: "single byte difference" },
        {
          a: [255, 0, 255],
          b: [255, 1, 255],
          name: "middle byte differs with edge values",
        },
      ];

      for (const tc of testCases) {
        await t2.step(tc.name, () => {
          assertFalse(
            timingSafeEqual(new Uint8Array(tc.a), new Uint8Array(tc.b)),
          );
        });
      }
    },
  );

  await t.step(
    "should return false for arrays with different lengths",
    async (t2) => {
      const testCases = [
        { a: [1, 2, 3], b: [1, 2], name: "b shorter" },
        { a: [1, 2], b: [1, 2, 3], name: "a shorter" },
        { a: [], b: [1, 2, 3], name: "a empty, b non-empty" },
        { a: [1, 2, 3], b: [], name: "a non-empty, b empty" },
      ];

      for (const tc of testCases) {
        await t2.step(tc.name, () => {
          assertFalse(
            timingSafeEqual(new Uint8Array(tc.a), new Uint8Array(tc.b)),
          );
        });
      }
    },
  );

  await t.step(
    "should return false where content matches up to shorter length",
    async (t2) => {
      const testCases = [
        { a: [1, 2], b: [1, 2, 0], name: "a is prefix, b has trailing zero" },
        { a: [1, 2, 0], b: [1, 2], name: "b is prefix, a has trailing zero" },
        { a: [0], b: [0, 0], name: "single zero vs two zeros" },
        { a: [0, 0], b: [0], name: "two zeros vs single zero" },
      ];

      for (const tc of testCases) {
        await t2.step(tc.name, () => {
          assertFalse(
            timingSafeEqual(new Uint8Array(tc.a), new Uint8Array(tc.b)),
          );
        });
      }
    },
  );

  await t.step(
    "should correctly handle comparisons involving padding bytes",
    async (t2) => {
      await t2.step("a=[1], b=[1,0] (b longer with trailing zero)", () => {
        const a1 = new Uint8Array([1]);
        const b1 = new Uint8Array([1, 0]);
        assertFalse(timingSafeEqual(a1, b1));
      });

      await t2.step("a=[1,0], b=[1] (a longer with trailing zero)", () => {
        const a2 = new Uint8Array([1, 0]);
        const b2 = new Uint8Array([1]);
        assertFalse(timingSafeEqual(a2, b2));
      });
    },
  );
});
+39 −1
Original line number Diff line number Diff line
@@ -10,7 +10,6 @@ import {
  ATTR_HTTP_REQUEST_METHOD,
  ATTR_URL_FULL,
} from "@opentelemetry/semantic-conventions";
import { timingSafeEqual } from "@std/crypto/timing-safe-equal";
import { decodeBase64, encodeBase64 } from "@std/encoding/base64";
import { encodeHex } from "@std/encoding/hex";
import {
@@ -1309,4 +1308,43 @@ export async function doubleKnock(
  return response;
}

/**
 * Performs a timing-safe equality comparison between two `Uint8Array` values.
 *
 * This function is designed to take a constant amount of time to execute,
 * dependent only on the length of the longer of the two arrays,
 * regardless of where the first difference in bytes occurs. This helps
 * prevent timing attacks.
 *
 * @param a The first bytes.
 * @param b The second bytes.
 * @returns `true` if the arrays are of the same length and contain the same
 *          bytes, `false` otherwise.
 * @since 1.6.0
 */
export function timingSafeEqual(a: Uint8Array, b: Uint8Array): boolean {
  const lenA = a.length;
  const lenB = b.length;
  const commonLength = Math.max(lenA, lenB);
  let result = 0;

  // Perform byte-wise XOR comparison for the length of the longer array.
  // If one array is shorter, its out-of-bounds "bytes" are treated as 0 for the comparison.
  // All byte differences are accumulated into the `result` using bitwise OR.
  for (let i = 0; i < commonLength; i++) {
    const byteA = i < lenA ? a[i] : 0;
    const byteB = i < lenB ? b[i] : 0;
    result |= byteA ^ byteB;
  }

  // Incorporate the length difference into the result.
  // If lengths are different, (lenA ^ lenB) will be non-zero, making `result` non-zero.
  // This ensures that arrays are only considered equal if both their contents
  // (up to their respective lengths) and their lengths are identical.
  result |= lenA ^ lenB;

  // `result` will be 0 if and only if all XORed byte pairs were 0 AND lengths were equal.
  return result === 0;
}

// cSpell: ignore keyid