Spaces:
Running
Running
Create keylock.js
Browse files- 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 |
+
}
|