broadfield-dev commited on
Commit
f32080a
·
verified ·
1 Parent(s): 055da99

Create keylock.js

Browse files
Files changed (1) hide show
  1. keylock.js +351 -0
keylock.js ADDED
@@ -0,0 +1,351 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // keylock.js
2
+
3
+ class KeyLock {
4
+
5
+ constructor() {
6
+ this.PREFERRED_FONTS = "bold 30px Arial, 'DejaVu Sans', Helvetica, sans-serif";
7
+ }
8
+
9
+ // --- Core Cryptography Methods ---
10
+
11
+ /**
12
+ * Converts a PEM-formatted key string to an ArrayBuffer.
13
+ * @param {string} pem - The PEM string (e.g., -----BEGIN...-----).
14
+ * @returns {ArrayBuffer} The raw binary data of the key.
15
+ */
16
+ _pemToBinary(pem) {
17
+ const lines = pem.split('\n');
18
+ const base64 = lines
19
+ .filter(line => !line.startsWith('-----'))
20
+ .join('');
21
+ const binaryDer = window.atob(base64);
22
+ const uint8Array = new Uint8Array(binaryDer.length);
23
+ for (let i = 0; i < binaryDer.length; i++) {
24
+ uint8Array[i] = binaryDer.charCodeAt(i);
25
+ }
26
+ return uint8Array.buffer;
27
+ }
28
+
29
+ /**
30
+ * Converts an ArrayBuffer key to a PEM-formatted string.
31
+ * @param {ArrayBuffer} buffer - The raw binary data of the key.
32
+ * @param {string} label - The label for the PEM file (e.g., "PRIVATE KEY").
33
+ * @returns {string} The PEM-formatted key string.
34
+ */
35
+ _binaryToPem(buffer, label) {
36
+ const binaryStr = String.fromCharCode.apply(null, new Uint8Array(buffer));
37
+ const base64 = window.btoa(binaryStr);
38
+ let pem = `-----BEGIN ${label}-----\n`;
39
+ for (let i = 0; i < base64.length; i += 64) {
40
+ pem += base64.slice(i, i + 64) + '\n';
41
+ }
42
+ pem += `-----END ${label}-----`;
43
+ return pem;
44
+ }
45
+
46
+ /**
47
+ * Imports an RSA public key from PEM format into a CryptoKey object.
48
+ * @param {string} pem - The public key in PEM format (SPKI).
49
+ * @returns {Promise<CryptoKey>}
50
+ */
51
+ async importRsaPublicKey(pem) {
52
+ return await window.crypto.subtle.importKey(
53
+ 'spki',
54
+ this._pemToBinary(pem), {
55
+ name: 'RSA-OAEP',
56
+ hash: 'SHA-256'
57
+ },
58
+ true,
59
+ ['encrypt']
60
+ );
61
+ }
62
+
63
+ /**
64
+ * Imports an RSA private key from PEM format into a CryptoKey object.
65
+ * @param {string} pem - The private key in PEM format (PKCS#8).
66
+ * @returns {Promise<CryptoKey>}
67
+ */
68
+ async importRsaPrivateKey(pem) {
69
+ return await window.crypto.subtle.importKey(
70
+ 'pkcs8',
71
+ this._pemToBinary(pem), {
72
+ name: 'RSA-OAEP',
73
+ hash: 'SHA-256'
74
+ },
75
+ true,
76
+ ['decrypt']
77
+ );
78
+ }
79
+
80
+ /**
81
+ * Generates a new RSA-2048 key pair.
82
+ * @returns {Promise<{privateKeyPem: string, publicKeyPem: string}>}
83
+ */
84
+ async generatePemKeys() {
85
+ const keyPair = await window.crypto.subtle.generateKey({
86
+ name: 'RSA-OAEP',
87
+ modulusLength: 2048,
88
+ publicExponent: new Uint8Array([1, 0, 1]), // 65537
89
+ hash: 'SHA-256',
90
+ },
91
+ true,
92
+ ['encrypt', 'decrypt']
93
+ );
94
+
95
+ const privateKeyDer = await window.crypto.subtle.exportKey('pkcs8', keyPair.privateKey);
96
+ const publicKeyDer = await window.crypto.subtle.exportKey('spki', keyPair.publicKey);
97
+
98
+ return {
99
+ privateKeyPem: this._binaryToPem(privateKeyDer, 'PRIVATE KEY'),
100
+ publicKeyPem: this._binaryToPem(publicKeyDer, 'PUBLIC KEY'),
101
+ };
102
+ }
103
+
104
+ // --- Image Generation and Steganography ---
105
+
106
+ /**
107
+ * Generates a procedural starfield image on a canvas.
108
+ * @param {number} w - Width of the canvas.
109
+ * @param {number} h - Height of the canvas.
110
+ * @returns {HTMLCanvasElement}
111
+ */
112
+ _generateStarfieldImage(w = 800, h = 800) {
113
+ const canvas = document.createElement('canvas');
114
+ canvas.width = w;
115
+ canvas.height = h;
116
+ const ctx = canvas.getContext('2d');
117
+
118
+ // Background gradient
119
+ const centerX = w / 2;
120
+ const centerY = h / 2;
121
+ const gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, w / 2);
122
+ gradient.addColorStop(0, 'rgb(20, 25, 40)');
123
+ gradient.addColorStop(1, 'rgb(0, 0, 5)');
124
+ ctx.fillStyle = gradient;
125
+ ctx.fillRect(0, 0, w, h);
126
+
127
+ // Dim stars
128
+ for (let i = 0; i < (w * h) / 200; i++) {
129
+ const x = Math.random() * w;
130
+ const y = Math.random() * h;
131
+ const brightness = 30 + Math.random() * 60;
132
+ ctx.fillStyle = `rgb(${Math.floor(brightness * 0.9)}, ${Math.floor(brightness * 0.9)}, ${brightness})`;
133
+ ctx.fillRect(x, y, 1, 1);
134
+ }
135
+
136
+ // Bright stars with glow
137
+ const starColors = ['rgb(255, 255, 255)', 'rgb(220, 230, 255)', 'rgb(255, 240, 220)'];
138
+ for (let i = 0; i < (w * h) / 1000; i++) {
139
+ const x = Math.random() * w;
140
+ const y = Math.random() * h;
141
+ const size = 0.5 + 2.5 * (Math.random() ** 2);
142
+ const brightness = 120 + 135 * (Math.random() ** 1.5);
143
+ const color = starColors[Math.floor(Math.random() * starColors.length)];
144
+
145
+ ctx.beginPath();
146
+ const glowGradient = ctx.createRadialGradient(x, y, 0, x, y, size * 3);
147
+ glowGradient.addColorStop(0, color.replace(')', `, ${brightness / 255})`).replace('rgb', 'rgba'));
148
+ glowGradient.addColorStop(1, color.replace(')', ', 0)').replace('rgb', 'rgba'));
149
+ ctx.fillStyle = glowGradient;
150
+ ctx.arc(x, y, size * 3, 0, 2 * Math.PI);
151
+ ctx.fill();
152
+
153
+ ctx.beginPath();
154
+ ctx.fillStyle = color.replace(')', `, ${brightness / 255})`).replace('rgb', 'rgba');
155
+ ctx.arc(x, y, size, 0, 2 * Math.PI);
156
+ ctx.fill();
157
+ }
158
+ return canvas;
159
+ }
160
+
161
+ /**
162
+ * Draws the text overlay on the image.
163
+ * @param {HTMLCanvasElement} canvas - The canvas to draw on.
164
+ * @returns {HTMLCanvasElement} The same canvas, now with an overlay.
165
+ */
166
+ _drawOverlay(canvas) {
167
+ const ctx = canvas.getContext('2d');
168
+ const { width, height } = canvas;
169
+
170
+ ctx.fillStyle = 'rgba(10, 15, 30, 0.78)'; // 200/255 alpha
171
+ ctx.fillRect(0, 20, width, 60);
172
+
173
+ ctx.fillStyle = 'rgb(200, 220, 255)';
174
+ ctx.font = this.PREFERRED_FONTS;
175
+ ctx.textAlign = 'center';
176
+ ctx.textBaseline = 'middle';
177
+ ctx.fillText("KeyLock Secure Data", width / 2, 50);
178
+
179
+ return canvas;
180
+ }
181
+
182
+ /**
183
+ * Parses a key-value string into a JavaScript object.
184
+ * @param {string} kvString - The input string (e.g., 'USER="test"\nPASS:123').
185
+ * @returns {object}
186
+ */
187
+ _parseKvString(kvString) {
188
+ const payload = {};
189
+ if (!kvString) return payload;
190
+
191
+ const lines = kvString.trim().split('\n');
192
+ for (const line of lines) {
193
+ const trimmedLine = line.trim();
194
+ if (!trimmedLine || trimmedLine.startsWith('#')) continue;
195
+
196
+ const parts = trimmedLine.split(/[:=]/, 2);
197
+ if (parts.length === 2) {
198
+ let [key, value] = parts;
199
+ key = key.trim().replace(/^['"]|['"]$/g, '');
200
+ value = value.trim().replace(/^['"]|['"]$/g, '');
201
+ if (key) {
202
+ payload[key] = value;
203
+ }
204
+ }
205
+ }
206
+ return payload;
207
+ }
208
+
209
+ // --- Main Public Methods: Encoding and Decoding ---
210
+
211
+ /**
212
+ * Generates an encrypted image containing the payload.
213
+ * @param {string} payloadKvString - The key-value data to encrypt.
214
+ * @param {string} publicKeyPem - The RSA public key in PEM format.
215
+ * @returns {Promise<string>} A data URL of the generated PNG image.
216
+ */
217
+ async generateEncryptedImage(payloadKvString, publicKeyPem) {
218
+ const payloadDict = this._parseKvString(payloadKvString);
219
+ if (Object.keys(payloadDict).length === 0) {
220
+ throw new Error("Payload is empty or could not be parsed.");
221
+ }
222
+
223
+ // 1. Prepare crypto primitives
224
+ const publicKey = await this.importRsaPublicKey(publicKeyPem);
225
+ const aesKey = await window.crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
226
+ const nonce = window.crypto.getRandomValues(new Uint8Array(12));
227
+
228
+ // 2. Encrypt payload with AES
229
+ const jsonBytes = new TextEncoder().encode(JSON.stringify(payloadDict));
230
+ const ciphertext = await window.crypto.subtle.encrypt({ name: 'AES-GCM', iv: nonce }, aesKey, jsonBytes);
231
+
232
+ // 3. Encrypt AES key with RSA
233
+ const exportedAesKey = await window.crypto.subtle.exportKey('raw', aesKey);
234
+ const encryptedAesKey = await window.crypto.subtle.encrypt({ name: 'RSA-OAEP' }, publicKey, exportedAesKey);
235
+
236
+ // 4. Construct the final binary payload
237
+ // Format: [4-byte len of RSA key][RSA key][12-byte nonce][AES ciphertext]
238
+ const totalLength = 4 + encryptedAesKey.byteLength + nonce.byteLength + ciphertext.byteLength;
239
+ const finalPayload = new Uint8Array(totalLength);
240
+ const view = new DataView(finalPayload.buffer);
241
+
242
+ let offset = 0;
243
+ view.setUint32(offset, encryptedAesKey.byteLength, false); // Big-endian
244
+ offset += 4;
245
+ finalPayload.set(new Uint8Array(encryptedAesKey), offset);
246
+ offset += encryptedAesKey.byteLength;
247
+ finalPayload.set(nonce, offset);
248
+ offset += nonce.byteLength;
249
+ finalPayload.set(new Uint8Array(ciphertext), offset);
250
+
251
+ // 5. Generate base image
252
+ const canvas = this._generateStarfieldImage();
253
+ this._drawOverlay(canvas);
254
+ const ctx = canvas.getContext('2d');
255
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
256
+ const pixelData = imageData.data;
257
+
258
+ // 6. Embed payload into image via LSB steganography
259
+ // Format: [32-bit length header][payload bits]
260
+ const payloadWithHeader = new Uint8Array(4 + finalPayload.length);
261
+ const headerView = new DataView(payloadWithHeader.buffer);
262
+ headerView.setUint32(0, finalPayload.length, false); // Big-endian
263
+ payloadWithHeader.set(finalPayload, 4);
264
+
265
+ let binaryPayload = '';
266
+ payloadWithHeader.forEach(byte => {
267
+ binaryPayload += byte.toString(2).padStart(8, '0');
268
+ });
269
+
270
+ if (binaryPayload.length > pixelData.length) {
271
+ throw new Error("Payload is too large for the image.");
272
+ }
273
+
274
+ for (let i = 0; i < binaryPayload.length; i++) {
275
+ pixelData[i] = (pixelData[i] & 0xFE) | parseInt(binaryPayload[i], 2);
276
+ }
277
+
278
+ // 7. Finalize and return image
279
+ ctx.putImageData(imageData, 0, 0);
280
+ return canvas.toDataURL('image/png');
281
+ }
282
+
283
+ /**
284
+ * Decodes a payload hidden inside an image.
285
+ * @param {HTMLImageElement} imageElement - The image containing the hidden data.
286
+ * @param {string} privateKeyPem - The RSA private key in PEM format.
287
+ * @returns {Promise<{status: string, payload?: object, message?: string}>}
288
+ */
289
+ async decodePayload(imageElement, privateKeyPem) {
290
+ try {
291
+ const privateKey = await this.importRsaPrivateKey(privateKeyPem);
292
+
293
+ // 1. Extract pixel data from image
294
+ const canvas = document.createElement('canvas');
295
+ canvas.width = imageElement.naturalWidth;
296
+ canvas.height = imageElement.naturalHeight;
297
+ const ctx = canvas.getContext('2d');
298
+ ctx.drawImage(imageElement, 0, 0);
299
+ const pixelData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
300
+
301
+ // 2. Extract binary data from LSBs
302
+ let headerBinary = '';
303
+ for (let i = 0; i < 32; i++) {
304
+ headerBinary += (pixelData[i] & 1).toString();
305
+ }
306
+ const dataLength = parseInt(headerBinary, 2); // Length in bytes
307
+ const requiredPixels = 32 + dataLength * 8;
308
+ if (requiredPixels > pixelData.length) {
309
+ throw new Error("Incomplete payload in image.");
310
+ }
311
+
312
+ let dataBinary = '';
313
+ for (let i = 32; i < requiredPixels; i++) {
314
+ dataBinary += (pixelData[i] & 1).toString();
315
+ }
316
+
317
+ // 3. Convert binary string to ArrayBuffer
318
+ const cryptoPayload = new Uint8Array(dataLength);
319
+ for (let i = 0; i < dataLength; i++) {
320
+ cryptoPayload[i] = parseInt(dataBinary.substring(i * 8, (i + 1) * 8), 2);
321
+ }
322
+
323
+ // 4. Parse the crypto payload
324
+ const view = new DataView(cryptoPayload.buffer);
325
+ let offset = 0;
326
+ const encryptedAesKeyLen = view.getUint32(offset, false); // Big-endian
327
+ offset += 4;
328
+
329
+ const encryptedAesKey = cryptoPayload.slice(offset, offset + encryptedAesKeyLen);
330
+ offset += encryptedAesKeyLen;
331
+ const nonce = cryptoPayload.slice(offset, offset + 12);
332
+ offset += 12;
333
+ const ciphertext = cryptoPayload.slice(offset);
334
+
335
+ // 5. Decrypt AES key with RSA private key
336
+ const recoveredAesKeyBytes = await window.crypto.subtle.decrypt({ name: 'RSA-OAEP' }, privateKey, encryptedAesKey);
337
+ const recoveredAesKey = await window.crypto.subtle.importKey('raw', recoveredAesKeyBytes, { name: 'AES-GCM' }, true, ['decrypt']);
338
+
339
+ // 6. Decrypt payload with AES key
340
+ const decryptedPayloadBytes = await window.crypto.subtle.decrypt({ name: 'AES-GCM', iv: nonce }, recoveredAesKey, ciphertext);
341
+ const payloadJson = new TextDecoder().decode(decryptedPayloadBytes);
342
+ const payload = JSON.parse(payloadJson);
343
+
344
+ return { status: "Success", payload: payload };
345
+
346
+ } catch (e) {
347
+ console.error("Decryption Failed:", e);
348
+ return { status: "Error", message: `Decryption Failed: ${e.message}` };
349
+ }
350
+ }
351
+ }