Spaces:
Sleeping
Sleeping
Delete keylock-decoder.js
Browse files- keylock-decoder.js +0 -202
keylock-decoder.js
DELETED
@@ -1,202 +0,0 @@
|
|
1 |
-
/**
|
2 |
-
* KeyLock-JS-Decoder
|
3 |
-
* A client-side JavaScript module to decode data from images created by the KeyLock app.
|
4 |
-
* Uses Web Crypto API for decryption and HTML Canvas for LSB steganography extraction.
|
5 |
-
*
|
6 |
-
* @version 1.1.0
|
7 |
-
* @license MIT
|
8 |
-
*/
|
9 |
-
|
10 |
-
/**
|
11 |
-
* Converts a PEM-formatted key string (PKCS8) to a DER ArrayBuffer.
|
12 |
-
* This is a necessary preprocessing step for the Web Crypto API.
|
13 |
-
* @param {string} pem The PEM key string.
|
14 |
-
* @returns {ArrayBuffer} The key in DER format.
|
15 |
-
*/
|
16 |
-
function pemToDer(pem) {
|
17 |
-
const b64 = pem
|
18 |
-
.replace(/-----BEGIN PRIVATE KEY-----/g, '')
|
19 |
-
.replace(/-----END PRIVATE KEY-----/g, '')
|
20 |
-
.replace(/\s/g, '');
|
21 |
-
|
22 |
-
const binaryDer = atob(b64);
|
23 |
-
const buffer = new ArrayBuffer(binaryDer.length);
|
24 |
-
const bytes = new Uint8Array(buffer);
|
25 |
-
for (let i = 0; i < binaryDer.length; i++) {
|
26 |
-
bytes[i] = binaryDer.charCodeAt(i);
|
27 |
-
}
|
28 |
-
return buffer;
|
29 |
-
}
|
30 |
-
|
31 |
-
/**
|
32 |
-
* Extracts the hidden data payload from an image's LSBs using a Canvas.
|
33 |
-
* @param {HTMLImageElement} imageElement The <img> element containing the stego image.
|
34 |
-
* @returns {Promise<Uint8Array>} A promise that resolves with the extracted data payload.
|
35 |
-
*/
|
36 |
-
function extractDataFromImage(imageElement) {
|
37 |
-
return new Promise((resolve, reject) => {
|
38 |
-
const canvas = document.createElement('canvas');
|
39 |
-
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
40 |
-
|
41 |
-
// Ensure the canvas is the same size as the image to avoid scaling artifacts
|
42 |
-
canvas.width = imageElement.naturalWidth;
|
43 |
-
canvas.height = imageElement.naturalHeight;
|
44 |
-
ctx.drawImage(imageElement, 0, 0);
|
45 |
-
|
46 |
-
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
|
47 |
-
const channels = 4; // RGBA
|
48 |
-
|
49 |
-
// 1. Extract the header to find the data length
|
50 |
-
const HEADER_BITS = 32; // 4 bytes for the length header
|
51 |
-
if (imageData.length < Math.ceil(HEADER_BITS / 3) * channels) {
|
52 |
-
return reject(new Error("Image is too small to contain a valid header."));
|
53 |
-
}
|
54 |
-
|
55 |
-
let headerBinaryString = '';
|
56 |
-
for (let i = 0; headerBinaryString.length < HEADER_BITS; i++) {
|
57 |
-
const pixelIndex = i * channels;
|
58 |
-
headerBinaryString += imageData[pixelIndex] & 1; // R
|
59 |
-
if (headerBinaryString.length < HEADER_BITS) headerBinaryString += imageData[pixelIndex + 1] & 1; // G
|
60 |
-
if (headerBinaryString.length < HEADER_BITS) headerBinaryString += imageData[pixelIndex + 2] & 1; // B
|
61 |
-
}
|
62 |
-
|
63 |
-
const headerBytes = new Uint8Array(HEADER_BITS / 8);
|
64 |
-
for (let i = 0; i < HEADER_BITS / 8; i++) {
|
65 |
-
headerBytes[i] = parseInt(headerBinaryString.substring(i * 8, (i + 1) * 8), 2);
|
66 |
-
}
|
67 |
-
|
68 |
-
const dataView = new DataView(headerBytes.buffer);
|
69 |
-
const dataLengthInBytes = dataView.getUint32(0, false); // false for big-endian
|
70 |
-
|
71 |
-
if (dataLengthInBytes === 0) {
|
72 |
-
return resolve(new Uint8Array(0));
|
73 |
-
}
|
74 |
-
|
75 |
-
// 2. Extract the data payload
|
76 |
-
const dataLengthInBits = dataLengthInBytes * 8;
|
77 |
-
const bitOffset = HEADER_BITS;
|
78 |
-
|
79 |
-
if (imageData.length < Math.ceil((bitOffset + dataLengthInBits) / 3) * channels) {
|
80 |
-
return reject(new Error("Image is too small for the data specified in the header. File may be corrupt."));
|
81 |
-
}
|
82 |
-
|
83 |
-
let dataBinaryString = '';
|
84 |
-
for (let i = 0; dataBinaryString.length < dataLengthInBits; i++) {
|
85 |
-
const bitPosition = bitOffset + i;
|
86 |
-
const pixelIndex = Math.floor(bitPosition / 3) * channels;
|
87 |
-
const channelOffset = bitPosition % 3;
|
88 |
-
dataBinaryString += imageData[pixelIndex + channelOffset] & 1;
|
89 |
-
}
|
90 |
-
|
91 |
-
if (dataBinaryString.length !== dataLengthInBits) {
|
92 |
-
return reject(new Error(`Data extraction error: expected ${dataLengthInBits} bits, but got ${dataBinaryString.length}. Image may be truncated or corrupt.`));
|
93 |
-
}
|
94 |
-
|
95 |
-
const dataBytes = new Uint8Array(dataLengthInBytes);
|
96 |
-
for (let i = 0; i < dataLengthInBytes; i++) {
|
97 |
-
dataBytes[i] = parseInt(dataBinaryString.substring(i * 8, (i + 1) * 8), 2);
|
98 |
-
}
|
99 |
-
|
100 |
-
resolve(dataBytes);
|
101 |
-
});
|
102 |
-
}
|
103 |
-
|
104 |
-
/**
|
105 |
-
* Decrypts a hybrid RSA-AES payload using the Web Crypto API.
|
106 |
-
* @param {Uint8Array} cryptoPayload The full encrypted payload.
|
107 |
-
* @param {string} privateKeyPem The recipient's private key in PEM format.
|
108 |
-
* @returns {Promise<object>} A promise that resolves with the decrypted JavaScript object.
|
109 |
-
*/
|
110 |
-
async function decryptHybridPayload(cryptoPayload, privateKeyPem) {
|
111 |
-
// --- Constants from Python core.py ---
|
112 |
-
const ENCRYPTED_AES_KEY_LEN_SIZE = 4;
|
113 |
-
const AES_GCM_NONCE_SIZE = 12;
|
114 |
-
|
115 |
-
// 1. Import the RSA private key
|
116 |
-
const privateKey = await crypto.subtle.importKey(
|
117 |
-
'pkcs8',
|
118 |
-
pemToDer(privateKeyPem),
|
119 |
-
{ name: 'RSA-OAEP', hash: 'SHA-256' },
|
120 |
-
true,
|
121 |
-
['decrypt']
|
122 |
-
);
|
123 |
-
|
124 |
-
// 2. Parse the crypto payload
|
125 |
-
const encryptedAesKeyLen = new DataView(cryptoPayload.buffer, 0, ENCRYPTED_AES_KEY_LEN_SIZE).getUint32(0, false);
|
126 |
-
|
127 |
-
let offset = ENCRYPTED_AES_KEY_LEN_SIZE;
|
128 |
-
const encryptedAesKey = cryptoPayload.slice(offset, offset + encryptedAesKeyLen);
|
129 |
-
|
130 |
-
offset += encryptedAesKeyLen;
|
131 |
-
const nonce = cryptoPayload.slice(offset, offset + AES_GCM_NONCE_SIZE);
|
132 |
-
|
133 |
-
offset += AES_GCM_NONCE_SIZE;
|
134 |
-
const ciphertextWithTag = cryptoPayload.slice(offset);
|
135 |
-
|
136 |
-
// 3. Decrypt the AES key with the RSA private key
|
137 |
-
const decryptedAesKeyBytes = await crypto.subtle.decrypt(
|
138 |
-
{ name: 'RSA-OAEP' },
|
139 |
-
privateKey,
|
140 |
-
encryptedAesKey
|
141 |
-
);
|
142 |
-
|
143 |
-
// 4. Import the decrypted AES key
|
144 |
-
const aesKey = await crypto.subtle.importKey(
|
145 |
-
'raw',
|
146 |
-
decryptedAesKeyBytes,
|
147 |
-
{ name: 'AES-GCM', length: 256 },
|
148 |
-
true,
|
149 |
-
['decrypt']
|
150 |
-
);
|
151 |
-
|
152 |
-
// 5. Decrypt the final data with the AES key
|
153 |
-
const decryptedDataBuffer = await crypto.subtle.decrypt(
|
154 |
-
{ name: 'AES-GCM', iv: nonce },
|
155 |
-
aesKey,
|
156 |
-
ciphertextWithTag
|
157 |
-
);
|
158 |
-
|
159 |
-
// 6. Decode the result from UTF-8 and parse as JSON
|
160 |
-
const decryptedText = new TextDecoder().decode(decryptedDataBuffer);
|
161 |
-
return JSON.parse(decryptedText);
|
162 |
-
}
|
163 |
-
|
164 |
-
|
165 |
-
/**
|
166 |
-
* Main public function. Decodes an auth object from a KeyLock image.
|
167 |
-
*
|
168 |
-
* @param {HTMLImageElement} imageElement The <img> element displaying the KeyLock PNG.
|
169 |
-
* Note: The image must be served from the same origin or have CORS headers
|
170 |
-
* allowing it to be drawn on a canvas. For file uploads, this is not an issue.
|
171 |
-
* @param {string} privateKeyPem The user's private RSA key in PEM format.
|
172 |
-
* @returns {Promise<object>} A promise that resolves to the decoded credentials object.
|
173 |
-
* @throws {Error} Throws an error if decoding or decryption fails.
|
174 |
-
*/
|
175 |
-
export async function decodeAuthFromImage(imageElement, privateKeyPem) {
|
176 |
-
if (!imageElement || !(imageElement instanceof HTMLImageElement)) {
|
177 |
-
throw new Error("A valid HTMLImageElement must be provided.");
|
178 |
-
}
|
179 |
-
if (!privateKeyPem || typeof privateKeyPem !== 'string') {
|
180 |
-
throw new Error("A valid private key in PEM format (string) must be provided.");
|
181 |
-
}
|
182 |
-
|
183 |
-
try {
|
184 |
-
// Step 1: Extract the encrypted byte payload from the image LSBs
|
185 |
-
const cryptoPayload = await extractDataFromImage(imageElement);
|
186 |
-
if (cryptoPayload.length === 0) {
|
187 |
-
throw new Error("No data found in image. It may not be a valid KeyLock file.");
|
188 |
-
}
|
189 |
-
|
190 |
-
// Step 2: Decrypt the payload
|
191 |
-
const credentials = await decryptHybridPayload(cryptoPayload, privateKeyPem);
|
192 |
-
return credentials;
|
193 |
-
|
194 |
-
} catch (error) {
|
195 |
-
// Provide a more user-friendly error message for common failures
|
196 |
-
if (error.name === 'OperationError' || (error.message && error.message.toLowerCase().includes('decryption failed'))) {
|
197 |
-
throw new Error("Decryption failed. This usually means the private key is incorrect or the image data is corrupted.");
|
198 |
-
}
|
199 |
-
// Re-throw other errors
|
200 |
-
throw error;
|
201 |
-
}
|
202 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|