// keylock.js class KeyLock { constructor() { this.PREFERRED_FONTS = "bold 30px Arial, 'DejaVu Sans', Helvetica, sans-serif"; } // --- Core Cryptography Methods --- /** * Converts a PEM-formatted key string to an ArrayBuffer. * @param {string} pem - The PEM string (e.g., -----BEGIN...-----). * @returns {ArrayBuffer} The raw binary data of the key. */ _pemToBinary(pem) { const lines = pem.split('\n'); const base64 = lines .filter(line => !line.startsWith('-----')) .join(''); const binaryDer = window.atob(base64); const uint8Array = new Uint8Array(binaryDer.length); for (let i = 0; i < binaryDer.length; i++) { uint8Array[i] = binaryDer.charCodeAt(i); } return uint8Array.buffer; } /** * Converts an ArrayBuffer key to a PEM-formatted string. * @param {ArrayBuffer} buffer - The raw binary data of the key. * @param {string} label - The label for the PEM file (e.g., "PRIVATE KEY"). * @returns {string} The PEM-formatted key string. */ _binaryToPem(buffer, label) { const binaryStr = String.fromCharCode.apply(null, new Uint8Array(buffer)); const base64 = window.btoa(binaryStr); let pem = `-----BEGIN ${label}-----\n`; for (let i = 0; i < base64.length; i += 64) { pem += base64.slice(i, i + 64) + '\n'; } pem += `-----END ${label}-----`; return pem; } /** * Imports an RSA public key from PEM format into a CryptoKey object. * @param {string} pem - The public key in PEM format (SPKI). * @returns {Promise} */ async importRsaPublicKey(pem) { return await window.crypto.subtle.importKey( 'spki', this._pemToBinary(pem), { name: 'RSA-OAEP', hash: 'SHA-256' }, true, ['encrypt'] ); } /** * Imports an RSA private key from PEM format into a CryptoKey object. * @param {string} pem - The private key in PEM format (PKCS#8). * @returns {Promise} */ async importRsaPrivateKey(pem) { return await window.crypto.subtle.importKey( 'pkcs8', this._pemToBinary(pem), { name: 'RSA-OAEP', hash: 'SHA-256' }, true, ['decrypt'] ); } /** * Generates a new RSA-2048 key pair. * @returns {Promise<{privateKeyPem: string, publicKeyPem: string}>} */ async generatePemKeys() { const keyPair = await window.crypto.subtle.generateKey({ name: 'RSA-OAEP', modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), // 65537 hash: 'SHA-256', }, true, ['encrypt', 'decrypt'] ); const privateKeyDer = await window.crypto.subtle.exportKey('pkcs8', keyPair.privateKey); const publicKeyDer = await window.crypto.subtle.exportKey('spki', keyPair.publicKey); return { privateKeyPem: this._binaryToPem(privateKeyDer, 'PRIVATE KEY'), publicKeyPem: this._binaryToPem(publicKeyDer, 'PUBLIC KEY'), }; } // --- Image Generation and Steganography --- /** * Generates a procedural starfield image on a canvas. * @param {number} w - Width of the canvas. * @param {number} h - Height of the canvas. * @returns {HTMLCanvasElement} */ _generateStarfieldImage(w = 800, h = 800) { const canvas = document.createElement('canvas'); canvas.width = w; canvas.height = h; const ctx = canvas.getContext('2d'); // Background gradient const centerX = w / 2; const centerY = h / 2; const gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, w / 2); gradient.addColorStop(0, 'rgb(20, 25, 40)'); gradient.addColorStop(1, 'rgb(0, 0, 5)'); ctx.fillStyle = gradient; ctx.fillRect(0, 0, w, h); // Dim stars for (let i = 0; i < (w * h) / 200; i++) { const x = Math.random() * w; const y = Math.random() * h; const brightness = 30 + Math.random() * 60; ctx.fillStyle = `rgb(${Math.floor(brightness * 0.9)}, ${Math.floor(brightness * 0.9)}, ${brightness})`; ctx.fillRect(x, y, 1, 1); } // Bright stars with glow const starColors = ['rgb(255, 255, 255)', 'rgb(220, 230, 255)', 'rgb(255, 240, 220)']; for (let i = 0; i < (w * h) / 1000; i++) { const x = Math.random() * w; const y = Math.random() * h; const size = 0.5 + 2.5 * (Math.random() ** 2); const brightness = 120 + 135 * (Math.random() ** 1.5); const color = starColors[Math.floor(Math.random() * starColors.length)]; ctx.beginPath(); const glowGradient = ctx.createRadialGradient(x, y, 0, x, y, size * 3); glowGradient.addColorStop(0, color.replace(')', `, ${brightness / 255})`).replace('rgb', 'rgba')); glowGradient.addColorStop(1, color.replace(')', ', 0)').replace('rgb', 'rgba')); ctx.fillStyle = glowGradient; ctx.arc(x, y, size * 3, 0, 2 * Math.PI); ctx.fill(); ctx.beginPath(); ctx.fillStyle = color.replace(')', `, ${brightness / 255})`).replace('rgb', 'rgba'); ctx.arc(x, y, size, 0, 2 * Math.PI); ctx.fill(); } return canvas; } /** * Draws the text overlay on the image. * @param {HTMLCanvasElement} canvas - The canvas to draw on. * @returns {HTMLCanvasElement} The same canvas, now with an overlay. */ _drawOverlay(canvas) { const ctx = canvas.getContext('2d'); const { width, height } = canvas; ctx.fillStyle = 'rgba(10, 15, 30, 0.78)'; // 200/255 alpha ctx.fillRect(0, 20, width, 60); ctx.fillStyle = 'rgb(200, 220, 255)'; ctx.font = this.PREFERRED_FONTS; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText("KeyLock Secure Data", width / 2, 50); return canvas; } /** * Parses a key-value string into a JavaScript object. * @param {string} kvString - The input string (e.g., 'USER="test"\nPASS:123'). * @returns {object} */ _parseKvString(kvString) { const payload = {}; if (!kvString) return payload; const lines = kvString.trim().split('\n'); for (const line of lines) { const trimmedLine = line.trim(); if (!trimmedLine || trimmedLine.startsWith('#')) continue; const parts = trimmedLine.split(/[:=]/, 2); if (parts.length === 2) { let [key, value] = parts; key = key.trim().replace(/^['"]|['"]$/g, ''); value = value.trim().replace(/^['"]|['"]$/g, ''); if (key) { payload[key] = value; } } } return payload; } // --- Main Public Methods: Encoding and Decoding --- /** * Generates an encrypted image containing the payload. * @param {string} payloadKvString - The key-value data to encrypt. * @param {string} publicKeyPem - The RSA public key in PEM format. * @returns {Promise} A data URL of the generated PNG image. */ async generateEncryptedImage(payloadKvString, publicKeyPem) { const payloadDict = this._parseKvString(payloadKvString); if (Object.keys(payloadDict).length === 0) { throw new Error("Payload is empty or could not be parsed."); } // 1. Prepare crypto primitives const publicKey = await this.importRsaPublicKey(publicKeyPem); const aesKey = await window.crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']); const nonce = window.crypto.getRandomValues(new Uint8Array(12)); // 2. Encrypt payload with AES const jsonBytes = new TextEncoder().encode(JSON.stringify(payloadDict)); const ciphertext = await window.crypto.subtle.encrypt({ name: 'AES-GCM', iv: nonce }, aesKey, jsonBytes); // 3. Encrypt AES key with RSA const exportedAesKey = await window.crypto.subtle.exportKey('raw', aesKey); const encryptedAesKey = await window.crypto.subtle.encrypt({ name: 'RSA-OAEP' }, publicKey, exportedAesKey); // 4. Construct the final binary payload // Format: [4-byte len of RSA key][RSA key][12-byte nonce][AES ciphertext] const totalLength = 4 + encryptedAesKey.byteLength + nonce.byteLength + ciphertext.byteLength; const finalPayload = new Uint8Array(totalLength); const view = new DataView(finalPayload.buffer); let offset = 0; view.setUint32(offset, encryptedAesKey.byteLength, false); // Big-endian offset += 4; finalPayload.set(new Uint8Array(encryptedAesKey), offset); offset += encryptedAesKey.byteLength; finalPayload.set(nonce, offset); offset += nonce.byteLength; finalPayload.set(new Uint8Array(ciphertext), offset); // 5. Generate base image const canvas = this._generateStarfieldImage(); this._drawOverlay(canvas); const ctx = canvas.getContext('2d'); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const pixelData = imageData.data; // 6. Embed payload into image via LSB steganography // Format: [32-bit length header][payload bits] const payloadWithHeader = new Uint8Array(4 + finalPayload.length); const headerView = new DataView(payloadWithHeader.buffer); headerView.setUint32(0, finalPayload.length, false); // Big-endian payloadWithHeader.set(finalPayload, 4); let binaryPayload = ''; payloadWithHeader.forEach(byte => { binaryPayload += byte.toString(2).padStart(8, '0'); }); if (binaryPayload.length > pixelData.length) { throw new Error("Payload is too large for the image."); } for (let i = 0; i < binaryPayload.length; i++) { pixelData[i] = (pixelData[i] & 0xFE) | parseInt(binaryPayload[i], 2); } // 7. Finalize and return image ctx.putImageData(imageData, 0, 0); return canvas.toDataURL('image/png'); } /** * Decodes a payload hidden inside an image. * @param {HTMLImageElement} imageElement - The image containing the hidden data. * @param {string} privateKeyPem - The RSA private key in PEM format. * @returns {Promise<{status: string, payload?: object, message?: string}>} */ async decodePayload(imageElement, privateKeyPem) { try { const privateKey = await this.importRsaPrivateKey(privateKeyPem); // 1. Extract pixel data from image const canvas = document.createElement('canvas'); canvas.width = imageElement.naturalWidth; canvas.height = imageElement.naturalHeight; const ctx = canvas.getContext('2d'); ctx.drawImage(imageElement, 0, 0); const pixelData = ctx.getImageData(0, 0, canvas.width, canvas.height).data; // 2. Extract binary data from LSBs let headerBinary = ''; for (let i = 0; i < 32; i++) { headerBinary += (pixelData[i] & 1).toString(); } const dataLength = parseInt(headerBinary, 2); // Length in bytes const requiredPixels = 32 + dataLength * 8; if (requiredPixels > pixelData.length) { throw new Error("Incomplete payload in image."); } let dataBinary = ''; for (let i = 32; i < requiredPixels; i++) { dataBinary += (pixelData[i] & 1).toString(); } // 3. Convert binary string to ArrayBuffer const cryptoPayload = new Uint8Array(dataLength); for (let i = 0; i < dataLength; i++) { cryptoPayload[i] = parseInt(dataBinary.substring(i * 8, (i + 1) * 8), 2); } // 4. Parse the crypto payload const view = new DataView(cryptoPayload.buffer); let offset = 0; const encryptedAesKeyLen = view.getUint32(offset, false); // Big-endian offset += 4; const encryptedAesKey = cryptoPayload.slice(offset, offset + encryptedAesKeyLen); offset += encryptedAesKeyLen; const nonce = cryptoPayload.slice(offset, offset + 12); offset += 12; const ciphertext = cryptoPayload.slice(offset); // 5. Decrypt AES key with RSA private key const recoveredAesKeyBytes = await window.crypto.subtle.decrypt({ name: 'RSA-OAEP' }, privateKey, encryptedAesKey); const recoveredAesKey = await window.crypto.subtle.importKey('raw', recoveredAesKeyBytes, { name: 'AES-GCM' }, true, ['decrypt']); // 6. Decrypt payload with AES key const decryptedPayloadBytes = await window.crypto.subtle.decrypt({ name: 'AES-GCM', iv: nonce }, recoveredAesKey, ciphertext); const payloadJson = new TextDecoder().decode(decryptedPayloadBytes); const payload = JSON.parse(payloadJson); return { status: "Success", payload: payload }; } catch (e) { console.error("Decryption Failed:", e); return { status: "Error", message: `Decryption Failed: ${e.message}` }; } } }