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
830const 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 */
3545function 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 */
64133async 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