Skip to content

Commit bcaad9a

Browse files
committed
test: improve coverage
1 parent a30f0cc commit bcaad9a

5 files changed

Lines changed: 156 additions & 63 deletions

File tree

__tests__/base64.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,49 @@ describe("decodeBase64", () => {
3030
]);
3131
});
3232

33+
it("should throw error for invalid length", () => {
34+
expect(() => decodeBase64("test", 0)).toThrow("Illegal length: 0");
35+
expect(() => decodeBase64("test", -1)).toThrow("Illegal length: -1");
36+
});
37+
3338
it("should handle invalid characters gracefully", () => {
3439
const result = decodeBase64("invalid@char", 5);
3540

3641
expect(result.length).toBeLessThanOrEqual(5);
3742
});
43+
44+
it("should handle early break on invalid c1 or c2", () => {
45+
// Test with string containing invalid characters at start
46+
const result = decodeBase64("@invalid", 5);
47+
48+
expect(result.length).toBe(0);
49+
});
50+
51+
it("should handle early break on invalid c3", () => {
52+
// Test with valid start but invalid c3
53+
const result = decodeBase64("..@", 5);
54+
55+
expect(result.length).toBe(1);
56+
});
57+
58+
it("should handle early break on invalid c4", () => {
59+
// Test with valid start but invalid c4 to trigger c4 === -1 case
60+
const result = decodeBase64("...@", 5);
61+
62+
expect(result.length).toBe(3);
63+
});
64+
65+
it("should handle length limit reached", () => {
66+
// Test case where length limit is reached
67+
const result = decodeBase64("..CA.uOD/eaGAOmJB.yMBv.", 3);
68+
69+
expect(result.length).toBe(3);
70+
});
71+
72+
it("should handle string length limit reached", () => {
73+
// Test case where off >= stringLength
74+
const result = decodeBase64("..", 5);
75+
76+
expect(result.length).toBe(1);
77+
});
3878
});

__tests__/compare.spec.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,16 @@ describe("compare", () => {
3737
);
3838
});
3939

40-
it("should resolve false for invalid hash length", async () => {
40+
it("should resolve false for invalid hash", async () => {
4141
await expect(compare("hello", "invalid")).resolves.toBe(false);
42-
await expect(compare("hello", "short")).resolves.toBe(false);
43-
await expect(compare("hello", "a".repeat(59))).resolves.toBe(false);
42+
});
43+
44+
it("should throw error with invalid salt", async () => {
45+
const invalidHash = "$2b$10$" + "@".repeat(22) + "x".repeat(31);
46+
47+
await expect(compare("hello", invalidHash)).rejects.toThrow(
48+
"Illegal salt: @@@@@@@@@@@@@@@@@@@@@@",
49+
);
4450
});
4551
});
4652

@@ -75,9 +81,15 @@ describe("compareSync", () => {
7581
);
7682
});
7783

78-
it("should return false for invalid hash length", () => {
84+
it("should resolve false for invalid hash", () => {
7985
expect(compareSync("hello", "invalid")).toBe(false);
80-
expect(compareSync("hello", "short")).toBe(false);
81-
expect(compareSync("hello", "a".repeat(59))).toBe(false);
86+
});
87+
88+
it("should throw error with invalid salt", () => {
89+
const invalidHash = "$2b$10$" + "@".repeat(22) + "x".repeat(31);
90+
91+
expect(() => compareSync("hello", invalidHash)).toThrow(
92+
"Illegal salt: @@@@@@@@@@@@@@@@@@@@@@",
93+
);
8294
});
8395
});

