1+
12document . addEventListener ( 'DOMContentLoaded' , ( ) => {
23 // TOAST
34 const toastContainer = document . createElement ( 'div' ) ;
@@ -20,19 +21,55 @@ document.addEventListener('DOMContentLoaded', () => {
2021 const countEl = document . getElementById ( 'entropy-count' ) ;
2122 const progressEl = document . getElementById ( 'entropy-progress' ) ;
2223 const entropy = [ ] ;
23- document . body . addEventListener ( 'mousemove' , function onMove ( e ) {
24- if ( entropy . length < 100 ) {
25- entropy . push ( e . clientX & 0xff , e . clientY & 0xff ) ;
26- const progress = Math . min ( entropy . length , 100 ) ;
27- countEl . textContent = progress ;
28- progressEl . style . width = `${ progress } %` ;
29- }
30- if ( entropy . length >= 100 ) {
31- document . body . removeEventListener ( 'mousemove' , onMove ) ;
32- overlay . style . display = 'none' ;
33- showToast ( 'Security initialization complete!' , 'success' ) ;
34- }
35- } ) ;
24+
25+ function completeEntropy ( ) {
26+ if ( countEl ) countEl . textContent = '100' ;
27+ if ( progressEl ) progressEl . style . width = '100%' ;
28+ if ( overlay ) overlay . style . display = 'none' ;
29+ showToast ( 'Security initialization complete!' , 'success' ) ;
30+ }
31+
32+ function simulateEntropy ( ) {
33+ // Feed random data until progress reaches 100, mirroring normal UI updates
34+ const step = ( ) => {
35+ if ( entropy . length < 100 ) {
36+ entropy . push ( ( Math . random ( ) * 256 ) | 0 , ( Math . random ( ) * 256 ) | 0 ) ;
37+ const progress = Math . min ( entropy . length , 100 ) ;
38+ if ( countEl ) countEl . textContent = progress ;
39+ if ( progressEl ) progressEl . style . width = `${ progress } %` ;
40+ requestAnimationFrame ( step ) ;
41+ } else {
42+ completeEntropy ( ) ;
43+ }
44+ } ;
45+ step ( ) ;
46+ }
47+
48+ const isCypress = typeof window !== 'undefined' && ! ! window . Cypress ;
49+ const shouldSimulate = / [ ? & ] s i m u l a t e E n t r o p y = 1 \b / . test ( location . search ) || ( isCypress && ! / [ ? & ] d i s a b l e E n t r o p y = 1 \b / . test ( location . search ) ) ;
50+ const shouldDisable = / [ ? & ] d i s a b l e E n t r o p y = 1 \b / . test ( location . search ) ;
51+
52+ if ( shouldDisable ) {
53+ // Explicitly disable overlay
54+ completeEntropy ( ) ;
55+ } else if ( shouldSimulate ) {
56+ // Simulate entropy for E2E or when requested via URL
57+ simulateEntropy ( ) ;
58+ } else {
59+ // Normal behavior: collect entropy from mouse movement
60+ document . body . addEventListener ( 'mousemove' , function onMove ( e ) {
61+ if ( entropy . length < 100 ) {
62+ entropy . push ( e . clientX & 0xff , e . clientY & 0xff ) ;
63+ const progress = Math . min ( entropy . length , 100 ) ;
64+ countEl . textContent = progress ;
65+ progressEl . style . width = `${ progress } %` ;
66+ }
67+ if ( entropy . length >= 100 ) {
68+ document . body . removeEventListener ( 'mousemove' , onMove ) ;
69+ completeEntropy ( ) ;
70+ }
71+ } ) ;
72+ }
3673
3774 // DARK MODE with shadcn/ui theming
3875 const darkToggle = document . getElementById ( 'toggle-dark' ) ;
@@ -208,34 +245,49 @@ document.addEventListener('DOMContentLoaded', () => {
208245
209246 // KEYBOARD SHORTCUTS
210247 function setupKeyboardShortcuts ( ) {
248+ function isEditableTarget ( el ) {
249+ if ( ! el ) return false ;
250+ const tag = ( el . tagName || '' ) . toLowerCase ( ) ;
251+ if ( tag === 'input' || tag === 'textarea' || tag === 'select' ) return true ;
252+ if ( el . isContentEditable ) return true ;
253+ // ARIA-compatible textboxes
254+ if ( el . getAttribute && el . getAttribute ( 'role' ) === 'textbox' ) return true ;
255+ return false ;
256+ }
257+
211258 document . addEventListener ( 'keydown' , ( e ) => {
259+ // Never intercept native shortcuts while typing
260+ if ( isEditableTarget ( e . target ) ) return ;
261+
212262 if ( e . ctrlKey || e . metaKey ) {
213- switch ( e . key . toLowerCase ( ) ) {
214- case 'g' :
215- e . preventDefault ( ) ;
216- location . hash = '#keygen' ;
217- document . getElementById ( 'gen-name' ) . focus ( ) ;
218- break ;
219- case 'e' :
220- e . preventDefault ( ) ;
221- location . hash = '#encrypt' ;
222- document . getElementById ( 'encrypt-message' ) . focus ( ) ;
223- break ;
224- case 'd' :
225- e . preventDefault ( ) ;
226- location . hash = '#decrypt' ;
227- document . getElementById ( 'decrypt-message' ) . focus ( ) ;
228- break ;
229- case 's' :
230- e . preventDefault ( ) ;
231- location . hash = '#sign' ;
232- document . getElementById ( 'sign-message' ) . focus ( ) ;
233- break ;
234- case 'v' :
235- e . preventDefault ( ) ;
236- location . hash = '#verify' ;
237- document . getElementById ( 'verify-message' ) . focus ( ) ;
238- break ;
263+ const key = e . key . toLowerCase ( ) ;
264+ // Note: Do NOT bind to 'v' (paste). Use Ctrl/Cmd+Shift+Y for Verify instead.
265+ if ( key === 'g' ) {
266+ e . preventDefault ( ) ;
267+ location . hash = '#keygen' ;
268+ const el = document . getElementById ( 'gen-name' ) ;
269+ if ( el ) el . focus ( ) ;
270+ } else if ( key === 'e' ) {
271+ e . preventDefault ( ) ;
272+ location . hash = '#encrypt' ;
273+ const el = document . getElementById ( 'encrypt-message' ) ;
274+ if ( el ) el . focus ( ) ;
275+ } else if ( key === 'd' ) {
276+ e . preventDefault ( ) ;
277+ location . hash = '#decrypt' ;
278+ const el = document . getElementById ( 'decrypt-message' ) ;
279+ if ( el ) el . focus ( ) ;
280+ } else if ( key === 's' ) {
281+ e . preventDefault ( ) ;
282+ location . hash = '#sign' ;
283+ const el = document . getElementById ( 'sign-message' ) ;
284+ if ( el ) el . focus ( ) ;
285+ } else if ( e . shiftKey && key === 'y' ) {
286+ // New mapping for Verify: Ctrl/Cmd+Shift+Y
287+ e . preventDefault ( ) ;
288+ location . hash = '#verify' ;
289+ const el = document . getElementById ( 'verify-message' ) ;
290+ if ( el ) el . focus ( ) ;
239291 }
240292 }
241293 } ) ;
@@ -386,8 +438,24 @@ document.addEventListener('DOMContentLoaded', () => {
386438 setupForm ( 'form-decrypt' , async ( ) => {
387439 const message = await openpgp . readMessage ( { armoredMessage : decryptMessage . value } ) ;
388440 const privateKey = await openpgp . readPrivateKey ( { armoredKey : decryptPrivkey . value } ) ;
389- const decryptedPrivateKey = await openpgp . decryptKey ( { privateKey, passphrase : decryptPassphrase . value || undefined } ) ;
390- const { data : decrypted } = await openpgp . decrypt ( { message, decryptionKeys : decryptedPrivateKey } ) ;
441+
442+ // Use the private key directly if no passphrase; otherwise decrypt with passphrase.
443+ let usablePrivateKey = privateKey ;
444+ if ( decryptPassphrase . value ) {
445+ try {
446+ usablePrivateKey = await openpgp . decryptKey ( {
447+ privateKey,
448+ passphrase : decryptPassphrase . value
449+ } ) ;
450+ } catch ( err ) {
451+ // If the key is already decrypted, proceed with the original key
452+ if ( ! String ( err && err . message || '' ) . includes ( 'already decrypted' ) ) {
453+ throw err ;
454+ }
455+ }
456+ }
457+
458+ const { data : decrypted } = await openpgp . decrypt ( { message, decryptionKeys : usablePrivateKey } ) ;
391459 outputDecrypt . textContent = decrypted ;
392460 showToast ( 'Message decrypted' , 'success' ) ;
393461 } ) ;
@@ -401,8 +469,22 @@ document.addEventListener('DOMContentLoaded', () => {
401469 setupForm ( 'form-sign' , async ( ) => {
402470 const message = await openpgp . createMessage ( { text : signMessage . value } ) ;
403471 const privateKey = await openpgp . readPrivateKey ( { armoredKey : signPrivkey . value } ) ;
404- const decryptedPrivateKey = await openpgp . decryptKey ( { privateKey, passphrase : signPassphrase . value || undefined } ) ;
405- const signature = await openpgp . sign ( { message, signingKeys : decryptedPrivateKey } ) ;
472+
473+ let signingKey = privateKey ;
474+ if ( signPassphrase . value ) {
475+ try {
476+ signingKey = await openpgp . decryptKey ( {
477+ privateKey,
478+ passphrase : signPassphrase . value
479+ } ) ;
480+ } catch ( err ) {
481+ if ( ! String ( err && err . message || '' ) . includes ( 'already decrypted' ) ) {
482+ throw err ;
483+ }
484+ }
485+ }
486+
487+ const signature = await openpgp . sign ( { message, signingKeys : signingKey } ) ;
406488 outputSign . textContent = signature ;
407489 downloadSignature . classList . remove ( 'hidden' ) ;
408490 downloadSignature . onclick = ( ) => downloadFile ( 'signature.asc' , signature ) ;
@@ -431,8 +513,25 @@ document.addEventListener('DOMContentLoaded', () => {
431513 const downloadRevocation = document . getElementById ( 'download-revocation' ) ;
432514 setupForm ( 'form-revoke' , async ( ) => {
433515 const privateKey = await openpgp . readPrivateKey ( { armoredKey : revokePrivkey . value } ) ;
434- const decryptedPrivateKey = await openpgp . decryptKey ( { privateKey, passphrase : revokePassphrase . value || undefined } ) ;
435- const revocationCertificate = await openpgp . revokeKey ( { key : decryptedPrivateKey , reasonForRevocation : { flag : + revokeReason . value , string : revokeDescription . value } } ) ;
516+
517+ let keyForRevocation = privateKey ;
518+ if ( revokePassphrase . value ) {
519+ try {
520+ keyForRevocation = await openpgp . decryptKey ( {
521+ privateKey,
522+ passphrase : revokePassphrase . value
523+ } ) ;
524+ } catch ( err ) {
525+ if ( ! String ( err && err . message || '' ) . includes ( 'already decrypted' ) ) {
526+ throw err ;
527+ }
528+ }
529+ }
530+
531+ const revocationCertificate = await openpgp . revokeKey ( {
532+ key : keyForRevocation ,
533+ reasonForRevocation : { flag : + revokeReason . value , string : revokeDescription . value }
534+ } ) ;
436535 outputRevoke . textContent = revocationCertificate ;
437536 downloadRevocation . classList . remove ( 'hidden' ) ;
438537 downloadRevocation . onclick = ( ) => downloadFile ( 'revocation.asc' , revocationCertificate ) ;
0 commit comments