KeyLock-JS / keylock.js
broadfield-dev's picture
Create keylock.js
f32080a verified
// 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<CryptoKey>}
*/
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<CryptoKey>}
*/
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<string>} 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}` };
}
}
}