__tests__/hash.spec.ts

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,29 +22,56 @@ describe("hash", () => {
2222
await expect(hash(123, 10)).rejects.toThrow(
2323
"Invalid content / salt: not a string",
2424
);
25-
await expect(hash("hello", "invalid")).rejects.toThrow(
26-
"Invalid salt version: in",
25+
// @ts-expect-error: error type check
26+
await expect(hash(123, "invalid")).rejects.toThrow(
27+
"Invalid content / salt: not a string",
2728
);
2829
});
2930

30-
it("should reject for invalid salt format", async () => {
31+
it("should reject for invalid salt version", async () => {
3132
await expect(hash("hello", "invalid")).rejects.toThrow(
3233
"Invalid salt version: in",
3334
);
3435
await expect(hash("hello", "$1$")).rejects.toThrow(
3536
"Invalid salt version: $1",
3637
);
38+
});
39+
40+
it("should reject for invalid salt revision", async () => {
3741
await expect(hash("hello", "$2x$10$")).rejects.toThrow(
3842
"Invalid salt revision: x$",
3943
);
40-
await expect(hash("hello", "$2a$")).rejects.toThrow(
41-
"Illegal salt length: 0 != 16",
44+
await expect(hash("hello", "$2xx")).rejects.toThrow(
45+
"Invalid salt revision: xx",
4246
);
43-
await expect(hash("hello", "$2$10$validsalthere")).rejects.toThrow(
44-
"Illegal salt length: 9 != 16",
47+
await expect(hash("hello", "$2xxaaa")).rejects.toThrow(
48+
"Invalid salt revision: xx",
4549
);
50+
});
51+
52+
it("should reject for missing salt rounds", async () => {
53+
await expect(hash("hello", "$2a$")).rejects.toThrow("Missing salt rounds");
4654
await expect(hash("hello", "$2a$1Z$validsalthere")).rejects.toThrow(
47-
"Illegal salt length: 9 != 16",
55+
"Missing salt rounds",
56+
);
57+
58+
await expect(hash("hello", "$2a$ab$validSaltHere123")).rejects.toThrow(
59+
"Missing salt rounds",
60+
);
61+
});
62+
63+
it("should reject for invalid rounds", async () => {
64+
await expect(hash("hello", "$2a$03$" + "a".repeat(22))).rejects.toThrow(
65+
"Illegal number of rounds (4-31): 3",
66+
);
67+
await expect(hash("hello", "$2a$32$" + "a".repeat(22))).rejects.toThrow(
68+
"Illegal number of rounds (4-31): 32",
69+
);
70+
});
71+
72+
it("should reject for invalid salt length", async () => {
73+
await expect(hash("hello", "$2$10$validsalthere")).rejects.toThrow(
74+
"Illegal salt: validsalthere",
4875
);
4976
});
5077
});
@@ -69,27 +96,54 @@ describe("hashSync", () => {
6996
expect(() => hashSync(123, 10)).toThrow(
7097
"Invalid content / salt: not a string",
7198
);
72-
expect(() => hashSync("hello", "invalid")).toThrow(
73-
"Invalid salt version: in",
99+
// @ts-expect-error: error type check
100+
expect(() => hashSync(123, "invalid")).toThrow(
101+
"Invalid content / salt: not a string",
74102
);
75103
});
76104

77-
it("should throw error for invalid salt format", () => {
105+
it("should reject for invalid salt version", () => {
78106
expect(() => hashSync("hello", "invalid")).toThrow(
79107
"Invalid salt version: in",
80108
);
81109
expect(() => hashSync("hello", "$1$")).toThrow("Invalid salt version: $1");
82-
expect(() => hashSync("hello", "$2x$10$valid_salt_here")).toThrow(
110+
});
111+
112+
it("should reject for invalid salt revision", () => {
113+
expect(() => hashSync("hello", "$2x$10$")).toThrow(
83114
"Invalid salt revision: x$",
84115
);
85-
expect(() => hashSync("hello", "$2a$0")).toThrow(
86-
"Illegal salt length: 0 != 16",
116+
expect(() => hashSync("hello", "$2xx")).toThrow(
117+
"Invalid salt revision: xx",
87118
);
88-
expect(() => hashSync("hello", "$2$10$validsalthere")).toThrow(
89-
"Illegal salt length: 9 != 16",
119+
expect(() => hashSync("hello", "$2xxaaa")).toThrow(
120+
"Invalid salt revision: xx",
90121
);
122+
});
123+
124+
it("should reject for missing salt rounds", () => {
125+
expect(() => hashSync("hello", "$2a$")).toThrow("Missing salt rounds");
91126
expect(() => hashSync("hello", "$2a$1Z$validsalthere")).toThrow(
92-
"Illegal salt length: 9 != 16",
127+
"Missing salt rounds",
128+
);
129+
130+
expect(() => hashSync("hello", "$2a$ab$validSaltHere123")).toThrow(
131+
"Missing salt rounds",
132+
);
133+
});
134+
135+
it("should reject for invalid rounds", () => {
136+
expect(() => hashSync("hello", "$2a$03$" + "a".repeat(22))).toThrow(
137+
"Illegal number of rounds (4-31): 3",
138+
);
139+
expect(() => hashSync("hello", "$2a$32$" + "a".repeat(22))).toThrow(
140+
"Illegal number of rounds (4-31): 32",
141+
);
142+
});
143+
144+
it("should reject for invalid salt length", () => {
145+
expect(() => hashSync("hello", "$2$10$validsalthere")).toThrow(
146+
"Illegal salt: validsalthere",
93147
);
94148
});
95149
});

src/crypt.ts

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { nextTick } from "nextTick";
22

33
import {
4-
BCRYPT_SALT_LEN,
54
BLOWFISH_NUM_ROUNDS,
65
C_ORIG,
76
MAX_EXECUTION_TIME,
@@ -236,25 +235,6 @@ export const crypt = (
236235
const cdata = new Int32Array(C_ORIG);
237236
const cLength = cdata.length;
238237

239-
// Validate
240-
if (rounds < 4 || rounds > 31) {
241-
const err = new Error(`Illegal number of rounds (4-31): ${rounds}`);
242-
243-
if (!sync) return Promise.reject(err);
244-
245-
throw err;
246-
}
247-
248-
if (salt.length !== BCRYPT_SALT_LEN) {
249-
const err = new Error(
250-
`Illegal salt length: ${salt.length} != ${BCRYPT_SALT_LEN}`,
251-
);
252-
253-
if (!sync) return Promise.reject(err);
254-
255-
throw err;
256-
}
257-
258238
rounds = (1 << rounds) >>> 0;
259239

260240
const P = new Int32Array(P_ORIG);

src/hash.ts

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { convertToUFT8Bytes } from "./uft8.js";
1313
*
1414
* @private
1515
* @param content String to hash
16-
* @param salt Salt to use, actually never null
16+
* @param salt Salt to use
1717
* @param progressCallback Callback called with the current progress
1818
*/
1919
const _hash = (
@@ -60,43 +60,50 @@ const _hash = (
6060
offset = 4;
6161
}
6262

63+
const roundText = salt.substring(offset, offset + 2);
64+
const rounds = /\d\d/.test(roundText) ? Number(roundText) : null;
65+
6366
// Extract number of rounds
64-
if (salt.charAt(offset + 2) > "$") {
67+
if (rounds === null) {
6568
const err = new Error("Missing salt rounds");
6669

6770
if (!sync) return Promise.reject(err);
6871

6972
throw err;
7073
}
7174

72-
const r1 = parseInt(salt.substring(offset, offset + 1), 10) * 10,
73-
r2 = parseInt(salt.substring(offset + 1, offset + 2), 10),
74-
rounds = r1 + r2,
75-
realSalt = salt.substring(offset + 3, offset + 25);
75+
if (rounds < 4 || rounds > 31) {
76+
const err = new Error(`Illegal number of rounds (4-31): ${rounds}`);
77+
78+
if (!sync) return Promise.reject(err);
79+
80+
throw err;
81+
}
82+
83+
const realSalt = salt.substring(offset + 3, offset + 25);
7684

7785
content += minor >= "a" ? "\x00" : "";
7886

7987
const passwordBytes = convertToUFT8Bytes(content),
8088
saltBytes = decodeBase64(realSalt, BCRYPT_SALT_LEN);
8189

90+
if (saltBytes.length !== BCRYPT_SALT_LEN) {
91+
const err = new Error(`Illegal salt: ${realSalt}`);
92+
93+
if (!sync) return Promise.reject(err);
94+
95+
throw err;
96+
}
97+
8298
/**
8399
* Finishes hashing.
84100
* @param bytes Byte array
85101
*/
86-
const finish = (bytes: number[]): string => {
87-
const res = [];
88-
89-
res.push("$2");
90-
if (minor >= "a") res.push(minor);
91-
res.push("$");
92-
if (rounds < 10) res.push("0");
93-
res.push(rounds.toString());
94-
res.push("$");
95-
res.push(encodeBase64(saltBytes, saltBytes.length));
96-
res.push(encodeBase64(bytes, C_ORIG.length * 4 - 1));
97-
98-
return res.join("");
99-
};
102+
const finish = (bytes: number[]): string =>
103+
`$2${minor >= "a" ? minor : ""}$${rounds < 10 ? "0" : ""}${rounds}$${encodeBase64(
104+
saltBytes,
105+
BCRYPT_SALT_LEN,
106+
)}${encodeBase64(bytes, C_ORIG.length * 4 - 1)}`;
100107

101108
// Sync
102109
if (!sync)

0 commit comments

Comments
 (0)