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 +1 -1
- package/src/core/captchaGenerator.ts +2 -2
- package/src/core/captchaValidator.ts +1 -1
- package/src/utils/crypto.ts +360 -0
- package/src/utils/customQuestionGenerator.ts +1 -1
- package/src/utils/logicGenerator.ts +1 -1
- package/src/utils/random.ts +3 -3
- package/src/utils/riddleBank.ts +1 -1
- package/src/utils/scrambleGenerator.ts +1 -1
- package/src/utils/sequenceGenerator.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "k9guard",
|
|
3
|
-
"version": "1.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;
|
|
@@ -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);
|
package/src/utils/random.ts
CHANGED
|
@@ -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);
|
package/src/utils/riddleBank.ts
CHANGED