-
Notifications
You must be signed in to change notification settings - Fork 2
Add passphrase-encrypted wallet export flow #110
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
8124bc0
12d670a
8aa6e03
0d60a1d
24735d8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -75,6 +75,64 @@ | |||||||||||||||||||||||||||
| .hidden { | ||||||||||||||||||||||||||||
| display: none; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| #passphrase-form-div { | ||||||||||||||||||||||||||||
| text-align: center; | ||||||||||||||||||||||||||||
| max-width: 500px; | ||||||||||||||||||||||||||||
| margin: 2em auto; | ||||||||||||||||||||||||||||
| padding: 1.5em; | ||||||||||||||||||||||||||||
| border: 1px solid #ddd; | ||||||||||||||||||||||||||||
| border-radius: 8px; | ||||||||||||||||||||||||||||
| background-color: #fafafa; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| #passphrase-form-div h2 { | ||||||||||||||||||||||||||||
| margin-top: 0; | ||||||||||||||||||||||||||||
| color: #333; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| #passphrase-form-div p { | ||||||||||||||||||||||||||||
| color: #555; | ||||||||||||||||||||||||||||
| font-size: 0.9em; | ||||||||||||||||||||||||||||
| margin-bottom: 1.5em; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| #passphrase-form-div label { | ||||||||||||||||||||||||||||
| display: block; | ||||||||||||||||||||||||||||
| text-align: left; | ||||||||||||||||||||||||||||
| margin: 0.5em 0 0.25em 0; | ||||||||||||||||||||||||||||
| font-weight: bold; | ||||||||||||||||||||||||||||
| color: #444; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| #passphrase-form-div input[type="password"] { | ||||||||||||||||||||||||||||
| width: 100%; | ||||||||||||||||||||||||||||
| padding: 0.6em; | ||||||||||||||||||||||||||||
| font-size: 1em; | ||||||||||||||||||||||||||||
| border: 1px solid #ccc; | ||||||||||||||||||||||||||||
| border-radius: 4px; | ||||||||||||||||||||||||||||
| box-sizing: border-box; | ||||||||||||||||||||||||||||
| margin-bottom: 0.5em; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| #passphrase-form-div input[type="password"]:focus { | ||||||||||||||||||||||||||||
| outline: none; | ||||||||||||||||||||||||||||
| border-color: #666; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| #encrypt-and-export { | ||||||||||||||||||||||||||||
| color: white; | ||||||||||||||||||||||||||||
| width: 100%; | ||||||||||||||||||||||||||||
| font-size: 1em; | ||||||||||||||||||||||||||||
| padding: 0.75em; | ||||||||||||||||||||||||||||
| margin-top: 1em; | ||||||||||||||||||||||||||||
| border-radius: 4px; | ||||||||||||||||||||||||||||
| background-color: rgb(50, 44, 44); | ||||||||||||||||||||||||||||
| border: 1px rgb(33, 33, 33) solid; | ||||||||||||||||||||||||||||
| cursor: pointer; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| #encrypt-and-export:hover { | ||||||||||||||||||||||||||||
| background-color: rgb(70, 64, 64); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| #passphrase-error { | ||||||||||||||||||||||||||||
| color: #c0392b; | ||||||||||||||||||||||||||||
| font-size: 0.9em; | ||||||||||||||||||||||||||||
| margin: 0.5em 0; | ||||||||||||||||||||||||||||
| text-align: left; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| </style> | ||||||||||||||||||||||||||||
| </head> | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
@@ -831,6 +889,104 @@ <h2>Message log</h2> | |||||||||||||||||||||||||||
| return JSON.stringify(validSettings); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||
| * Encrypts a Uint8Array using PBKDF2 key derivation and AES-GCM-256 encryption. | ||||||||||||||||||||||||||||
| * @param {Uint8Array} buf - The data to encrypt | ||||||||||||||||||||||||||||
| * @param {string} passphrase - The passphrase to derive the key from | ||||||||||||||||||||||||||||
| * @returns {Promise<Uint8Array>} - Concatenated salt || iv || ciphertext | ||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||
| async function encryptWithPassphrase(buf, passphrase) { | ||||||||||||||||||||||||||||
| // Generate random 16-byte salt and 12-byte IV | ||||||||||||||||||||||||||||
| const salt = crypto.getRandomValues(new Uint8Array(16)); | ||||||||||||||||||||||||||||
| const iv = crypto.getRandomValues(new Uint8Array(12)); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Import passphrase as PBKDF2 key material | ||||||||||||||||||||||||||||
| const keyMaterial = await crypto.subtle.importKey( | ||||||||||||||||||||||||||||
| "raw", | ||||||||||||||||||||||||||||
| new TextEncoder().encode(passphrase), | ||||||||||||||||||||||||||||
| "PBKDF2", | ||||||||||||||||||||||||||||
| false, | ||||||||||||||||||||||||||||
| ["deriveBits", "deriveKey"] | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Derive AES-256 key using PBKDF2 (600,000 iterations, SHA-256). | ||||||||||||||||||||||||||||
| // NOTE: The iteration count must match during decryption; changing it | ||||||||||||||||||||||||||||
| // affects compatibility with data encrypted using a different value. | ||||||||||||||||||||||||||||
| const aesKey = await crypto.subtle.deriveKey( | ||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||
| name: "PBKDF2", | ||||||||||||||||||||||||||||
| salt: salt, | ||||||||||||||||||||||||||||
| iterations: 600000, | ||||||||||||||||||||||||||||
| hash: "SHA-256", | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| keyMaterial, | ||||||||||||||||||||||||||||
| { name: "AES-GCM", length: 256 }, | ||||||||||||||||||||||||||||
| false, | ||||||||||||||||||||||||||||
| ["encrypt"] | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Encrypt using AES-GCM | ||||||||||||||||||||||||||||
| const ciphertext = await crypto.subtle.encrypt( | ||||||||||||||||||||||||||||
| { name: "AES-GCM", iv: iv }, | ||||||||||||||||||||||||||||
| aesKey, | ||||||||||||||||||||||||||||
| buf | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Return concatenated salt || iv || ciphertext | ||||||||||||||||||||||||||||
| const result = new Uint8Array( | ||||||||||||||||||||||||||||
| salt.length + iv.length + ciphertext.byteLength | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
| result.set(salt, 0); | ||||||||||||||||||||||||||||
| result.set(iv, salt.length); | ||||||||||||||||||||||||||||
| result.set(new Uint8Array(ciphertext), salt.length + iv.length); | ||||||||||||||||||||||||||||
| return result; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||
| * Decrypts a buffer encrypted by encryptWithPassphrase. | ||||||||||||||||||||||||||||
| * @param {Uint8Array} encryptedBuf - The encrypted data (salt || iv || ciphertext) | ||||||||||||||||||||||||||||
| * @param {string} passphrase - The passphrase to derive the key from | ||||||||||||||||||||||||||||
| * @returns {Promise<Uint8Array>} - The decrypted data | ||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||
| async function decryptWithPassphrase(encryptedBuf, passphrase) { | ||||||||||||||||||||||||||||
| // Extract salt (bytes 0-16), iv (bytes 16-28), ciphertext (bytes 28+) | ||||||||||||||||||||||||||||
| const salt = encryptedBuf.slice(0, 16); | ||||||||||||||||||||||||||||
| const iv = encryptedBuf.slice(16, 28); | ||||||||||||||||||||||||||||
| const ciphertext = encryptedBuf.slice(28); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Import passphrase as PBKDF2 key material | ||||||||||||||||||||||||||||
| const keyMaterial = await crypto.subtle.importKey( | ||||||||||||||||||||||||||||
| "raw", | ||||||||||||||||||||||||||||
| new TextEncoder().encode(passphrase), | ||||||||||||||||||||||||||||
| "PBKDF2", | ||||||||||||||||||||||||||||
| false, | ||||||||||||||||||||||||||||
| ["deriveBits", "deriveKey"] | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Derive same AES key using PBKDF2 | ||||||||||||||||||||||||||||
| const aesKey = await crypto.subtle.deriveKey( | ||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||
| name: "PBKDF2", | ||||||||||||||||||||||||||||
| salt: salt, | ||||||||||||||||||||||||||||
| iterations: 600000, | ||||||||||||||||||||||||||||
| hash: "SHA-256", | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| keyMaterial, | ||||||||||||||||||||||||||||
| { name: "AES-GCM", length: 256 }, | ||||||||||||||||||||||||||||
| false, | ||||||||||||||||||||||||||||
| ["decrypt"] | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Decrypt using AES-GCM | ||||||||||||||||||||||||||||
| const decrypted = await crypto.subtle.decrypt( | ||||||||||||||||||||||||||||
| { name: "AES-GCM", iv: iv }, | ||||||||||||||||||||||||||||
| aesKey, | ||||||||||||||||||||||||||||
| ciphertext | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| return new Uint8Array(decrypted); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||
| initEmbeddedKey, | ||||||||||||||||||||||||||||
| generateTargetKey, | ||||||||||||||||||||||||||||
|
|
@@ -858,6 +1014,8 @@ <h2>Message log</h2> | |||||||||||||||||||||||||||
| validateStyles, | ||||||||||||||||||||||||||||
| getSettings, | ||||||||||||||||||||||||||||
| setSettings, | ||||||||||||||||||||||||||||
| encryptWithPassphrase, | ||||||||||||||||||||||||||||
| decryptWithPassphrase, | ||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
| })(); | ||||||||||||||||||||||||||||
| </script> | ||||||||||||||||||||||||||||
|
|
@@ -949,6 +1107,20 @@ <h2>Message log</h2> | |||||||||||||||||||||||||||
| TKHQ.sendMessageUp("ERROR", e.toString(), event.data["requestId"]); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| if (event.data && event.data["type"] == "INJECT_WALLET_EXPORT_BUNDLE_ENCRYPTED") { | ||||||||||||||||||||||||||||
| TKHQ.logMessage( | ||||||||||||||||||||||||||||
| `⬇️ Received message ${event.data["type"]}: ${event.data["value"]}, ${event.data["organizationId"]}` | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||
| await onInjectWalletBundleEncrypted( | ||||||||||||||||||||||||||||
| event.data["value"], | ||||||||||||||||||||||||||||
| event.data["organizationId"], | ||||||||||||||||||||||||||||
| event.data["requestId"] | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||
| TKHQ.sendMessageUp("ERROR", e.toString(), event.data["requestId"]); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| if (event.data && event.data["type"] == "APPLY_SETTINGS") { | ||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||
| await onApplySettings(event.data["value"], event.data["requestId"]); | ||||||||||||||||||||||||||||
|
|
@@ -1069,6 +1241,135 @@ <h2>Message log</h2> | |||||||||||||||||||||||||||
| TKHQ.applySettings(TKHQ.getSettings()); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||
| * Display a passphrase form to encrypt the mnemonic before exporting. | ||||||||||||||||||||||||||||
| * @param {string} mnemonic - The wallet mnemonic to encrypt | ||||||||||||||||||||||||||||
| * @param {string} requestId - The request ID for message correlation | ||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||
| function displayPassphraseForm(mnemonic, requestId) { | ||||||||||||||||||||||||||||
| // Hide all existing DOM elements except scripts | ||||||||||||||||||||||||||||
| Array.from(document.body.children).forEach((child) => { | ||||||||||||||||||||||||||||
| if (child.tagName !== "SCRIPT") { | ||||||||||||||||||||||||||||
| child.style.display = "none"; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Create the passphrase form container | ||||||||||||||||||||||||||||
| const formDiv = document.createElement("div"); | ||||||||||||||||||||||||||||
| formDiv.id = "passphrase-form-div"; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Create heading | ||||||||||||||||||||||||||||
| const heading = document.createElement("h2"); | ||||||||||||||||||||||||||||
| heading.innerText = "Encrypt Your Wallet Export"; | ||||||||||||||||||||||||||||
| formDiv.appendChild(heading); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Create description | ||||||||||||||||||||||||||||
| const description = document.createElement("p"); | ||||||||||||||||||||||||||||
| description.innerText = | ||||||||||||||||||||||||||||
| "Enter a passphrase to encrypt your wallet mnemonic. You will need this passphrase to decrypt your wallet later."; | ||||||||||||||||||||||||||||
| formDiv.appendChild(description); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Create passphrase input | ||||||||||||||||||||||||||||
| const passphraseLabel = document.createElement("label"); | ||||||||||||||||||||||||||||
| passphraseLabel.setAttribute("for", "export-passphrase"); | ||||||||||||||||||||||||||||
| passphraseLabel.innerText = "Passphrase"; | ||||||||||||||||||||||||||||
| formDiv.appendChild(passphraseLabel); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const passphraseInput = document.createElement("input"); | ||||||||||||||||||||||||||||
| passphraseInput.type = "password"; | ||||||||||||||||||||||||||||
| passphraseInput.id = "export-passphrase"; | ||||||||||||||||||||||||||||
| passphraseInput.placeholder = "Enter passphrase (min 8 characters)"; | ||||||||||||||||||||||||||||
| passphraseInput.required = true; | ||||||||||||||||||||||||||||
| passphraseInput.setAttribute("aria-required", "true"); | ||||||||||||||||||||||||||||
| passphraseInput.minLength = 8; | ||||||||||||||||||||||||||||
| formDiv.appendChild(passphraseInput); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Create confirmation input | ||||||||||||||||||||||||||||
| const confirmLabel = document.createElement("label"); | ||||||||||||||||||||||||||||
| confirmLabel.setAttribute("for", "export-passphrase-confirm"); | ||||||||||||||||||||||||||||
| confirmLabel.innerText = "Confirm Passphrase"; | ||||||||||||||||||||||||||||
| formDiv.appendChild(confirmLabel); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const confirmInput = document.createElement("input"); | ||||||||||||||||||||||||||||
| confirmInput.type = "password"; | ||||||||||||||||||||||||||||
| confirmInput.id = "export-passphrase-confirm"; | ||||||||||||||||||||||||||||
| confirmInput.placeholder = "Confirm passphrase"; | ||||||||||||||||||||||||||||
| confirmInput.required = true; | ||||||||||||||||||||||||||||
| confirmInput.setAttribute("aria-required", "true"); | ||||||||||||||||||||||||||||
| formDiv.appendChild(confirmInput); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Create error message paragraph | ||||||||||||||||||||||||||||
| const errorMsg = document.createElement("p"); | ||||||||||||||||||||||||||||
| errorMsg.id = "passphrase-error"; | ||||||||||||||||||||||||||||
| errorMsg.style.display = "none"; | ||||||||||||||||||||||||||||
| formDiv.appendChild(errorMsg); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Create submit button | ||||||||||||||||||||||||||||
| const submitButton = document.createElement("button"); | ||||||||||||||||||||||||||||
| submitButton.type = "button"; | ||||||||||||||||||||||||||||
| submitButton.id = "encrypt-and-export"; | ||||||||||||||||||||||||||||
| submitButton.innerText = "Encrypt & Export"; | ||||||||||||||||||||||||||||
| formDiv.appendChild(submitButton); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Append the form to the body | ||||||||||||||||||||||||||||
| document.body.appendChild(formDiv); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Add click event listener to the submit button | ||||||||||||||||||||||||||||
| submitButton.addEventListener("click", async () => { | ||||||||||||||||||||||||||||
| const passphrase = passphraseInput.value; | ||||||||||||||||||||||||||||
| const confirmPassphrase = confirmInput.value; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Validate minimum passphrase length (8 characters) | ||||||||||||||||||||||||||||
| if (passphrase.length < 8) { | ||||||||||||||||||||||||||||
| errorMsg.innerText = | ||||||||||||||||||||||||||||
| "Passphrase must be at least 8 characters long."; | ||||||||||||||||||||||||||||
| errorMsg.style.display = "block"; | ||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Validate passphrases match | ||||||||||||||||||||||||||||
| if (passphrase !== confirmPassphrase) { | ||||||||||||||||||||||||||||
| errorMsg.innerText = "Passphrases do not match."; | ||||||||||||||||||||||||||||
| errorMsg.style.display = "block"; | ||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Hide error message and disable button to prevent duplicate submissions | ||||||||||||||||||||||||||||
| errorMsg.style.display = "none"; | ||||||||||||||||||||||||||||
| submitButton.disabled = true; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||
| // Encode mnemonic to Uint8Array | ||||||||||||||||||||||||||||
| const encoder = new TextEncoder(); | ||||||||||||||||||||||||||||
| const mnemonicBytes = encoder.encode(mnemonic); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Encrypt with passphrase | ||||||||||||||||||||||||||||
| const encryptedBytes = await TKHQ.encryptWithPassphrase( | ||||||||||||||||||||||||||||
| mnemonicBytes, | ||||||||||||||||||||||||||||
| passphrase | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Convert to base64 | ||||||||||||||||||||||||||||
| const encryptedBase64 = btoa( | ||||||||||||||||||||||||||||
| String.fromCharCode.apply(null, encryptedBytes) | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
|
Comment on lines
+1352
to
+1355
|
||||||||||||||||||||||||||||
| // Convert to base64 | |
| const encryptedBase64 = btoa( | |
| String.fromCharCode.apply(null, encryptedBytes) | |
| ); | |
| // Convert to base64 without risking call stack overflow | |
| let binary = ""; | |
| const CHUNK_SIZE = 0x8000; // 32,768 bytes per chunk | |
| for (let i = 0; i < encryptedBytes.length; i += CHUNK_SIZE) { | |
| const chunk = encryptedBytes.subarray(i, i + CHUNK_SIZE); | |
| binary += String.fromCharCode.apply(null, chunk); | |
| } | |
| const encryptedBase64 = btoa(binary); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we're only exporting wallets with this functionality. I think this suggestion is overkill.
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After encryption succeeds and the message is sent to the parent frame, the sensitive mnemonic remains in memory within the closure. Consider clearing the mnemonic variable and the form inputs after successful encryption to minimize the window of exposure for this sensitive data in memory.
For example, setting the passphrase input values to empty strings and potentially overwriting the mnemonic variable.
| ); | |
| ); | |
| // Clear sensitive data from memory after successful encryption | |
| passphraseInput.value = ""; | |
| confirmInput.value = ""; | |
| for (let i = 0; i < mnemonicBytes.length; i++) { | |
| mnemonicBytes[i] = 0; | |
| } | |
| for (let i = 0; i < encryptedBytes.length; i++) { | |
| encryptedBytes[i] = 0; | |
| } | |
| mnemonic = ""; |
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When an encryption error occurs, the passphrase values remain in the input fields, which could be a security concern if the iframe is still accessible. Consider clearing the passphrase input fields even when an error occurs to prevent potential exposure of the passphrase.
| } catch (e) { | |
| } catch (e) { | |
| // Clear sensitive passphrase inputs on error to avoid lingering secrets in the DOM | |
| passphraseInput.value = ""; | |
| confirmInput.value = ""; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The passphrase validation only checks for a minimum length of 8 characters but doesn't enforce any character composition requirements (e.g., mix of letters, numbers, special characters) or check for common weak passphrases. While this gives users flexibility, consider adding a warning or recommendation for stronger passphrases, especially since this protects sensitive wallet mnemonics.
Alternatively, you could provide visual feedback on passphrase strength to help users make informed decisions.