@xivdyetools/auth 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Tests for Timing-Safe Comparison Utilities
3
+ */
4
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
5
+ import { timingSafeEqual, timingSafeEqualBytes } from './timing.js';
6
+
7
+ describe('timing.ts', () => {
8
+ describe('timingSafeEqual', () => {
9
+ it('should return true for equal strings', async () => {
10
+ const result = await timingSafeEqual('hello', 'hello');
11
+ expect(result).toBe(true);
12
+ });
13
+
14
+ it('should return false for different strings', async () => {
15
+ const result = await timingSafeEqual('hello', 'world');
16
+ expect(result).toBe(false);
17
+ });
18
+
19
+ it('should return false for strings of different lengths', async () => {
20
+ const result = await timingSafeEqual('short', 'much longer string');
21
+ expect(result).toBe(false);
22
+ });
23
+
24
+ it('should return true for empty strings', async () => {
25
+ const result = await timingSafeEqual('', '');
26
+ expect(result).toBe(true);
27
+ });
28
+
29
+ it('should return false when one string is empty', async () => {
30
+ const result = await timingSafeEqual('hello', '');
31
+ expect(result).toBe(false);
32
+ });
33
+
34
+ it('should handle unicode strings', async () => {
35
+ const result = await timingSafeEqual('こんにちは', 'こんにちは');
36
+ expect(result).toBe(true);
37
+ });
38
+
39
+ it('should correctly compare similar strings', async () => {
40
+ // These strings differ only in the last character
41
+ const result = await timingSafeEqual('password1', 'password2');
42
+ expect(result).toBe(false);
43
+ });
44
+
45
+ it('should correctly compare strings that differ only in first character', async () => {
46
+ const result = await timingSafeEqual('apassword', 'bpassword');
47
+ expect(result).toBe(false);
48
+ });
49
+
50
+ describe('fallback implementation', () => {
51
+ let originalTimingSafeEqual: typeof crypto.subtle.timingSafeEqual;
52
+
53
+ beforeEach(() => {
54
+ // Save original and make it throw to test fallback
55
+ originalTimingSafeEqual = crypto.subtle.timingSafeEqual;
56
+ vi.stubGlobal('crypto', {
57
+ ...crypto,
58
+ subtle: {
59
+ ...crypto.subtle,
60
+ timingSafeEqual: () => {
61
+ throw new Error('Not available');
62
+ },
63
+ },
64
+ });
65
+ });
66
+
67
+ afterEach(() => {
68
+ vi.unstubAllGlobals();
69
+ });
70
+
71
+ it('should use fallback when crypto.subtle.timingSafeEqual throws', async () => {
72
+ const result = await timingSafeEqual('hello', 'hello');
73
+ expect(result).toBe(true);
74
+ });
75
+
76
+ it('should correctly compare different strings in fallback', async () => {
77
+ const result = await timingSafeEqual('hello', 'world');
78
+ expect(result).toBe(false);
79
+ });
80
+
81
+ it('should handle different lengths in fallback', async () => {
82
+ const result = await timingSafeEqual('short', 'longer');
83
+ expect(result).toBe(false);
84
+ });
85
+ });
86
+ });
87
+
88
+ describe('timingSafeEqualBytes', () => {
89
+ it('should return true for equal byte arrays', async () => {
90
+ const a = new Uint8Array([1, 2, 3, 4, 5]);
91
+ const b = new Uint8Array([1, 2, 3, 4, 5]);
92
+ const result = await timingSafeEqualBytes(a, b);
93
+ expect(result).toBe(true);
94
+ });
95
+
96
+ it('should return false for different byte arrays', async () => {
97
+ const a = new Uint8Array([1, 2, 3, 4, 5]);
98
+ const b = new Uint8Array([1, 2, 3, 4, 6]);
99
+ const result = await timingSafeEqualBytes(a, b);
100
+ expect(result).toBe(false);
101
+ });
102
+
103
+ it('should return false for arrays of different lengths', async () => {
104
+ const a = new Uint8Array([1, 2, 3]);
105
+ const b = new Uint8Array([1, 2, 3, 4, 5]);
106
+ const result = await timingSafeEqualBytes(a, b);
107
+ expect(result).toBe(false);
108
+ });
109
+
110
+ it('should return true for empty arrays', async () => {
111
+ const a = new Uint8Array([]);
112
+ const b = new Uint8Array([]);
113
+ const result = await timingSafeEqualBytes(a, b);
114
+ expect(result).toBe(true);
115
+ });
116
+ });
117
+ });
package/src/timing.ts ADDED
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Timing-Safe Comparison Utilities
3
+ *
4
+ * Provides constant-time comparison to prevent timing attacks.
5
+ * Regular string comparison (===) can leak information about secrets
6
+ * because it short-circuits on the first non-matching character.
7
+ *
8
+ * @module timing
9
+ */
10
+
11
+ /**
12
+ * Performs a constant-time string comparison to prevent timing attacks.
13
+ *
14
+ * Uses `crypto.subtle.timingSafeEqual()` when available (Cloudflare Workers),
15
+ * with a fallback XOR-based implementation for other environments.
16
+ *
17
+ * @param a - First string to compare
18
+ * @param b - Second string to compare
19
+ * @returns true if strings are equal, false otherwise
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * const isValid = await timingSafeEqual(providedToken, expectedToken);
24
+ * ```
25
+ */
26
+ export async function timingSafeEqual(a: string, b: string): Promise<boolean> {
27
+ const encoder = new TextEncoder();
28
+ const aBytes = encoder.encode(a);
29
+ const bBytes = encoder.encode(b);
30
+
31
+ // If lengths differ, we still need to do constant-time comparison
32
+ // to avoid leaking length information. Use the longer length.
33
+ const maxLength = Math.max(aBytes.length, bBytes.length);
34
+
35
+ // Pad shorter array to match length (prevents length-based timing leak)
36
+ const aPadded = new Uint8Array(maxLength);
37
+ const bPadded = new Uint8Array(maxLength);
38
+ aPadded.set(aBytes);
39
+ bPadded.set(bBytes);
40
+
41
+ // Use crypto.subtle.timingSafeEqual if available (Cloudflare Workers)
42
+ try {
43
+ const result = await crypto.subtle.timingSafeEqual(aPadded, bPadded);
44
+ // Also check original lengths matched
45
+ return result && aBytes.length === bBytes.length;
46
+ } catch {
47
+ // Fallback: manual constant-time comparison (for environments without timingSafeEqual)
48
+ let diff = aBytes.length ^ bBytes.length;
49
+ for (let i = 0; i < maxLength; i++) {
50
+ diff |= aPadded[i] ^ bPadded[i];
51
+ }
52
+ return diff === 0;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Performs constant-time comparison on Uint8Arrays.
58
+ *
59
+ * @param a - First array to compare
60
+ * @param b - Second array to compare
61
+ * @returns true if arrays are equal, false otherwise
62
+ */
63
+ export async function timingSafeEqualBytes(
64
+ a: Uint8Array,
65
+ b: Uint8Array
66
+ ): Promise<boolean> {
67
+ const maxLength = Math.max(a.length, b.length);
68
+
69
+ const aPadded = new Uint8Array(maxLength);
70
+ const bPadded = new Uint8Array(maxLength);
71
+ aPadded.set(a);
72
+ bPadded.set(b);
73
+
74
+ try {
75
+ const result = await crypto.subtle.timingSafeEqual(aPadded, bPadded);
76
+ return result && a.length === b.length;
77
+ } catch {
78
+ let diff = a.length ^ b.length;
79
+ for (let i = 0; i < maxLength; i++) {
80
+ diff |= aPadded[i] ^ bPadded[i];
81
+ }
82
+ return diff === 0;
83
+ }
84
+ }