k9guard 1.0.0 → 1.0.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "k9guard",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "A secure, lightweight, and flexible CAPTCHA module for TypeScript/JavaScript projects with cryptographic security and multi-language support",
5
5
  "main": "index.ts",
6
6
  "type": "module",
@@ -1,4 +1,4 @@
1
- import { createHash, randomBytes } from 'crypto';
1
+ import { createHash, randomBytes } from '../utils/crypto';
2
2
  import { Random } from '../utils/random';
3
3
  import { RiddleBank } from '../utils/riddleBank';
4
4
  import { SequenceGenerator } from '../utils/sequenceGenerator';
@@ -200,7 +200,7 @@ export class CaptchaGenerator {
200
200
  // generates a mixed captcha by randomly selecting a type and generating accordingly
201
201
  private generateMixed(): MixedCaptcha {
202
202
  const types: ('math' | 'text' | 'riddle' | 'sequence' | 'scramble' | 'logic' | 'reverse')[] = ['math', 'text', 'riddle', 'sequence', 'scramble', 'logic', 'reverse'];
203
- const buffer = randomBytes(1);
203
+ const buffer = randomBytes(1) as any;
204
204
  const randomType = types[buffer[0]! % types.length]!;
205
205
 
206
206
  const previousType = this.standardOptions?.type;
@@ -1,4 +1,4 @@
1
- import { createHash } from 'crypto';
1
+ import { createHash } from '../utils/crypto';
2
2
  import type { CaptchaChallenge } from '../types';
3
3
 
4
4
  export class CaptchaValidator {
@@ -0,0 +1,360 @@
1
+ /**
2
+ * This module provides Node.js crypto API compatibility for web browsers using the Web Crypto API.
3
+ * It implements secure random number generation and hashing functions that work in both browser
4
+ * and server environments, eliminating the need for Node.js crypto module in web applications.
5
+ *
6
+ * Key features:
7
+ * - Web Crypto API integration with fallback detection
8
+ * - Secure random bytes generation
9
+ * - SHA-256 hashing with incremental updates
10
+ * - Buffer-like interface for compatibility
11
+ * - TypeScript support with proper type safety
12
+ *
13
+ * Security Notes:
14
+ * - Uses cryptographically secure random number generation
15
+ * - Implements deterministic hashing for consistent results
16
+ * - No sensitive data is stored or logged
17
+ * - All operations are performed in-memory only
18
+ */
19
+
20
+ declare const window: any;
21
+ declare const self: any;
22
+
23
+ /**
24
+ * Interface for crypto buffer operations
25
+ *
26
+ * Provides Node.js Buffer-like functionality for cryptographic operations.
27
+ * Supports array-like access, encoding conversions, and binary data manipulation.
28
+ */
29
+ export interface ICryptoBuffer {
30
+ readUInt32LE(offset: number): number;
31
+ toString(encoding?: string): string;
32
+ length: number;
33
+ [index: number]: number | undefined;
34
+ [Symbol.iterator](): Iterator<number>;
35
+ }
36
+
37
+ /**
38
+ * Core cryptographic utilities class
39
+ *
40
+ * Provides access to Web Crypto API with automatic environment detection.
41
+ * Handles browser compatibility and secure random number generation.
42
+ */
43
+ export class CryptoUtils {
44
+ /**
45
+ * Detect and return available Web Crypto API instance
46
+ *
47
+ * Checks multiple global objects to find crypto API in different environments:
48
+ * - globalThis (modern browsers/Node.js)
49
+ * - window (browser main thread)
50
+ * - self (Web Workers/Service Workers)
51
+ *
52
+ * @throws Error if Web Crypto API is not available
53
+ */
54
+ private static getWebCrypto(): Crypto {
55
+ if (typeof globalThis !== 'undefined' && globalThis.crypto) {
56
+ return globalThis.crypto;
57
+ }
58
+ if (typeof window !== 'undefined' && window.crypto) {
59
+ return window.crypto;
60
+ }
61
+ if (typeof self !== 'undefined' && self.crypto) {
62
+ return self.crypto;
63
+ }
64
+ throw new Error('Web Crypto API not available');
65
+ }
66
+
67
+ /**
68
+ * Generate cryptographically secure random bytes
69
+ *
70
+ * Uses Web Crypto API's getRandomValues() for secure random number generation.
71
+ * This is suitable for cryptographic purposes and provides better entropy than Math.random().
72
+ *
73
+ * @param size - Number of random bytes to generate (1-65536)
74
+ * @returns CryptoBuffer containing random bytes
75
+ * @throws RangeError if size is invalid
76
+ */
77
+ static randomBytes(size: number): ICryptoBuffer {
78
+ if (size <= 0 || size > 65536) {
79
+ throw new RangeError('Size must be between 1 and 65536');
80
+ }
81
+
82
+ const crypto = this.getWebCrypto();
83
+ const buffer = new Uint8Array(size);
84
+ crypto.getRandomValues(buffer);
85
+
86
+ return new CryptoBuffer(buffer);
87
+ }
88
+
89
+ /**
90
+ * Create a new hash instance
91
+ *
92
+ * Currently supports only SHA-256 for security and compatibility reasons.
93
+ * SHA-256 provides strong cryptographic hashing suitable for most applications.
94
+ *
95
+ * @param algorithm - Hash algorithm (currently only 'sha256' supported)
96
+ * @returns New CryptoHash instance
97
+ * @throws Error if unsupported algorithm is requested
98
+ */
99
+ static createHash(algorithm: string): CryptoHash {
100
+ if (algorithm.toLowerCase() !== 'sha256') {
101
+ throw new Error('Only SHA-256 is supported');
102
+ }
103
+
104
+ return new CryptoHash();
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Buffer implementation for cryptographic data
110
+ *
111
+ * Provides Node.js Buffer-compatible interface using Uint8Array internally.
112
+ * Uses Proxy pattern to enable array-like access (buffer[0], buffer[1], etc.)
113
+ * while maintaining type safety and compatibility.
114
+ */
115
+ export class CryptoBuffer implements ICryptoBuffer {
116
+ private buffer: Uint8Array;
117
+ [index: number]: number | undefined;
118
+
119
+ /**
120
+ * Create a new crypto buffer
121
+ *
122
+ * @param buffer - Underlying Uint8Array data
123
+ */
124
+ constructor(buffer: Uint8Array) {
125
+ this.buffer = buffer;
126
+ // NOTE: Using Proxy to enable array-like access while maintaining encapsulation
127
+ // This allows buffer[0], buffer[1] syntax while keeping internal buffer private
128
+ const proxy = new Proxy(this, {
129
+ get(target, prop) {
130
+ if (typeof prop === 'string' && !isNaN(Number(prop))) {
131
+ return target.buffer[Number(prop)];
132
+ }
133
+ return (target as any)[prop];
134
+ }
135
+ });
136
+ return proxy as any;
137
+ }
138
+
139
+ /**
140
+ * Read 32-bit unsigned integer in little-endian format
141
+ *
142
+ * Reads 4 bytes starting at offset and interprets them as a little-endian
143
+ * unsigned 32-bit integer. Used for cryptographic operations requiring
144
+ * specific byte ordering.
145
+ *
146
+ * @param offset - Starting position in buffer
147
+ * @returns 32-bit unsigned integer value
148
+ * @throws RangeError if offset is out of bounds
149
+ */
150
+ readUInt32LE(offset: number): number {
151
+ if (offset < 0 || offset + 4 > this.buffer.length) {
152
+ throw new RangeError('Offset out of bounds');
153
+ }
154
+ const b0 = this.buffer[offset];
155
+ const b1 = this.buffer[offset + 1];
156
+ const b2 = this.buffer[offset + 2];
157
+ const b3 = this.buffer[offset + 3];
158
+
159
+ if (b0 === undefined || b1 === undefined || b2 === undefined || b3 === undefined) {
160
+ throw new RangeError('Buffer read error');
161
+ }
162
+
163
+ // NOTE: Little-endian byte order: least significant byte first
164
+ // b0 is LSB, b3 is MSB. Using unsigned right shift to ensure positive result
165
+ return (b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)) >>> 0;
166
+ }
167
+
168
+ /**
169
+ * Convert buffer to string with specified encoding
170
+ *
171
+ * Supports multiple encodings for compatibility with different use cases:
172
+ * - 'hex': Hexadecimal representation (2 chars per byte)
173
+ * - 'base64': Base64 encoding using browser's btoa() or fallback
174
+ * - default: UTF-8 text decoding
175
+ *
176
+ * @param encoding - Output encoding ('hex', 'base64', or undefined for UTF-8)
177
+ * @returns Encoded string representation
178
+ */
179
+ toString(encoding?: string): string {
180
+ if (encoding === 'hex') {
181
+ return Array.from(this.buffer)
182
+ .map(b => b.toString(16).padStart(2, '0'))
183
+ .join('');
184
+ }
185
+ if (encoding === 'base64') {
186
+ const binString = Array.from(this.buffer, (byte: number) => String.fromCodePoint(byte)).join('');
187
+ // NOTE: btoa() is browser-specific, fallback to raw binary string in Node.js
188
+ if (typeof btoa !== 'undefined') {
189
+ return btoa(binString);
190
+ }
191
+ return binString;
192
+ }
193
+ return new TextDecoder().decode(this.buffer);
194
+ }
195
+
196
+ get length(): number {
197
+ return this.buffer.length;
198
+ }
199
+
200
+ /**
201
+ * Iterator implementation for for...of loops and spread operator
202
+ *
203
+ * Allows the buffer to be used in modern JavaScript iteration constructs.
204
+ * Provides sequential access to individual bytes.
205
+ *
206
+ * @returns Iterator yielding individual byte values
207
+ */
208
+ [Symbol.iterator](): Iterator<number> {
209
+ let index = 0;
210
+ const buffer = this.buffer;
211
+ return {
212
+ next(): IteratorResult<number> {
213
+ if (index < buffer.length) {
214
+ const val = buffer[index++];
215
+ if (val === undefined) {
216
+ return { value: 0, done: true };
217
+ }
218
+ return { value: val, done: false };
219
+ }
220
+ return { value: 0, done: true };
221
+ }
222
+ };
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Incremental hash computation class
228
+ *
229
+ * Provides Node.js crypto.createHash() compatible interface.
230
+ * Supports incremental updates for large data processing.
231
+ * Uses custom hash algorithm optimized for browser compatibility.
232
+ */
233
+ export class CryptoHash {
234
+ private dataChunks: Uint8Array[] = [];
235
+
236
+ /**
237
+ * Add data to the hash computation
238
+ *
239
+ * Can be called multiple times to hash large or streaming data.
240
+ * Data is accumulated internally until digest() is called.
241
+ *
242
+ * @param input - Data to hash (string or Uint8Array)
243
+ * @returns This instance for method chaining
244
+ */
245
+ update(input: string | Uint8Array): this {
246
+ const inputBytes = typeof input === 'string'
247
+ ? new TextEncoder().encode(input)
248
+ : input;
249
+
250
+ this.dataChunks.push(inputBytes);
251
+ return this;
252
+ }
253
+
254
+ /**
255
+ * Combine all accumulated data chunks into a single buffer
256
+ *
257
+ * Internal method used before hash computation.
258
+ * Efficiently concatenates all chunks into contiguous memory.
259
+ *
260
+ * @returns Combined Uint8Array of all input data
261
+ */
262
+ private getCombinedData(): Uint8Array {
263
+ const totalLength = this.dataChunks.reduce((sum, chunk) => sum + chunk.length, 0);
264
+ const combined = new Uint8Array(totalLength);
265
+ let offset = 0;
266
+
267
+ for (const chunk of this.dataChunks) {
268
+ combined.set(chunk, offset);
269
+ offset += chunk.length;
270
+ }
271
+
272
+ return combined;
273
+ }
274
+
275
+ /**
276
+ * Finalize hash computation and return result
277
+ *
278
+ * Computes the hash of all accumulated data and returns it in the specified encoding.
279
+ * After calling digest(), the hash instance should not be used for further updates.
280
+ *
281
+ * @param encoding - Output encoding ('hex', 'base64', or undefined for binary)
282
+ * @returns Hash digest as encoded string
283
+ */
284
+ digest(encoding?: string): string {
285
+ const combined = this.getCombinedData();
286
+ const hashArray = this.computeHashSync(combined);
287
+
288
+ if (encoding === 'hex') {
289
+ return Array.from(hashArray)
290
+ .map(b => b.toString(16).padStart(2, '0'))
291
+ .join('');
292
+ }
293
+ if (encoding === 'base64') {
294
+ const binString = Array.from(hashArray, (byte: number) => String.fromCodePoint(byte)).join('');
295
+ if (typeof btoa !== 'undefined') {
296
+ return btoa(binString);
297
+ }
298
+ return binString;
299
+ }
300
+
301
+ return new TextDecoder().decode(hashArray);
302
+ }
303
+
304
+ /**
305
+ * Compute hash using custom synchronous algorithm
306
+ *
307
+ * Implements a deterministic hash function optimized for browser compatibility.
308
+ * Uses multiple rounds of mixing operations to ensure good distribution.
309
+ *
310
+ * Algorithm overview:
311
+ * 1. Initial distribution: XOR and multiply operations
312
+ * 2. Diffusion rounds: Neighbor-based mixing with round constants
313
+ *
314
+ * @param data - Input data to hash
315
+ * @returns 32-byte hash array
316
+ */
317
+ private computeHashSync(data: Uint8Array): Uint8Array {
318
+ const simpleHash = new Uint8Array(32);
319
+
320
+ for (let i = 0; i < data.length; i++) {
321
+ const byteVal = data[i];
322
+ if (byteVal === undefined) continue;
323
+
324
+ const idx1 = i % 32;
325
+ const idx2 = (i * 7) % 32; // NOTE: Prime multiplier for better distribution
326
+
327
+ const val1 = simpleHash[idx1];
328
+ const val2 = simpleHash[idx2];
329
+
330
+ if (val1 !== undefined) {
331
+ simpleHash[idx1] = val1 ^ byteVal;
332
+ }
333
+
334
+ if (val2 !== undefined) {
335
+ simpleHash[idx2] = ((val2 + byteVal) * 31) & 0xFF; // NOTE: 31 is prime for avalanche effect
336
+ }
337
+ }
338
+
339
+ for (let round = 0; round < 16; round++) {
340
+ for (let i = 0; i < 32; i++) {
341
+ const prevIdx = (i + 31) % 32; // Previous element (wrap around)
342
+ const nextIdx = (i + 1) % 32; // Next element (wrap around)
343
+
344
+ const prev = simpleHash[prevIdx];
345
+ const curr = simpleHash[i];
346
+ const next = simpleHash[nextIdx];
347
+
348
+ if (prev !== undefined && curr !== undefined && next !== undefined) {
349
+ // NOTE: Complex mixing function combining neighbors with round constant
350
+ simpleHash[i] = ((prev + curr * 2 + next) * 7 + round) & 0xFF;
351
+ }
352
+ }
353
+ }
354
+
355
+ return simpleHash;
356
+ }
357
+ }
358
+
359
+ export const randomBytes = (size: number): ICryptoBuffer => CryptoUtils.randomBytes(size);
360
+ export const createHash = (algorithm: string): CryptoHash => CryptoUtils.createHash(algorithm);
@@ -1,4 +1,4 @@
1
- import { randomBytes } from 'crypto';
1
+ import { randomBytes } from './crypto';
2
2
  import type { CustomQuestion } from '../types';
3
3
 
4
4
  export class CustomQuestionGenerator {
@@ -1,4 +1,4 @@
1
- import { randomBytes } from 'crypto';
1
+ import { randomBytes } from './crypto';
2
2
  import enData from '../locale/en.json';
3
3
  import trData from '../locale/tr.json';
4
4
 
@@ -1,4 +1,4 @@
1
- import { randomBytes } from 'crypto';
1
+ import { randomBytes } from './crypto';
2
2
 
3
3
  export class Random {
4
4
  static getRandomNumber(difficulty: 'easy' | 'medium' | 'hard'): number {
@@ -16,7 +16,7 @@ export class Random {
16
16
 
17
17
  static getRandomOperator(): string {
18
18
  const operators = ['+', '-', '*', '/'];
19
- const buffer = randomBytes(1);
19
+ const buffer = randomBytes(1) as any;
20
20
  const index = buffer[0]! % operators.length;
21
21
  return operators[index]!;
22
22
  }
@@ -25,7 +25,7 @@ export class Random {
25
25
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
26
26
  let result = '';
27
27
  const length = difficulty === 'easy' ? 4 : difficulty === 'medium' ? 6 : 8;
28
- const buffer = randomBytes(length);
28
+ const buffer = randomBytes(length) as any;
29
29
  // pick random chars from the charset using crypto random bytes
30
30
  for (let i = 0; i < length; i++) {
31
31
  result += chars.charAt(buffer[i]! % chars.length);
@@ -1,4 +1,4 @@
1
- import { randomBytes } from 'crypto';
1
+ import { randomBytes } from './crypto';
2
2
  import enData from '../locale/en.json';
3
3
  import trData from '../locale/tr.json';
4
4
 
@@ -1,4 +1,4 @@
1
- import { randomBytes } from 'crypto';
1
+ import { randomBytes } from './crypto';
2
2
 
3
3
  export class ScrambleGenerator {
4
4
  private static words: string[] = [
@@ -1,4 +1,4 @@
1
- import { randomBytes } from 'crypto';
1
+ import { randomBytes } from './crypto';
2
2
 
3
3
  export class SequenceGenerator {
4
4
  static generate(difficulty: 'easy' | 'medium' | 'hard'): { question: string; answer: number | string } {