diff --git a/export/index.template.html b/export/index.template.html
index 98ad026..ba4fd46 100644
--- a/export/index.template.html
+++ b/export/index.template.html
@@ -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;
+ }
@@ -831,6 +889,104 @@
Message log
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} - 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} - 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 @@ Message log
validateStyles,
getSettings,
setSettings,
+ encryptWithPassphrase,
+ decryptWithPassphrase,
};
})();
@@ -949,6 +1107,20 @@ Message log
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 @@ Message log
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)
+ );
+
+ // Send message up
+ TKHQ.sendMessageUp(
+ "ENCRYPTED_WALLET_EXPORT",
+ encryptedBase64,
+ requestId
+ );
+
+ // Keep button disabled after success (operation complete)
+ } catch (e) {
+ errorMsg.innerText = "Encryption failed: " + e.toString();
+ errorMsg.style.display = "block";
+ submitButton.disabled = false;
+ }
+ });
+ }
+
/**
* Parse and decrypt the export bundle.
* The `bundle` param is a JSON string of the encapsulated public
@@ -1222,6 +1523,30 @@ Message log
TKHQ.sendMessageUp("BUNDLE_INJECTED", true, requestId);
}
+ /**
+ * Function triggered when INJECT_WALLET_EXPORT_BUNDLE_ENCRYPTED event is received.
+ * @param {string} bundle
+ * @param {string} organizationId
+ * @param {string} requestId
+ */
+ async function onInjectWalletBundleEncrypted(
+ bundle,
+ organizationId,
+ requestId
+ ) {
+ // Decrypt the export bundle
+ const walletBytes = await decryptBundle(bundle, organizationId);
+
+ // Reset embedded key after using for decryption
+ TKHQ.onResetEmbeddedKey();
+
+ // Parse the decrypted wallet bytes
+ const wallet = TKHQ.encodeWallet(new Uint8Array(walletBytes));
+
+ // Display passphrase form instead of showing the key directly
+ displayPassphraseForm(wallet.mnemonic, requestId);
+ }
+
/**
* Function triggered when APPLY_SETTINGS event is received.
* For now, the only settings that can be applied are for "styles".
diff --git a/export/index.test.js b/export/index.test.js
index abe9230..8c1fa81 100644
--- a/export/index.test.js
+++ b/export/index.test.js
@@ -385,4 +385,370 @@ describe("TKHQ", () => {
};
expect(TKHQ.validateStyles(allStylesValid)).toEqual(allStylesValid);
});
+
+ it("encrypts data with passphrase correctly", async () => {
+ const plaintext = new TextEncoder().encode("test mnemonic phrase");
+ const passphrase = "securepassword123";
+
+ const encrypted = await TKHQ.encryptWithPassphrase(plaintext, passphrase);
+
+ // Result should be salt (16) + iv (12) + ciphertext (at least as long as plaintext + auth tag)
+ // Note: Using ArrayBuffer check due to JSDOM cross-realm Uint8Array difference
+ expect(encrypted.buffer).toBeDefined();
+ expect(encrypted.length).toBeGreaterThanOrEqual(16 + 12 + plaintext.length);
+
+ // Salt and IV should be present at the beginning
+ const salt = encrypted.slice(0, 16);
+ const iv = encrypted.slice(16, 28);
+ expect(salt.length).toBe(16);
+ expect(iv.length).toBe(12);
+ });
+
+ it("decrypts data encrypted by encryptWithPassphrase correctly", async () => {
+ const originalText = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
+ const plaintext = new TextEncoder().encode(originalText);
+ const passphrase = "mySecurePassphrase!";
+
+ // Encrypt
+ const encrypted = await TKHQ.encryptWithPassphrase(plaintext, passphrase);
+
+ // Decrypt
+ const decrypted = await TKHQ.decryptWithPassphrase(encrypted, passphrase);
+
+ // Verify
+ const decryptedText = new TextDecoder().decode(decrypted);
+ expect(decryptedText).toBe(originalText);
+ });
+
+ it("fails to decrypt with wrong passphrase", async () => {
+ const plaintext = new TextEncoder().encode("secret data");
+ const correctPassphrase = "correctPassphrase";
+ const wrongPassphrase = "wrongPassphrase";
+
+ const encrypted = await TKHQ.encryptWithPassphrase(plaintext, correctPassphrase);
+
+ // Attempting to decrypt with wrong passphrase should throw
+ await expect(
+ TKHQ.decryptWithPassphrase(encrypted, wrongPassphrase)
+ ).rejects.toThrow();
+ });
+
+ it("produces different ciphertext for same plaintext (due to random salt/IV)", async () => {
+ const plaintext = new TextEncoder().encode("same plaintext");
+ const passphrase = "samePassphrase";
+
+ const encrypted1 = await TKHQ.encryptWithPassphrase(plaintext, passphrase);
+ const encrypted2 = await TKHQ.encryptWithPassphrase(plaintext, passphrase);
+
+ // Encrypted results should be different due to random salt and IV
+ expect(TKHQ.uint8arrayToHexString(encrypted1)).not.toBe(
+ TKHQ.uint8arrayToHexString(encrypted2)
+ );
+
+ // But both should decrypt to the same plaintext
+ const decrypted1 = await TKHQ.decryptWithPassphrase(encrypted1, passphrase);
+ const decrypted2 = await TKHQ.decryptWithPassphrase(encrypted2, passphrase);
+
+ expect(new TextDecoder().decode(decrypted1)).toBe("same plaintext");
+ expect(new TextDecoder().decode(decrypted2)).toBe("same plaintext");
+ });
+
+ it("handles encryption of wallet mnemonic end-to-end", async () => {
+ const mnemonic =
+ "suffer surround soup duck goose patrol add unveil appear eye neglect hurry alpha project tomorrow embody hen wish twenty join notable amused burden treat";
+ const passphrase = "strongPassphrase123!";
+
+ // Encode mnemonic to bytes
+ const mnemonicBytes = new TextEncoder().encode(mnemonic);
+
+ // Encrypt
+ const encrypted = await TKHQ.encryptWithPassphrase(mnemonicBytes, passphrase);
+
+ // Convert to base64 (as would be done in displayPassphraseForm)
+ const encryptedBase64 = btoa(String.fromCharCode.apply(null, encrypted));
+ expect(typeof encryptedBase64).toBe("string");
+ expect(encryptedBase64.length).toBeGreaterThan(0);
+
+ // Convert back from base64
+ const encryptedFromBase64 = new Uint8Array(
+ atob(encryptedBase64)
+ .split("")
+ .map((c) => c.charCodeAt(0))
+ );
+
+ // Decrypt
+ const decrypted = await TKHQ.decryptWithPassphrase(encryptedFromBase64, passphrase);
+ const decryptedMnemonic = new TextDecoder().decode(decrypted);
+
+ expect(decryptedMnemonic).toBe(mnemonic);
+ });
+});
+
+/**
+ * Tests for passphrase form validation
+ * These tests create the form elements manually and test the validation logic
+ */
+describe("Passphrase Form Validation", () => {
+ let dom;
+ let document;
+ let TKHQ;
+
+ // Helper to create the passphrase form elements (mimics displayPassphraseForm)
+ function createPassphraseForm() {
+ const formDiv = document.createElement("div");
+ formDiv.id = "passphrase-form-div";
+
+ const passphraseInput = document.createElement("input");
+ passphraseInput.type = "password";
+ passphraseInput.id = "export-passphrase";
+ formDiv.appendChild(passphraseInput);
+
+ const confirmInput = document.createElement("input");
+ confirmInput.type = "password";
+ confirmInput.id = "export-passphrase-confirm";
+ formDiv.appendChild(confirmInput);
+
+ const errorMsg = document.createElement("p");
+ errorMsg.id = "passphrase-error";
+ errorMsg.style.display = "none";
+ formDiv.appendChild(errorMsg);
+
+ const submitButton = document.createElement("button");
+ submitButton.type = "button";
+ submitButton.id = "encrypt-and-export";
+ formDiv.appendChild(submitButton);
+
+ document.body.appendChild(formDiv);
+
+ return { formDiv, passphraseInput, confirmInput, errorMsg, submitButton };
+ }
+
+ // Helper to create click handler that mimics displayPassphraseForm logic
+ function addValidationHandler(elements, mnemonic, onSuccess) {
+ const { passphraseInput, confirmInput, errorMsg, submitButton } = elements;
+
+ 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
+ errorMsg.style.display = "none";
+
+ if (onSuccess) {
+ await onSuccess(passphrase);
+ }
+ });
+ }
+
+ beforeEach(() => {
+ dom = new JSDOM(html, {
+ runScripts: "dangerously",
+ url: "http://localhost",
+ beforeParse(window) {
+ window.TextDecoder = TextDecoder;
+ window.TextEncoder = TextEncoder;
+ },
+ });
+
+ Object.defineProperty(dom.window, "crypto", {
+ value: crypto.webcrypto,
+ });
+
+ document = dom.window.document;
+ TKHQ = dom.window.TKHQ;
+ });
+
+ it("shows error when passphrase is too short", () => {
+ const elements = createPassphraseForm();
+ addValidationHandler(elements, "test mnemonic");
+
+ // Set a passphrase that's too short (< 8 chars)
+ elements.passphraseInput.value = "short";
+ elements.confirmInput.value = "short";
+
+ // Click submit
+ elements.submitButton.click();
+
+ // Error should be displayed
+ expect(elements.errorMsg.style.display).toBe("block");
+ expect(elements.errorMsg.innerText).toBe(
+ "Passphrase must be at least 8 characters long."
+ );
+ });
+
+ it("shows error when passphrase is exactly 7 characters", () => {
+ const elements = createPassphraseForm();
+ addValidationHandler(elements, "test mnemonic");
+
+ // Set a passphrase that's exactly 7 chars (boundary case)
+ elements.passphraseInput.value = "1234567";
+ elements.confirmInput.value = "1234567";
+
+ elements.submitButton.click();
+
+ expect(elements.errorMsg.style.display).toBe("block");
+ expect(elements.errorMsg.innerText).toBe(
+ "Passphrase must be at least 8 characters long."
+ );
+ });
+
+ it("accepts passphrase with exactly 8 characters", async () => {
+ const elements = createPassphraseForm();
+ let successCalled = false;
+
+ addValidationHandler(elements, "test mnemonic", async () => {
+ successCalled = true;
+ });
+
+ // Set a passphrase that's exactly 8 chars (boundary case - should pass)
+ elements.passphraseInput.value = "12345678";
+ elements.confirmInput.value = "12345678";
+
+ elements.submitButton.click();
+
+ // Allow async handler to complete
+ await new Promise((resolve) => setTimeout(resolve, 10));
+
+ expect(elements.errorMsg.style.display).toBe("none");
+ expect(successCalled).toBe(true);
+ });
+
+ it("shows error when passphrases do not match", () => {
+ const elements = createPassphraseForm();
+ addValidationHandler(elements, "test mnemonic");
+
+ // Set mismatched passphrases (both >= 8 chars)
+ elements.passphraseInput.value = "password123";
+ elements.confirmInput.value = "password456";
+
+ elements.submitButton.click();
+
+ expect(elements.errorMsg.style.display).toBe("block");
+ expect(elements.errorMsg.innerText).toBe("Passphrases do not match.");
+ });
+
+ it("shows length error before mismatch error", () => {
+ const elements = createPassphraseForm();
+ addValidationHandler(elements, "test mnemonic");
+
+ // Set short AND mismatched passphrases
+ elements.passphraseInput.value = "short";
+ elements.confirmInput.value = "diff";
+
+ elements.submitButton.click();
+
+ // Length error should take precedence
+ expect(elements.errorMsg.style.display).toBe("block");
+ expect(elements.errorMsg.innerText).toBe(
+ "Passphrase must be at least 8 characters long."
+ );
+ });
+
+ it("hides error message on successful validation", async () => {
+ const elements = createPassphraseForm();
+ addValidationHandler(elements, "test mnemonic", async () => {});
+
+ // First trigger an error
+ elements.passphraseInput.value = "short";
+ elements.confirmInput.value = "short";
+ elements.submitButton.click();
+ expect(elements.errorMsg.style.display).toBe("block");
+
+ // Now enter valid passphrases
+ elements.passphraseInput.value = "validpassword123";
+ elements.confirmInput.value = "validpassword123";
+ elements.submitButton.click();
+
+ // Allow async handler to complete
+ await new Promise((resolve) => setTimeout(resolve, 10));
+
+ // Error should be hidden
+ expect(elements.errorMsg.style.display).toBe("none");
+ });
+
+ it("accepts empty confirmation when passphrase is too short (length check first)", () => {
+ const elements = createPassphraseForm();
+ addValidationHandler(elements, "test mnemonic");
+
+ // Short passphrase with empty confirmation
+ elements.passphraseInput.value = "short";
+ elements.confirmInput.value = "";
+
+ elements.submitButton.click();
+
+ // Should show length error, not mismatch
+ expect(elements.errorMsg.innerText).toBe(
+ "Passphrase must be at least 8 characters long."
+ );
+ });
+
+ it("validates with special characters in passphrase", async () => {
+ const elements = createPassphraseForm();
+ let receivedPassphrase = null;
+
+ addValidationHandler(elements, "test mnemonic", async (passphrase) => {
+ receivedPassphrase = passphrase;
+ });
+
+ // Passphrase with special characters
+ const specialPass = "p@$$w0rd!#%^&*()";
+ elements.passphraseInput.value = specialPass;
+ elements.confirmInput.value = specialPass;
+
+ elements.submitButton.click();
+
+ await new Promise((resolve) => setTimeout(resolve, 10));
+
+ expect(elements.errorMsg.style.display).toBe("none");
+ expect(receivedPassphrase).toBe(specialPass);
+ });
+
+ it("validates with unicode characters in passphrase", async () => {
+ const elements = createPassphraseForm();
+ let receivedPassphrase = null;
+
+ addValidationHandler(elements, "test mnemonic", async (passphrase) => {
+ receivedPassphrase = passphrase;
+ });
+
+ // Passphrase with unicode
+ const unicodePass = "密码🔐secure";
+ elements.passphraseInput.value = unicodePass;
+ elements.confirmInput.value = unicodePass;
+
+ elements.submitButton.click();
+
+ await new Promise((resolve) => setTimeout(resolve, 10));
+
+ expect(elements.errorMsg.style.display).toBe("none");
+ expect(receivedPassphrase).toBe(unicodePass);
+ });
+
+ it("is case-sensitive when comparing passphrases", () => {
+ const elements = createPassphraseForm();
+ addValidationHandler(elements, "test mnemonic");
+
+ // Same passphrase but different case
+ elements.passphraseInput.value = "Password123";
+ elements.confirmInput.value = "password123";
+
+ elements.submitButton.click();
+
+ expect(elements.errorMsg.style.display).toBe("block");
+ expect(elements.errorMsg.innerText).toBe("Passphrases do not match.");
+ });
});