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
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,12 +353,20 @@ const encodedData = hex.encode("Data to encode");

### Decoding

Decode hexadecimal-encoded data. Input can be a string or `ArrayBuffer`.
Decode hexadecimal-encoded data into a string. Input can be a string, `ArrayBuffer`, or `TypedArray`. Uppercase and lowercase hexadecimal are both accepted.

```ts
const decodedData = hex.decode(encodedData);
```

### Converting to bytes

Convert a hexadecimal string into its raw bytes. Use this when the decoded value is binary data, such as a signature or key, rather than UTF-8 text.

```ts
const bytes = hex.toBytes("48656c6c6f"); // Uint8Array([72, 101, 108, 108, 111])
```

## Binary

A utilities provide a simple interface to encode and decode data in binary format. It uses `TextEncode` and `TextDecoder` to encode and decode data respectively.
Expand Down
27 changes: 27 additions & 0 deletions src/hex.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,29 @@ describe("hex", () => {
});
});

describe("toBytes", () => {
it("should convert hexadecimal to bytes", () => {
expect(hex.toBytes("48656c6c6f")).toEqual(
Uint8Array.from([72, 101, 108, 108, 111]),
);
});

it("should accept uppercase hexadecimal", () => {
expect(hex.toBytes("DEADBEEF")).toEqual(
Uint8Array.from([222, 173, 190, 239]),
);
});

it("should convert an empty string to empty bytes", () => {
expect(hex.toBytes("")).toEqual(new Uint8Array());
});

it("should throw an error for invalid hexadecimal", () => {
expect(() => hex.toBytes("123")).toThrow(Error);
expect(() => hex.toBytes("zzzz")).toThrow(Error);
});
});

describe("decode", () => {
it("should decode a hexadecimal string to its original value", () => {
const expected = "Hello, World!";
Expand All @@ -30,6 +53,10 @@ describe("hex", () => {
expect(hex.decode(Buffer.from(expected).toString("hex"))).toBe(expected);
});

it("should decode uppercase hexadecimal", () => {
expect(hex.decode("48656C6C6F")).toBe("Hello");
});

it("should throw an error for an odd-length string", () => {
const input = "123";
expect(() => hex.decode(input)).toThrow(Error);
Expand Down
19 changes: 6 additions & 13 deletions src/hex.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { TypedArray } from "./type";
import { hexToBytes } from "@noble/hashes/utils.js";
import type { TypedArray, Uint8Array_ } from "./type";
import { toUint8Array } from "./bytes";

const hexadecimal = "0123456789abcdef";
const toBytes = (data: string): Uint8Array_ => hexToBytes(data) as Uint8Array_;

export const hex = {
encode: (data: string | ArrayBuffer | TypedArray) => {
const buffer = toUint8Array(data);
Expand All @@ -19,18 +21,9 @@ export const hex = {
return "";
}
if (typeof data === "string") {
if (data.length % 2 !== 0) {
throw new Error("Invalid hexadecimal string");
}
if (!new RegExp(`^[${hexadecimal}]+$`).test(data)) {
throw new Error("Invalid hexadecimal string");
}
const result = new Uint8Array(data.length / 2);
for (let i = 0; i < data.length; i += 2) {
result[i / 2] = parseInt(data.slice(i, i + 2), 16);
}
return new TextDecoder().decode(result);
return new TextDecoder().decode(toBytes(data));
}
return new TextDecoder().decode(data);
},
toBytes,
};
59 changes: 46 additions & 13 deletions src/hmac.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ describe("hmac module", () => {
const algorithm = "SHA-256";
const testKey = "super-secret-key";
const testData = "Hello, HMAC!";
let signature: ArrayBuffer;

it("imports a key for HMAC", async () => {
const cryptoKey = await createHMAC().importKey(testKey, "sign");
Expand All @@ -15,32 +14,66 @@ describe("hmac module", () => {
});

it("signs data using HMAC", async () => {
signature = await createHMAC().sign(testKey, testData);
const signature = await createHMAC().sign(testKey, testData);

expect(signature).toBeInstanceOf(ArrayBuffer);
expect(signature.byteLength).toBeGreaterThan(0);
});

it("verifies HMAC signature", async () => {
const isValid = await createHMAC().verify(testKey, testData, signature);
const hmac = createHMAC(algorithm);
const signature = await hmac.sign(testKey, testData);
const isValid = await hmac.verify(testKey, testData, signature);

expect(isValid).toBe(true);
});

it("fails verification for modified data", async () => {
const isValid = await createHMAC(algorithm).verify(
testKey,
"Modified data",
signature,
);
const hmac = createHMAC(algorithm);
const signature = await hmac.sign(testKey, testData);
const isValid = await hmac.verify(testKey, "Modified data", signature);

expect(isValid).toBe(false);
});

it("fails verification for a different key", async () => {
const hmac = createHMAC(algorithm);
const signature = await hmac.sign(testKey, testData);
const differentKey = "different-secret-key";
const isValid = await createHMAC(algorithm).verify(
differentKey,
testData,
signature,
);
const isValid = await hmac.verify(differentKey, testData, signature);

expect(isValid).toBe(false);
});

it.each([
["hex", "be961e914067801857b0429afaa685c9b31c35b8175bd2582d95a2887c5162ec"],
["base64", "vpYekUBngBhXsEKa+qaFybMcNbgXW9JYLZWiiHxRYuw="],
["base64url", "vpYekUBngBhXsEKa-qaFybMcNbgXW9JYLZWiiHxRYuw="],
["base64urlnopad", "vpYekUBngBhXsEKa-qaFybMcNbgXW9JYLZWiiHxRYuw"],
] as const)(
"signs and verifies valid and tampered data using %s encoding",
async (encoding, expected) => {
const hmac = createHMAC(algorithm, encoding);
const encodedSignature = await hmac.sign(testKey, testData);
const replacement = encodedSignature.startsWith("0") ? "1" : "0";
const tamperedSignature = `${replacement}${encodedSignature.slice(1)}`;

expect(encodedSignature).toBe(expected);
await expect(
hmac.verify(testKey, testData, encodedSignature),
).resolves.toBe(true);
await expect(
hmac.verify(testKey, testData, tamperedSignature),
).resolves.toBe(false);
},
);

it("verifies uppercase hexadecimal signatures", async () => {
const hmac = createHMAC(algorithm, "hex");
const encodedSignature = await hmac.sign(testKey, testData);

await expect(
hmac.verify(testKey, testData, encodedSignature.toUpperCase()),
).resolves.toBe(true);
});
});
14 changes: 8 additions & 6 deletions src/hmac.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,12 @@ export const createHMAC = <E extends EncodingFormat = "none">(
if (encoding === "hex") {
return hex.encode(signature) as E extends "none" ? ArrayBuffer : string;
}
if (
encoding === "base64" ||
encoding === "base64url" ||
encoding === "base64urlnopad"
) {
if (encoding === "base64") {
return base64.encode(signature) as E extends "none"
? ArrayBuffer
: string;
}
if (encoding === "base64url" || encoding === "base64urlnopad") {
return base64Url.encode(signature, {
padding: encoding !== "base64urlnopad",
}) as E extends "none" ? ArrayBuffer : string;
Expand All @@ -56,7 +57,8 @@ export const createHMAC = <E extends EncodingFormat = "none">(
hmacKey = await hmac.importKey(hmacKey, "verify");
}
if (encoding === "hex") {
signature = hex.decode(signature);
signature =
typeof signature === "string" ? hex.toBytes(signature) : signature;
}
if (
encoding === "base64" ||
Expand Down