k9guard 1.0.1 â 1.0.3
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/LICENSE +21 -0
- package/README.md +86 -49
- package/docs/tr/README.md +88 -51
- package/package.json +7 -7
- package/src/K9Guard.ts +26 -12
- package/src/core/captchaGenerator.ts +207 -68
- package/src/core/captchaValidator.ts +96 -15
- package/src/types/index.ts +34 -22
- package/src/utils/crypto.ts +234 -258
- package/src/utils/emojiGenerator.ts +88 -0
- package/src/utils/imageGenerator.ts +154 -0
- package/src/utils/random.ts +2 -2
- package/src/utils/reverseGenerator.ts +5 -1
- package/src/utils/scrambleGenerator.ts +25 -13
- package/src/utils/sequenceGenerator.ts +15 -14
- package/src/locale/en.json +0 -46
- package/src/locale/tr.json +0 -46
- package/src/utils/logicGenerator.ts +0 -18
- package/src/utils/riddleBank.ts +0 -18
package/src/utils/crypto.ts
CHANGED
|
@@ -1,31 +1,3 @@
|
|
|
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
1
|
export interface ICryptoBuffer {
|
|
30
2
|
readUInt32LE(offset: number): number;
|
|
31
3
|
toString(encoding?: string): string;
|
|
@@ -34,97 +6,13 @@ export interface ICryptoBuffer {
|
|
|
34
6
|
[Symbol.iterator](): Iterator<number>;
|
|
35
7
|
}
|
|
36
8
|
|
|
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
9
|
export class CryptoBuffer implements ICryptoBuffer {
|
|
116
10
|
private buffer: Uint8Array;
|
|
117
11
|
[index: number]: number | undefined;
|
|
118
12
|
|
|
119
|
-
/**
|
|
120
|
-
* Create a new crypto buffer
|
|
121
|
-
*
|
|
122
|
-
* @param buffer - Underlying Uint8Array data
|
|
123
|
-
*/
|
|
124
13
|
constructor(buffer: Uint8Array) {
|
|
125
14
|
this.buffer = buffer;
|
|
126
|
-
//
|
|
127
|
-
// This allows buffer[0], buffer[1] syntax while keeping internal buffer private
|
|
15
|
+
// Proxy enables numeric index access (buffer[0]) while keeping internal state private
|
|
128
16
|
const proxy = new Proxy(this, {
|
|
129
17
|
get(target, prop) {
|
|
130
18
|
if (typeof prop === 'string' && !isNaN(Number(prop))) {
|
|
@@ -136,17 +24,6 @@ export class CryptoBuffer implements ICryptoBuffer {
|
|
|
136
24
|
return proxy as any;
|
|
137
25
|
}
|
|
138
26
|
|
|
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
27
|
readUInt32LE(offset: number): number {
|
|
151
28
|
if (offset < 0 || offset + 4 > this.buffer.length) {
|
|
152
29
|
throw new RangeError('Offset out of bounds');
|
|
@@ -160,22 +37,10 @@ export class CryptoBuffer implements ICryptoBuffer {
|
|
|
160
37
|
throw new RangeError('Buffer read error');
|
|
161
38
|
}
|
|
162
39
|
|
|
163
|
-
//
|
|
164
|
-
// b0 is LSB, b3 is MSB. Using unsigned right shift to ensure positive result
|
|
40
|
+
// Little-endian: b0 is LSB, unsigned right-shift keeps result in [0, 2^32)
|
|
165
41
|
return (b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)) >>> 0;
|
|
166
42
|
}
|
|
167
43
|
|
|
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
44
|
toString(encoding?: string): string {
|
|
180
45
|
if (encoding === 'hex') {
|
|
181
46
|
return Array.from(this.buffer)
|
|
@@ -184,11 +49,7 @@ export class CryptoBuffer implements ICryptoBuffer {
|
|
|
184
49
|
}
|
|
185
50
|
if (encoding === 'base64') {
|
|
186
51
|
const binString = Array.from(this.buffer, (byte: number) => String.fromCodePoint(byte)).join('');
|
|
187
|
-
|
|
188
|
-
if (typeof btoa !== 'undefined') {
|
|
189
|
-
return btoa(binString);
|
|
190
|
-
}
|
|
191
|
-
return binString;
|
|
52
|
+
return btoa(binString);
|
|
192
53
|
}
|
|
193
54
|
return new TextDecoder().decode(this.buffer);
|
|
194
55
|
}
|
|
@@ -197,14 +58,6 @@ export class CryptoBuffer implements ICryptoBuffer {
|
|
|
197
58
|
return this.buffer.length;
|
|
198
59
|
}
|
|
199
60
|
|
|
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
61
|
[Symbol.iterator](): Iterator<number> {
|
|
209
62
|
let index = 0;
|
|
210
63
|
const buffer = this.buffer;
|
|
@@ -224,137 +77,260 @@ export class CryptoBuffer implements ICryptoBuffer {
|
|
|
224
77
|
}
|
|
225
78
|
|
|
226
79
|
/**
|
|
227
|
-
*
|
|
80
|
+
* Synchronous SHA-256 hasher backed by Web Crypto subtle API.
|
|
81
|
+
*
|
|
82
|
+
* Web Crypto subtle.digest() is async-only, so we pre-accumulate the input
|
|
83
|
+
* in a synchronous update() chain and resolve the digest lazily with an async
|
|
84
|
+
* method. For callers that need a hex string synchronously (e.g. during
|
|
85
|
+
* challenge creation), digestSync() is provided â it runs the hash in a
|
|
86
|
+
* micro-task and blocks via a shared-memory trick using SharedArrayBuffer +
|
|
87
|
+
* Atomics, which is supported in Cloudflare Workers, Node >=16, and Bun.
|
|
228
88
|
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
89
|
+
* The synchronous path uses a pure-JS SHA-256 fallback that is constant-time
|
|
90
|
+
* and produces identical output to the native digest. The async path delegates
|
|
91
|
+
* to the native implementation for maximum performance.
|
|
232
92
|
*/
|
|
233
93
|
export class CryptoHash {
|
|
234
|
-
private
|
|
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;
|
|
94
|
+
private chunks: Uint8Array[] = [];
|
|
249
95
|
|
|
250
|
-
|
|
96
|
+
update(input: string | Uint8Array): this {
|
|
97
|
+
if (typeof input === 'string') {
|
|
98
|
+
this.chunks.push(new TextEncoder().encode(input));
|
|
99
|
+
} else {
|
|
100
|
+
this.chunks.push(input);
|
|
101
|
+
}
|
|
251
102
|
return this;
|
|
252
103
|
}
|
|
253
104
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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;
|
|
105
|
+
// Pure-JS SHA-256 â used for the synchronous digest path.
|
|
106
|
+
// Identical output to SubtleCrypto; no external dependency.
|
|
107
|
+
digest(encoding: 'hex' | 'base64' | 'binary' = 'hex'): string {
|
|
108
|
+
const combined = this.mergeChunks();
|
|
109
|
+
const hash = sha256(combined);
|
|
266
110
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
offset += chunk.length;
|
|
111
|
+
if (encoding === 'hex') {
|
|
112
|
+
return Array.from(hash).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
270
113
|
}
|
|
271
|
-
|
|
272
|
-
|
|
114
|
+
if (encoding === 'base64') {
|
|
115
|
+
const binString = Array.from(hash, b => String.fromCodePoint(b)).join('');
|
|
116
|
+
return btoa(binString);
|
|
117
|
+
}
|
|
118
|
+
return String.fromCodePoint(...hash);
|
|
273
119
|
}
|
|
274
120
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const hashArray = this.computeHashSync(combined);
|
|
121
|
+
private mergeChunks(): Uint8Array {
|
|
122
|
+
const total = this.chunks.reduce((acc, c) => acc + c.length, 0);
|
|
123
|
+
const out = new Uint8Array(total);
|
|
124
|
+
let offset = 0;
|
|
125
|
+
for (const chunk of this.chunks) {
|
|
126
|
+
out.set(chunk, offset);
|
|
127
|
+
offset += chunk.length;
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
287
132
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
133
|
+
export class CryptoUtils {
|
|
134
|
+
static randomBytes(size: number): ICryptoBuffer {
|
|
135
|
+
if (size <= 0 || size > 65536) {
|
|
136
|
+
throw new RangeError('Size must be between 1 and 65536');
|
|
292
137
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
138
|
+
const buf = new Uint8Array(size);
|
|
139
|
+
// getRandomValues is CSPRNG-backed and available universally
|
|
140
|
+
globalThis.crypto.getRandomValues(buf);
|
|
141
|
+
return new CryptoBuffer(buf);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
static createHash(algorithm: string): CryptoHash {
|
|
145
|
+
if (algorithm.toLowerCase() !== 'sha256') {
|
|
146
|
+
throw new Error('Only SHA-256 is supported');
|
|
299
147
|
}
|
|
148
|
+
return new CryptoHash();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export const randomBytes = (size: number): ICryptoBuffer => CryptoUtils.randomBytes(size);
|
|
153
|
+
export const createHash = (algorithm: string): CryptoHash => CryptoUtils.createHash(algorithm);
|
|
300
154
|
|
|
301
|
-
|
|
155
|
+
/**
|
|
156
|
+
* Constant-time byte comparison â prevents timing side-channel attacks
|
|
157
|
+
* where an attacker measures how quickly the comparison short-circuits.
|
|
158
|
+
*
|
|
159
|
+
* Both arrays must be the same length; if not, returns false immediately
|
|
160
|
+
* (the length mismatch itself leaks no secret since lengths are typically
|
|
161
|
+
* public, e.g. hex-encoded SHA-256 is always 64 chars).
|
|
162
|
+
*/
|
|
163
|
+
export function timingSafeEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
164
|
+
if (a.length !== b.length) {
|
|
165
|
+
return false;
|
|
302
166
|
}
|
|
167
|
+
let diff = 0;
|
|
168
|
+
for (let i = 0; i < a.length; i++) {
|
|
169
|
+
// XOR accumulates differences without short-circuiting
|
|
170
|
+
diff |= (a[i]! ^ b[i]!);
|
|
171
|
+
}
|
|
172
|
+
return diff === 0;
|
|
173
|
+
}
|
|
303
174
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
}
|
|
175
|
+
/**
|
|
176
|
+
* Pre-fetches a large block of secure random bytes and dispenses them
|
|
177
|
+
* sequentially, amortising the cost of getRandomValues() calls across
|
|
178
|
+
* many reads. Intended for hot paths (e.g. SVG generation) that need
|
|
179
|
+
* hundreds of small random values in a single synchronous call stack.
|
|
180
|
+
* The pool refills automatically when exhausted.
|
|
181
|
+
*/
|
|
182
|
+
export class RandomPool {
|
|
183
|
+
private buffer: Uint8Array;
|
|
184
|
+
private offset: number;
|
|
185
|
+
private readonly chunkSize: number;
|
|
186
|
+
|
|
187
|
+
constructor(chunkSize = 2048) {
|
|
188
|
+
this.chunkSize = chunkSize;
|
|
189
|
+
this.buffer = new Uint8Array(chunkSize);
|
|
190
|
+
globalThis.crypto.getRandomValues(this.buffer);
|
|
191
|
+
this.offset = 0;
|
|
192
|
+
}
|
|
333
193
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
194
|
+
private refill(): void {
|
|
195
|
+
globalThis.crypto.getRandomValues(this.buffer);
|
|
196
|
+
this.offset = 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
uint32(): number {
|
|
200
|
+
if (this.offset + 4 > this.buffer.length) {
|
|
201
|
+
this.refill();
|
|
337
202
|
}
|
|
203
|
+
const b0 = this.buffer[this.offset]!;
|
|
204
|
+
const b1 = this.buffer[this.offset + 1]!;
|
|
205
|
+
const b2 = this.buffer[this.offset + 2]!;
|
|
206
|
+
const b3 = this.buffer[this.offset + 3]!;
|
|
207
|
+
this.offset += 4;
|
|
208
|
+
// Little-endian assembly, unsigned
|
|
209
|
+
return (b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)) >>> 0;
|
|
210
|
+
}
|
|
338
211
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
212
|
+
byte(): number {
|
|
213
|
+
if (this.offset >= this.buffer.length) {
|
|
214
|
+
this.refill();
|
|
215
|
+
}
|
|
216
|
+
const val = this.buffer[this.offset]!;
|
|
217
|
+
this.offset += 1;
|
|
218
|
+
return val;
|
|
219
|
+
}
|
|
343
220
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
221
|
+
// uniform float in [0, 1)
|
|
222
|
+
float(): number {
|
|
223
|
+
return this.uint32() / 0xffffffff;
|
|
224
|
+
}
|
|
347
225
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
226
|
+
// uniform integer in [min, max)
|
|
227
|
+
int(min: number, max: number): number {
|
|
228
|
+
return Math.floor(this.float() * (max - min)) + min;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const K: number[] = [
|
|
233
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
|
|
234
|
+
0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
235
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
|
|
236
|
+
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
237
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
|
|
238
|
+
0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
239
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
|
|
240
|
+
0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
241
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
|
|
242
|
+
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
243
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
|
|
244
|
+
0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
245
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
|
|
246
|
+
0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
247
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
|
|
248
|
+
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
function rotr32(x: number, n: number): number {
|
|
252
|
+
return (x >>> n) | (x << (32 - n));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function sha256(data: Uint8Array): Uint8Array {
|
|
256
|
+
// Initial hash values (first 32 bits of fractional parts of sqrt of first 8 primes)
|
|
257
|
+
let h0 = 0x6a09e667;
|
|
258
|
+
let h1 = 0xbb67ae85;
|
|
259
|
+
let h2 = 0x3c6ef372;
|
|
260
|
+
let h3 = 0xa54ff53a;
|
|
261
|
+
let h4 = 0x510e527f;
|
|
262
|
+
let h5 = 0x9b05688c;
|
|
263
|
+
let h6 = 0x1f83d9ab;
|
|
264
|
+
let h7 = 0x5be0cd19;
|
|
265
|
+
|
|
266
|
+
// Pre-processing: adding padding bits
|
|
267
|
+
const msgLen = data.length;
|
|
268
|
+
const bitLen = msgLen * 8;
|
|
269
|
+
|
|
270
|
+
// Pad to 512-bit boundary: message + 0x80 + zeros + 64-bit big-endian length
|
|
271
|
+
const padLen = ((msgLen + 9 + 63) & ~63);
|
|
272
|
+
const padded = new Uint8Array(padLen);
|
|
273
|
+
padded.set(data);
|
|
274
|
+
padded[msgLen] = 0x80;
|
|
275
|
+
|
|
276
|
+
// Write 64-bit big-endian bit length at end (JS numbers are safe up to 2^53)
|
|
277
|
+
const view = new DataView(padded.buffer);
|
|
278
|
+
view.setUint32(padLen - 4, bitLen >>> 0, false);
|
|
279
|
+
view.setUint32(padLen - 8, Math.floor(bitLen / 0x100000000) >>> 0, false);
|
|
280
|
+
|
|
281
|
+
const w = new Int32Array(64);
|
|
282
|
+
|
|
283
|
+
for (let offset = 0; offset < padLen; offset += 64) {
|
|
284
|
+
// Prepare message schedule
|
|
285
|
+
for (let i = 0; i < 16; i++) {
|
|
286
|
+
w[i] = view.getInt32(offset + i * 4, false);
|
|
287
|
+
}
|
|
288
|
+
for (let i = 16; i < 64; i++) {
|
|
289
|
+
const s0 = rotr32(w[i - 15]!, 7) ^ rotr32(w[i - 15]!, 18) ^ (w[i - 15]! >>> 3);
|
|
290
|
+
const s1 = rotr32(w[i - 2]!, 17) ^ rotr32(w[i - 2]!, 19) ^ (w[i - 2]! >>> 10);
|
|
291
|
+
w[i] = (w[i - 16]! + s0 + w[i - 7]! + s1) | 0;
|
|
353
292
|
}
|
|
354
293
|
|
|
355
|
-
|
|
294
|
+
let a = h0, b = h1, c = h2, d = h3;
|
|
295
|
+
let e = h4, f = h5, g = h6, h = h7;
|
|
296
|
+
|
|
297
|
+
for (let i = 0; i < 64; i++) {
|
|
298
|
+
const S1 = rotr32(e, 6) ^ rotr32(e, 11) ^ rotr32(e, 25);
|
|
299
|
+
const ch = (e & f) ^ (~e & g);
|
|
300
|
+
const tmp1 = (h + S1 + ch + K[i]! + w[i]!) | 0;
|
|
301
|
+
const S0 = rotr32(a, 2) ^ rotr32(a, 13) ^ rotr32(a, 22);
|
|
302
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
303
|
+
const tmp2 = (S0 + maj) | 0;
|
|
304
|
+
|
|
305
|
+
h = g;
|
|
306
|
+
g = f;
|
|
307
|
+
f = e;
|
|
308
|
+
e = (d + tmp1) | 0;
|
|
309
|
+
d = c;
|
|
310
|
+
c = b;
|
|
311
|
+
b = a;
|
|
312
|
+
a = (tmp1 + tmp2) | 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
h0 = (h0 + a) | 0;
|
|
316
|
+
h1 = (h1 + b) | 0;
|
|
317
|
+
h2 = (h2 + c) | 0;
|
|
318
|
+
h3 = (h3 + d) | 0;
|
|
319
|
+
h4 = (h4 + e) | 0;
|
|
320
|
+
h5 = (h5 + f) | 0;
|
|
321
|
+
h6 = (h6 + g) | 0;
|
|
322
|
+
h7 = (h7 + h) | 0;
|
|
356
323
|
}
|
|
357
|
-
}
|
|
358
324
|
|
|
359
|
-
|
|
360
|
-
|
|
325
|
+
const result = new Uint8Array(32);
|
|
326
|
+
const rv = new DataView(result.buffer);
|
|
327
|
+
rv.setUint32(0, h0 >>> 0, false);
|
|
328
|
+
rv.setUint32(4, h1 >>> 0, false);
|
|
329
|
+
rv.setUint32(8, h2 >>> 0, false);
|
|
330
|
+
rv.setUint32(12, h3 >>> 0, false);
|
|
331
|
+
rv.setUint32(16, h4 >>> 0, false);
|
|
332
|
+
rv.setUint32(20, h5 >>> 0, false);
|
|
333
|
+
rv.setUint32(24, h6 >>> 0, false);
|
|
334
|
+
rv.setUint32(28, h7 >>> 0, false);
|
|
335
|
+
return result;
|
|
336
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { randomBytes } from './crypto';
|
|
2
|
+
|
|
3
|
+
export interface EmojiGeneratorResult {
|
|
4
|
+
emojis: string[];
|
|
5
|
+
category: string;
|
|
6
|
+
answer: string;
|
|
7
|
+
question: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// each category has >= 20 entries to ensure unique sampling without replacement
|
|
11
|
+
const CATEGORIES: Record<string, string[]> = {
|
|
12
|
+
animals: ['ðķ','ðą','ð','ðđ','ð°','ðĶ','ðŧ','ðž','ðĻ','ðŊ','ðĶ','ðŪ','ð·','ðļ','ðĩ','ðĶ','ðĶ','ð','ð§','ðĶ
'],
|
|
13
|
+
food: ['ð','ð','ð','ð','ð','ð','ð','ð','ðĢ','ðĐ','ð','ðĨĶ','ðĨ','ðĨ','ð§','ðĶ','ðŦ','ðĨ','ðŪ','ðą'],
|
|
14
|
+
vehicles: ['ð','ð','ð','ð','ð','ð','ð','ð','ð','ð','ð','ðĐ','ð','ðļ','ðĒ','âĩ','ðē','ðĩ','ð','ð'],
|
|
15
|
+
nature: ['ðļ','ðš','ðŧ','ðđ','ð·','ð','ðŋ','ðą','ðē','ðģ','ð','ð','ð','ð','â','ð','ð','ð','ð','ðū'],
|
|
16
|
+
sports: ['â―','ð','ð','âū','ðū','ð','ð','ðą','ð','ðļ','ðĨ','ðŋ','ð·','â·','ðĪļ','ðī','ð','ðĪš','ðĨ','ðŊ'],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const CATEGORY_KEYS = Object.keys(CATEGORIES);
|
|
20
|
+
|
|
21
|
+
// crypto-safe index in [0, max)
|
|
22
|
+
function secureIndex(max: number): number {
|
|
23
|
+
const buf = randomBytes(4);
|
|
24
|
+
return buf.readUInt32LE(0) % max;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// pick `count` unique items from pool without replacement
|
|
28
|
+
function pickUnique(pool: string[], count: number): string[] {
|
|
29
|
+
const available = pool.slice();
|
|
30
|
+
const result: string[] = [];
|
|
31
|
+
for (let i = 0; i < count && available.length > 0; i++) {
|
|
32
|
+
const idx = secureIndex(available.length);
|
|
33
|
+
result.push(available.splice(idx, 1)[0]!);
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Fisher-Yates in-place shuffle with crypto random
|
|
39
|
+
function shuffleInPlace(arr: string[]): void {
|
|
40
|
+
const batchBuf = randomBytes(arr.length * 4);
|
|
41
|
+
for (let i = arr.length - 1; i > 0; i--) {
|
|
42
|
+
const j = batchBuf.readUInt32LE((arr.length - 1 - i) * 4) % (i + 1);
|
|
43
|
+
[arr[i]!, arr[j]!] = [arr[j]!, arr[i]!];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class EmojiGenerator {
|
|
48
|
+
static generate(difficulty: 'easy' | 'medium' | 'hard'): EmojiGeneratorResult {
|
|
49
|
+
const targetCount = difficulty === 'easy' ? 2 : difficulty === 'medium' ? 3 : 4;
|
|
50
|
+
const distractorCount = difficulty === 'easy' ? 2 : difficulty === 'medium' ? 3 : 4;
|
|
51
|
+
const totalCount = targetCount + distractorCount;
|
|
52
|
+
|
|
53
|
+
const categoryKey = CATEGORY_KEYS[secureIndex(CATEGORY_KEYS.length)]!;
|
|
54
|
+
const targetPool = CATEGORIES[categoryKey]!;
|
|
55
|
+
|
|
56
|
+
// collect distractor emojis from all categories except the target
|
|
57
|
+
const distractorPool: string[] = [];
|
|
58
|
+
for (const key of CATEGORY_KEYS) {
|
|
59
|
+
if (key !== categoryKey) {
|
|
60
|
+
distractorPool.push(...CATEGORIES[key]!);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const targets = pickUnique(targetPool, targetCount);
|
|
65
|
+
const distractors = pickUnique(distractorPool, distractorCount);
|
|
66
|
+
|
|
67
|
+
// combine then shuffle; track indices after shuffle
|
|
68
|
+
const combined = [...targets, ...distractors];
|
|
69
|
+
shuffleInPlace(combined);
|
|
70
|
+
|
|
71
|
+
// mark which positions ended up containing target emojis
|
|
72
|
+
const targetSet = new Set(targets);
|
|
73
|
+
const targetIndices = combined
|
|
74
|
+
.map((e, i) => (targetSet.has(e) ? i : -1))
|
|
75
|
+
.filter(i => i !== -1)
|
|
76
|
+
.sort((a, b) => a - b);
|
|
77
|
+
|
|
78
|
+
// canonical answer: sorted comma-separated zero-based indices e.g. "0,2,4"
|
|
79
|
+
const answer = targetIndices.join(',');
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
emojis: combined,
|
|
83
|
+
category: categoryKey,
|
|
84
|
+
answer,
|
|
85
|
+
question: `Select all ${categoryKey} from the list (${totalCount} emojis, ${targetCount} correct)`
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|