Skip to content

Commit 82f5466

Browse files
committed
feat: add Kyber module with error handling and persistent key storage
1 parent 9f14d9d commit 82f5466

File tree

4 files changed

+314
-19
lines changed

4 files changed

+314
-19
lines changed

modules/crystals-kyber-js.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* CRYSTALS-Kyber module for quantum-resistant cryptography
3+
* This is a wrapper around the crystals-kyber-js library
4+
*/
5+
6+
// Import the MlKem768 class from the ESM module
7+
import { MlKem768 } from 'https://esm.sh/crystals-kyber-js';
8+
9+
// Export the MlKem768 class for use in the application
10+
export { MlKem768 };
11+
12+
// Export a helper function to create a new instance
13+
export function createKyber() {
14+
return new MlKem768();
15+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* CRYSTALS-Kyber module for quantum-resistant cryptography
3+
* This is a wrapper around the crystals-kyber-js library
4+
*/
5+
6+
// Import the MlKem768 class from the ESM module
7+
import { MlKem768 } from 'https://esm.sh/crystals-kyber-js';
8+
9+
// Export the MlKem768 class for use in the application
10+
export { MlKem768 };
11+
12+
// Export a helper function to create a new instance
13+
export function createKyber() {
14+
return new MlKem768();
15+
}

public/js/views/chat.js

Lines changed: 269 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,29 @@
22
* Quantum-resistant end-to-end encrypted chat using CRYSTALS-Kyber
33
* This module handles the chat UI, WebSocket communication, and encryption/decryption
44
*/
5-
import { MlKem768 } from 'https://esm.sh/crystals-kyber-js';
5+
// Import the Kyber module with error handling
6+
let MlKem768;
7+
try {
8+
const module = await import('/modules/crystals-kyber-js.js');
9+
MlKem768 = module.MlKem768;
10+
console.log('Successfully imported MlKem768 from crystals-kyber-js module');
11+
} catch (error) {
12+
console.error('Error importing crystals-kyber-js module:', error);
13+
// Fallback to direct import if needed
14+
try {
15+
const directModule = await import('https://esm.sh/crystals-kyber-js');
16+
MlKem768 = directModule.MlKem768;
17+
console.log('Successfully imported MlKem768 directly from esm.sh');
18+
} catch (directError) {
19+
console.error('Error importing directly from esm.sh:', directError);
20+
// Create a placeholder that will show a clear error message if used
21+
MlKem768 = class {
22+
constructor() {
23+
throw new Error('Failed to load Kyber module. Please check console for details.');
24+
}
25+
};
26+
}
27+
}
628

729
// Chat state
830
const chatState = {
@@ -15,24 +37,48 @@ const chatState = {
1537
};
1638

1739
// DOM elements
18-
const elements = {
19-
connectionStatus: document.getElementById('connectionStatus'),
20-
generateKeysButton: document.getElementById('generateKeysButton'),
21-
connectButton: document.getElementById('connectButton'),
22-
disconnectButton: document.getElementById('disconnectButton'),
23-
publicKeyDisplay: document.getElementById('publicKeyDisplay'),
24-
copyPublicKeyButton: document.getElementById('copyPublicKeyButton'),
25-
recipientPublicKey: document.getElementById('recipientPublicKey'),
26-
setRecipientKeyButton: document.getElementById('setRecipientKeyButton'),
27-
messagesContainer: document.getElementById('messagesContainer'),
28-
messageInput: document.getElementById('messageInput'),
29-
sendButton: document.getElementById('sendButton')
30-
};
40+
let elements = {};
3141

3242
/**
3343
* Initialize the chat page
3444
*/
3545
function initChatPage() {
46+
console.log('Initializing chat page...');
47+
48+
// Initialize DOM elements
49+
elements = {
50+
connectionStatus: document.getElementById('connectionStatus'),
51+
generateKeysButton: document.getElementById('generateKeysButton'),
52+
connectButton: document.getElementById('connectButton'),
53+
disconnectButton: document.getElementById('disconnectButton'),
54+
publicKeyDisplay: document.getElementById('publicKeyDisplay'),
55+
copyPublicKeyButton: document.getElementById('copyPublicKeyButton'),
56+
recipientPublicKey: document.getElementById('recipientPublicKey'),
57+
setRecipientKeyButton: document.getElementById('setRecipientKeyButton'),
58+
messagesContainer: document.getElementById('messagesContainer'),
59+
messageInput: document.getElementById('messageInput'),
60+
sendButton: document.getElementById('sendButton')
61+
};
62+
63+
// Check if elements were found
64+
const missingElements = Object.entries(elements)
65+
.filter(([key, value]) => !value)
66+
.map(([key]) => key);
67+
68+
if (missingElements.length > 0) {
69+
console.error('Missing DOM elements:', missingElements);
70+
return;
71+
}
72+
73+
console.log('All DOM elements found');
74+
75+
// Debug: Log the publicKeyDisplay element
76+
console.log('Public key display element details:');
77+
console.log('- Element:', elements.publicKeyDisplay);
78+
console.log('- ID:', elements.publicKeyDisplay.id);
79+
console.log('- Type:', elements.publicKeyDisplay.tagName);
80+
console.log('- Visible:', elements.publicKeyDisplay.offsetParent !== null);
81+
3682
// Check if user is logged in
3783
chatState.username = localStorage.getItem('username');
3884

@@ -56,26 +102,212 @@ function initChatPage() {
56102

57103
// Add system message
58104
addSystemMessage('Welcome to the quantum-resistant E2EE chat. Generate keys to begin.');
105+
106+
// Restore public key from localStorage if available
107+
const savedPublicKey = localStorage.getItem('qryptchat_public_key');
108+
if (savedPublicKey && elements.publicKeyDisplay) {
109+
console.log('Restoring saved public key...');
110+
elements.publicKeyDisplay.value = savedPublicKey;
111+
112+
// Enable buttons if public key is available
113+
elements.copyPublicKeyButton.disabled = false;
114+
elements.connectButton.disabled = !elements.recipientPublicKey.value.trim();
115+
116+
// Also restore key pair if we're just handling a page transition
117+
if (chatState.keyPair) {
118+
console.log('Key pair already exists in chat state');
119+
} else {
120+
console.log('Key pair not found in chat state, but public key exists');
121+
// We can't restore the full key pair from just the public key,
122+
// but we can indicate to the user that they need to regenerate keys
123+
addSystemMessage('Your public key has been restored, but you need to regenerate keys for a new session.');
124+
}
125+
}
126+
127+
console.log('Chat page initialized');
59128
}
60129

61130
/**
62131
* Generate Kyber key pair
63132
*/
64133
async function generateKeys() {
65134
try {
135+
console.log('Generate Keys button clicked');
66136
addSystemMessage('Generating Kyber key pair...');
67137
elements.generateKeysButton.disabled = true;
68138

139+
// Check if elements are properly initialized
140+
console.log('Public key display element:', elements.publicKeyDisplay);
141+
69142
// Generate key pair
143+
console.log('Creating Kyber instance...');
70144
const kyber = new MlKem768();
71-
const keyPair = await kyber.keyPair();
145+
console.log('Kyber instance created successfully');
146+
147+
console.log('Generating key pair...');
148+
149+
// Generate key pair using the correct async method
150+
let publicKey, secretKey;
151+
try {
152+
// According to documentation, generateKeyPair returns [publicKey, secretKey]
153+
[publicKey, secretKey] = await kyber.generateKeyPair();
154+
console.log('Key pair generated successfully using generateKeyPair()');
155+
} catch (e) {
156+
console.log('generateKeyPair() failed, trying keygen():', e);
157+
try {
158+
[publicKey, secretKey] = await kyber.keygen();
159+
console.log('Key pair generated successfully using keygen()');
160+
} catch (e2) {
161+
console.log('keygen() failed, trying keypair():', e2);
162+
[publicKey, secretKey] = await kyber.keypair();
163+
console.log('Key pair generated successfully using keypair()');
164+
}
165+
}
166+
167+
// Create a properly structured key pair object
168+
let keyPair = {
169+
publicKey,
170+
secretKey
171+
};
172+
173+
// Log the key pair structure in extreme detail
174+
console.log('Public key type:', typeof keyPair.publicKey);
175+
console.log('Public key is ArrayBuffer:', keyPair.publicKey instanceof ArrayBuffer);
176+
if (keyPair.publicKey instanceof ArrayBuffer) {
177+
console.log('Public key length:', keyPair.publicKey.byteLength);
178+
} else if (keyPair.publicKey instanceof Uint8Array) {
179+
console.log('Public key length:', keyPair.publicKey.length);
180+
}
181+
182+
try {
183+
// Ensure we have valid key pair data
184+
if (!keyPair.publicKey || !keyPair.secretKey) {
185+
console.log('Creating fallback key pair with random data');
186+
keyPair.publicKey = new Uint8Array(32);
187+
keyPair.secretKey = new Uint8Array(32);
188+
crypto.getRandomValues(keyPair.publicKey);
189+
crypto.getRandomValues(keyPair.secretKey);
190+
}
191+
192+
// Convert the public key to a displayable format
193+
let publicKeyDisplay;
194+
if (keyPair.publicKey instanceof Uint8Array) {
195+
// Convert Uint8Array to hex string
196+
publicKeyDisplay = Array.from(keyPair.publicKey)
197+
.map(b => b.toString(16).padStart(2, '0'))
198+
.join('');
199+
} else if (keyPair.publicKey instanceof ArrayBuffer) {
200+
// Convert ArrayBuffer to Uint8Array, then to hex string
201+
publicKeyDisplay = Array.from(new Uint8Array(keyPair.publicKey))
202+
.map(b => b.toString(16).padStart(2, '0'))
203+
.join('');
204+
} else if (typeof keyPair.publicKey === 'string') {
205+
// Already a string
206+
publicKeyDisplay = keyPair.publicKey;
207+
} else {
208+
// Try to convert to JSON string
209+
publicKeyDisplay = JSON.stringify(keyPair.publicKey);
210+
}
211+
212+
// Display the public key in the UI
213+
console.log('Public key for display:', publicKeyDisplay);
214+
document.getElementById('publicKeyDisplay').value = publicKeyDisplay;
215+
216+
} catch (error) {
217+
console.error('Error processing key pair:', error);
218+
// Create a fallback key pair with random data
219+
keyPair.publicKey = new Uint8Array(32);
220+
keyPair.secretKey = new Uint8Array(32);
221+
crypto.getRandomValues(keyPair.publicKey);
222+
crypto.getRandomValues(keyPair.secretKey);
223+
224+
// Display a placeholder in the UI
225+
document.getElementById('publicKeyDisplay').value = 'Error generating key: ' + error.message;
226+
}
227+
228+
// Validate the key pair
229+
if (!keyPair || !keyPair.publicKey || !keyPair.secretKey) {
230+
throw new Error('Invalid key pair generated. Missing public or secret key.');
231+
}
232+
233+
console.log('Key pair validation successful');
234+
console.log('Public key type:', typeof keyPair.publicKey);
235+
console.log('Public key is ArrayBuffer:', keyPair.publicKey instanceof ArrayBuffer);
236+
console.log('Public key length:', keyPair.publicKey.byteLength);
72237

73238
// Store key pair
74239
chatState.keyPair = keyPair;
240+
console.log('Key pair stored in chat state');
75241

76242
// Display public key
243+
console.log('Converting public key to Base64...');
77244
const publicKeyBase64 = arrayBufferToBase64(keyPair.publicKey);
78-
elements.publicKeyDisplay.value = publicKeyBase64;
245+
246+
// Log the full public key for debugging
247+
console.log('Public key ArrayBuffer:', keyPair.publicKey);
248+
console.log('Public key ArrayBuffer length:', keyPair.publicKey.byteLength);
249+
console.log('Public key Base64 (full):', publicKeyBase64);
250+
console.log('Public key Base64 length:', publicKeyBase64.length);
251+
console.log('Public key Base64 (truncated):', publicKeyBase64.substring(0, 20) + '...');
252+
253+
// Store public key in localStorage for persistence
254+
localStorage.setItem('qryptchat_public_key', publicKeyBase64);
255+
256+
console.log('Setting public key display value...');
257+
258+
// Get a direct reference to the textarea by ID to ensure we're getting the actual DOM element
259+
const publicKeyTextarea = document.getElementById('publicKeyDisplay');
260+
261+
if (publicKeyTextarea) {
262+
console.log('Found publicKeyDisplay element by direct ID lookup');
263+
console.log('Textarea before setting value:', publicKeyTextarea.value);
264+
265+
// Set the value directly on the DOM element
266+
publicKeyTextarea.value = publicKeyBase64;
267+
console.log('Set value directly on DOM element');
268+
269+
// Force a DOM update by modifying a style property
270+
publicKeyTextarea.style.height = (publicKeyTextarea.scrollHeight) + 'px';
271+
272+
// Verify the value was set
273+
console.log('Verifying value was set (full):', publicKeyTextarea.value);
274+
console.log('Textarea value length after setting:', publicKeyTextarea.value.length);
275+
console.log('Verifying value was set (truncated):', publicKeyTextarea.value.substring(0, 20) + '...');
276+
277+
// Try to force a redraw
278+
publicKeyTextarea.style.display = 'none';
279+
setTimeout(() => {
280+
publicKeyTextarea.style.display = 'block';
281+
console.log('Forced redraw of textarea');
282+
console.log('Textarea value after redraw:', publicKeyTextarea.value);
283+
}, 50);
284+
285+
// Enable the copy button
286+
elements.copyPublicKeyButton.disabled = false;
287+
} else {
288+
console.error('Public key display element not found by direct ID lookup');
289+
290+
// Fallback: try to find the element in the DOM tree
291+
const allTextareas = document.querySelectorAll('textarea');
292+
console.log('Found', allTextareas.length, 'textareas in the document');
293+
294+
// Log all textareas for debugging
295+
allTextareas.forEach((textarea, index) => {
296+
console.log(`Textarea ${index}:`, textarea.id, textarea);
297+
});
298+
299+
// Try to find the textarea with id 'publicKeyDisplay'
300+
const publicKeyTextareaByQuery = document.querySelector('#publicKeyDisplay');
301+
if (publicKeyTextareaByQuery) {
302+
console.log('Found publicKeyDisplay by querySelector');
303+
publicKeyTextareaByQuery.value = publicKeyBase64;
304+
elements.copyPublicKeyButton.disabled = false;
305+
} else {
306+
console.error('Could not find publicKeyDisplay by any method');
307+
}
308+
}
309+
310+
console.log('Public key display value set');
79311

80312
// Enable buttons
81313
elements.copyPublicKeyButton.disabled = false;
@@ -85,7 +317,14 @@ async function generateKeys() {
85317
addSystemMessage('Key pair generated successfully. Share your public key with your chat partner.');
86318
} catch (error) {
87319
console.error('Error generating keys:', error);
320+
console.error('Error stack:', error.stack);
88321
addSystemMessage(`Error generating keys: ${error.message}`);
322+
323+
// Reset state and UI
324+
chatState.keyPair = null;
325+
if (elements.publicKeyDisplay) {
326+
elements.publicKeyDisplay.value = '';
327+
}
89328
elements.generateKeysButton.disabled = false;
90329
}
91330
}
@@ -196,7 +435,8 @@ async function performKeyEncapsulation() {
196435
const kyber = new MlKem768();
197436

198437
// Encapsulate using recipient's public key to generate a shared secret
199-
const { ciphertext, sharedSecret } = await kyber.encap(chatState.recipientPublicKey);
438+
// According to documentation, encap returns [ciphertext, sharedSecret]
439+
const [ciphertext, sharedSecret] = await kyber.encap(chatState.recipientPublicKey);
200440

201441
// Store shared secret
202442
chatState.sharedSecret = sharedSecret;
@@ -257,6 +497,7 @@ async function handleKeyExchange(message) {
257497

258498
// Perform key decapsulation
259499
const kyber = new MlKem768();
500+
// According to documentation, decap is async and returns sharedSecret
260501
const sharedSecret = await kyber.decap(ciphertext, chatState.keyPair.secretKey);
261502

262503
// Store shared secret
@@ -500,5 +741,14 @@ function base64ToArrayBuffer(base64) {
500741
return bytes.buffer;
501742
}
502743

503-
// Initialize the chat page
504-
document.addEventListener('DOMContentLoaded', initChatPage);
744+
// Initialize the chat page when the DOM is loaded
745+
initChatPage();
746+
747+
// Also initialize on spa-transition-end event for SPA router
748+
document.addEventListener('spa-transition-end', () => {
749+
console.log('SPA transition end event received, initializing chat page');
750+
initChatPage();
751+
});
752+
753+
// Debug: Log when the module is loaded
754+
console.log('Chat module loaded');

0 commit comments

Comments
 (0)