From b5f950c9a27a032594ae6318bc48f35bb96de292 Mon Sep 17 00:00:00 2001
From: fainashalts 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 {PromiseMessage log
validateStyles,
getSettings,
setSettings,
+ encryptWithPassphrase,
+ decryptWithPassphrase,
};
})();
@@ -1312,6 +1468,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"]);
@@ -1432,6 +1602,126 @@ 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)";
+ 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";
+ 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
+ errorMsg.style.display = "none";
+
+ 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
+ );
+ } catch (e) {
+ errorMsg.innerText = "Encryption failed: " + e.toString();
+ errorMsg.style.display = "block";
+ }
+ });
+ }
+
/**
* Parse and decrypt the export bundle.
* The `bundle` param is a JSON string of the encapsulated public
@@ -1585,6 +1875,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 79648fe..f6ab366 100644
--- a/export/index.test.js
+++ b/export/index.test.js
@@ -446,4 +446,101 @@ 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);
+ });
});
From 6af2ee2e1af319635ffc0b197b89d24e18ca555a Mon Sep 17 00:00:00 2001
From: Faina Shalts Message log
["deriveBits", "deriveKey"]
);
- // Derive AES-256 key using PBKDF2 (100,000 iterations, SHA-256)
+ // 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: 100000,
+ iterations: 600000,
hash: "SHA-256",
},
keyMaterial,
From 26665e7407b5a0b645257769b5162dd6ebd8871e Mon Sep 17 00:00:00 2001
From: fainashalts Message log
{
name: "PBKDF2",
salt: salt,
- iterations: 100000,
+ iterations: 600000,
hash: "SHA-256",
},
keyMaterial,
From e698e3791569dd138c8b5c126ee6ace34b78e2ae Mon Sep 17 00:00:00 2001
From: fainashalts Message log
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
@@ -1654,6 +1657,8 @@ Message log
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
diff --git a/export/index.test.js b/export/index.test.js
index f6ab366..c0d1df7 100644
--- a/export/index.test.js
+++ b/export/index.test.js
@@ -544,3 +544,272 @@ describe("TKHQ", () => {
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.");
+ });
+});
From 847504c4cd6cad566d7d4052c05aac6cc67e8af6 Mon Sep 17 00:00:00 2001
From: fainashalts Message log
return;
}
- // Hide error message
+ // Hide error message and disable button to prevent duplicate submissions
errorMsg.style.display = "none";
+ submitButton.disabled = true;
try {
// Encode mnemonic to Uint8Array
@@ -1722,9 +1723,12 @@ Message log
encryptedBase64,
requestId
);
+
+ // Keep button disabled after success (operation complete)
} catch (e) {
errorMsg.innerText = "Encryption failed: " + e.toString();
errorMsg.style.display = "block";
+ submitButton.disabled = false;
}
});
}
From 19c9c3c441d43811491232b23272737bce4ec376 Mon Sep 17 00:00:00 2001
From: fainashalts Message log
passphraseInput.placeholder = "Enter passphrase (min 8 characters)";
passphraseInput.required = true;
passphraseInput.setAttribute("aria-required", "true");
+ passphraseInput.setAttribute("autocomplete", "new-password");
passphraseInput.minLength = 8;
formDiv.appendChild(passphraseInput);
@@ -1659,6 +1660,7 @@ Message log
confirmInput.placeholder = "Confirm passphrase";
confirmInput.required = true;
confirmInput.setAttribute("aria-required", "true");
+ confirmInput.setAttribute("autocomplete", "new-password");
formDiv.appendChild(confirmInput);
// Create error message paragraph
From d7209f18299d96344f77b45d0381664df0c089c7 Mon Sep 17 00:00:00 2001
From: fainashalts Message log
});
// Create the passphrase form container
- const formDiv = document.createElement("div");
+ const formDiv = document.createElement("form");
formDiv.id = "passphrase-form-div";
// Create heading
@@ -1671,7 +1671,7 @@ Message log
// Create submit button
const submitButton = document.createElement("button");
- submitButton.type = "button";
+ submitButton.type = "submit";
submitButton.id = "encrypt-and-export";
submitButton.innerText = "Encrypt & Export";
formDiv.appendChild(submitButton);
@@ -1679,8 +1679,9 @@ Message log
// Append the form to the body
document.body.appendChild(formDiv);
- // Add click event listener to the submit button
- submitButton.addEventListener("click", async () => {
+ // Add submit event listener to the form
+ formDiv.addEventListener("submit", async (event) => {
+ event.preventDefault();
const passphrase = passphraseInput.value;
const confirmPassphrase = confirmInput.value;
diff --git a/export/index.test.js b/export/index.test.js
index c0d1df7..a493a27 100644
--- a/export/index.test.js
+++ b/export/index.test.js
@@ -556,7 +556,7 @@ describe("Passphrase Form Validation", () => {
// Helper to create the passphrase form elements (mimics displayPassphraseForm)
function createPassphraseForm() {
- const formDiv = document.createElement("div");
+ const formDiv = document.createElement("form");
formDiv.id = "passphrase-form-div";
const passphraseInput = document.createElement("input");
@@ -575,7 +575,7 @@ describe("Passphrase Form Validation", () => {
formDiv.appendChild(errorMsg);
const submitButton = document.createElement("button");
- submitButton.type = "button";
+ submitButton.type = "submit";
submitButton.id = "encrypt-and-export";
formDiv.appendChild(submitButton);
@@ -584,11 +584,18 @@ describe("Passphrase Form Validation", () => {
return { formDiv, passphraseInput, confirmInput, errorMsg, submitButton };
}
- // Helper to create click handler that mimics displayPassphraseForm logic
+ // Helper to submit form (triggers validation)
+ function submitForm(elements) {
+ const event = new dom.window.Event("submit", { bubbles: true, cancelable: true });
+ elements.formDiv.dispatchEvent(event);
+ }
+
+ // Helper to create submit handler that mimics displayPassphraseForm logic
function addValidationHandler(elements, mnemonic, onSuccess) {
- const { passphraseInput, confirmInput, errorMsg, submitButton } = elements;
+ const { formDiv, passphraseInput, confirmInput, errorMsg } = elements;
- submitButton.addEventListener("click", async () => {
+ formDiv.addEventListener("submit", async (event) => {
+ event.preventDefault();
const passphrase = passphraseInput.value;
const confirmPassphrase = confirmInput.value;
@@ -641,8 +648,8 @@ describe("Passphrase Form Validation", () => {
elements.passphraseInput.value = "short";
elements.confirmInput.value = "short";
- // Click submit
- elements.submitButton.click();
+ // Submit form
+ submitForm(elements);
// Error should be displayed
expect(elements.errorMsg.style.display).toBe("block");
@@ -659,7 +666,7 @@ describe("Passphrase Form Validation", () => {
elements.passphraseInput.value = "1234567";
elements.confirmInput.value = "1234567";
- elements.submitButton.click();
+ submitForm(elements);
expect(elements.errorMsg.style.display).toBe("block");
expect(elements.errorMsg.innerText).toBe(
@@ -679,7 +686,7 @@ describe("Passphrase Form Validation", () => {
elements.passphraseInput.value = "12345678";
elements.confirmInput.value = "12345678";
- elements.submitButton.click();
+ submitForm(elements);
// Allow async handler to complete
await new Promise((resolve) => setTimeout(resolve, 10));
@@ -696,7 +703,7 @@ describe("Passphrase Form Validation", () => {
elements.passphraseInput.value = "password123";
elements.confirmInput.value = "password456";
- elements.submitButton.click();
+ submitForm(elements);
expect(elements.errorMsg.style.display).toBe("block");
expect(elements.errorMsg.innerText).toBe("Passphrases do not match.");
@@ -710,7 +717,7 @@ describe("Passphrase Form Validation", () => {
elements.passphraseInput.value = "short";
elements.confirmInput.value = "diff";
- elements.submitButton.click();
+ submitForm(elements);
// Length error should take precedence
expect(elements.errorMsg.style.display).toBe("block");
@@ -726,13 +733,13 @@ describe("Passphrase Form Validation", () => {
// First trigger an error
elements.passphraseInput.value = "short";
elements.confirmInput.value = "short";
- elements.submitButton.click();
+ submitForm(elements);
expect(elements.errorMsg.style.display).toBe("block");
// Now enter valid passphrases
elements.passphraseInput.value = "validpassword123";
elements.confirmInput.value = "validpassword123";
- elements.submitButton.click();
+ submitForm(elements);
// Allow async handler to complete
await new Promise((resolve) => setTimeout(resolve, 10));
@@ -749,7 +756,7 @@ describe("Passphrase Form Validation", () => {
elements.passphraseInput.value = "short";
elements.confirmInput.value = "";
- elements.submitButton.click();
+ submitForm(elements);
// Should show length error, not mismatch
expect(elements.errorMsg.innerText).toBe(
@@ -770,7 +777,7 @@ describe("Passphrase Form Validation", () => {
elements.passphraseInput.value = specialPass;
elements.confirmInput.value = specialPass;
- elements.submitButton.click();
+ submitForm(elements);
await new Promise((resolve) => setTimeout(resolve, 10));
@@ -791,7 +798,7 @@ describe("Passphrase Form Validation", () => {
elements.passphraseInput.value = unicodePass;
elements.confirmInput.value = unicodePass;
- elements.submitButton.click();
+ submitForm(elements);
await new Promise((resolve) => setTimeout(resolve, 10));
@@ -807,7 +814,7 @@ describe("Passphrase Form Validation", () => {
elements.passphraseInput.value = "Password123";
elements.confirmInput.value = "password123";
- elements.submitButton.click();
+ submitForm(elements);
expect(elements.errorMsg.style.display).toBe("block");
expect(elements.errorMsg.innerText).toBe("Passphrases do not match.");
From abd94a4d036ed8bc80920ccf49d89d6286f735c2 Mon Sep 17 00:00:00 2001
From: fainashalts Message log
passphraseInput.minLength = 8;
formDiv.appendChild(passphraseInput);
+ // Create passphrase strength indicator
+ const strengthDiv = document.createElement("div");
+ strengthDiv.id = "passphrase-strength";
+
+ const strengthBar = document.createElement("div");
+ strengthBar.id = "passphrase-strength-bar";
+
+ const strengthFill = document.createElement("div");
+ strengthFill.id = "passphrase-strength-fill";
+ strengthBar.appendChild(strengthFill);
+ strengthDiv.appendChild(strengthBar);
+
+ const strengthText = document.createElement("span");
+ strengthText.id = "passphrase-strength-text";
+ strengthDiv.appendChild(strengthText);
+ formDiv.appendChild(strengthDiv);
+
+ /**
+ * Evaluate passphrase strength and return score (0-4) with feedback
+ * @param {string} passphrase
+ * @returns {{score: number, label: string, feedback: string}}
+ */
+ function evaluatePassphraseStrength(passphrase) {
+ if (!passphrase) {
+ return { score: 0, label: "", feedback: "" };
+ }
+
+ let score = 0;
+ const checks = {
+ length8: passphrase.length >= 8,
+ length12: passphrase.length >= 12,
+ length16: passphrase.length >= 16,
+ lowercase: /[a-z]/.test(passphrase),
+ uppercase: /[A-Z]/.test(passphrase),
+ numbers: /[0-9]/.test(passphrase),
+ special: /[^a-zA-Z0-9]/.test(passphrase),
+ };
+
+ // Base score from length
+ if (checks.length8) score += 1;
+ if (checks.length12) score += 1;
+ if (checks.length16) score += 1;
+
+ // Character variety
+ const varietyCount = [checks.lowercase, checks.uppercase, checks.numbers, checks.special].filter(Boolean).length;
+ if (varietyCount >= 2) score += 1;
+ if (varietyCount >= 3) score += 1;
+ if (varietyCount >= 4) score += 1;
+
+ // Cap at 4
+ score = Math.min(score, 4);
+
+ // Determine label and feedback
+ let label, feedback;
+ if (score <= 1) {
+ label = "Weak";
+ feedback = "Add more characters and mix letters, numbers, and symbols.";
+ } else if (score === 2) {
+ label = "Fair";
+ feedback = "Consider adding more length or character variety.";
+ } else if (score === 3) {
+ label = "Good";
+ feedback = "Good passphrase strength.";
+ } else {
+ label = "Strong";
+ feedback = "Excellent passphrase strength!";
+ }
+
+ return { score, label, feedback };
+ }
+
+ // Update strength indicator on input
+ passphraseInput.addEventListener("input", () => {
+ const { score, label, feedback } = evaluatePassphraseStrength(passphraseInput.value);
+
+ // Update fill width and color
+ const strengthClasses = ["strength-weak", "strength-fair", "strength-good", "strength-strong"];
+ strengthFill.className = "";
+
+ if (score === 0) {
+ strengthFill.style.width = "0%";
+ strengthText.textContent = "";
+ } else {
+ strengthFill.style.width = `${score * 25}%`;
+ strengthFill.classList.add(strengthClasses[Math.min(score - 1, 3)]);
+ strengthText.textContent = `${label} — ${feedback}`;
+ }
+ });
+
// Create confirmation input
const confirmLabel = document.createElement("label");
confirmLabel.setAttribute("for", "export-passphrase-confirm");
@@ -1720,6 +1833,14 @@ Message log
String.fromCharCode.apply(null, encryptedBytes)
);
+ // Clear passphrase fields for security before sending message
+ passphraseInput.value = "";
+ confirmInput.value = "";
+ // Reset strength indicator
+ strengthFill.style.width = "0%";
+ strengthFill.className = "";
+ strengthText.textContent = "";
+
// Send message up
TKHQ.sendMessageUp(
"ENCRYPTED_WALLET_EXPORT",
@@ -1729,6 +1850,14 @@ Message log
// Keep button disabled after success (operation complete)
} catch (e) {
+ // Clear passphrase fields for security
+ passphraseInput.value = "";
+ confirmInput.value = "";
+ // Reset strength indicator
+ strengthFill.style.width = "0%";
+ strengthFill.className = "";
+ strengthText.textContent = "";
+
errorMsg.innerText = "Encryption failed: " + e.toString();
errorMsg.style.display = "block";
submitButton.disabled = false;
From 2001fe04d40d8a362ad7c90a40e509f259c24deb Mon Sep 17 00:00:00 2001
From: fainashalts Message log
errorMsg.style.display = "none";
submitButton.disabled = true;
+ let mnemonicBytes;
+ let encryptedBytes;
try {
// Encode mnemonic to Uint8Array
const encoder = new TextEncoder();
- const mnemonicBytes = encoder.encode(mnemonic);
+ mnemonicBytes = encoder.encode(mnemonic);
// Encrypt with passphrase
- const encryptedBytes = await TKHQ.encryptWithPassphrase(
+ encryptedBytes = await TKHQ.encryptWithPassphrase(
mnemonicBytes,
passphrase
);
@@ -1841,6 +1843,13 @@ Message log
strengthFill.className = "";
strengthText.textContent = "";
+ // Zero out sensitive byte arrays (mutable, so we can wipe them)
+ for (let i = 0; i < mnemonicBytes.length; i++) mnemonicBytes[i] = 0;
+ for (let i = 0; i < encryptedBytes.length; i++) encryptedBytes[i] = 0;
+
+ // Clear the mnemonic closure reference (can't truly zero a JS string, but removes the easy reference)
+ mnemonic = "";
+
// Send message up
TKHQ.sendMessageUp(
"ENCRYPTED_WALLET_EXPORT",
@@ -1848,8 +1857,17 @@ Message log
requestId
);
- // Keep button disabled after success (operation complete)
+ // Remove the entire form from the DOM — no sensitive material should linger
+ formDiv.remove();
} catch (e) {
+ // Zero out sensitive byte arrays if they were created
+ if (mnemonicBytes) {
+ for (let i = 0; i < mnemonicBytes.length; i++) mnemonicBytes[i] = 0;
+ }
+ if (encryptedBytes) {
+ for (let i = 0; i < encryptedBytes.length; i++) encryptedBytes[i] = 0;
+ }
+
// Clear passphrase fields for security
passphraseInput.value = "";
confirmInput.value = "";
From 5533728b4e7bec708c73c069585b2f919d6b20dc Mon Sep 17 00:00:00 2001
From: fainashalts Message log
// Convert to base64
const encryptedBase64 = btoa(
- String.fromCharCode.apply(null, encryptedBytes)
+ Array.from(encryptedBytes, (b) => String.fromCharCode(b)).join("")
);
// Clear passphrase fields for security before sending message
diff --git a/export/index.test.js b/export/index.test.js
index a493a27..beb25fd 100644
--- a/export/index.test.js
+++ b/export/index.test.js
@@ -526,7 +526,7 @@ describe("TKHQ", () => {
const encrypted = await TKHQ.encryptWithPassphrase(mnemonicBytes, passphrase);
// Convert to base64 (as would be done in displayPassphraseForm)
- const encryptedBase64 = btoa(String.fromCharCode.apply(null, encrypted));
+ const encryptedBase64 = btoa(Array.from(encrypted, (b) => String.fromCharCode(b)).join(""));
expect(typeof encryptedBase64).toBe("string");
expect(encryptedBase64.length).toBeGreaterThan(0);
From 8773586d1c5d3ece98f679c5d83dc27ba852e0c4 Mon Sep 17 00:00:00 2001
From: fainashalts Message log
TKHQ.sendMessageUp("ERROR", e.toString(), event.data["requestId"]);
}
}
- if (event.data && event.data["type"] == "INJECT_WALLET_EXPORT_BUNDLE_ENCRYPTED") {
+ if (
+ event.data &&
+ event.data["type"] == "INJECT_WALLET_EXPORT_BUNDLE_ENCRYPTED"
+ ) {
TKHQ.logMessage(
`⬇️ Received message ${event.data["type"]}: ${event.data["value"]}, ${event.data["organizationId"]}`
);
@@ -1716,7 +1727,12 @@ Message log
if (checks.length16) score += 1;
// Character variety
- const varietyCount = [checks.lowercase, checks.uppercase, checks.numbers, checks.special].filter(Boolean).length;
+ const varietyCount = [
+ checks.lowercase,
+ checks.uppercase,
+ checks.numbers,
+ checks.special,
+ ].filter(Boolean).length;
if (varietyCount >= 2) score += 1;
if (varietyCount >= 3) score += 1;
if (varietyCount >= 4) score += 1;
@@ -1728,7 +1744,8 @@ Message log
let label, feedback;
if (score <= 1) {
label = "Weak";
- feedback = "Add more characters and mix letters, numbers, and symbols.";
+ feedback =
+ "Add more characters and mix letters, numbers, and symbols.";
} else if (score === 2) {
label = "Fair";
feedback = "Consider adding more length or character variety.";
@@ -1745,12 +1762,19 @@ Message log
// Update strength indicator on input
passphraseInput.addEventListener("input", () => {
- const { score, label, feedback } = evaluatePassphraseStrength(passphraseInput.value);
-
+ const { score, label, feedback } = evaluatePassphraseStrength(
+ passphraseInput.value
+ );
+
// Update fill width and color
- const strengthClasses = ["strength-weak", "strength-fair", "strength-good", "strength-strong"];
+ const strengthClasses = [
+ "strength-weak",
+ "strength-fair",
+ "strength-good",
+ "strength-strong",
+ ];
strengthFill.className = "";
-
+
if (score === 0) {
strengthFill.style.width = "0%";
strengthText.textContent = "";
@@ -1845,7 +1869,8 @@ Message log
// Zero out sensitive byte arrays (mutable, so we can wipe them)
for (let i = 0; i < mnemonicBytes.length; i++) mnemonicBytes[i] = 0;
- for (let i = 0; i < encryptedBytes.length; i++) encryptedBytes[i] = 0;
+ for (let i = 0; i < encryptedBytes.length; i++)
+ encryptedBytes[i] = 0;
// Clear the mnemonic closure reference (can't truly zero a JS string, but removes the easy reference)
mnemonic = "";
@@ -1862,10 +1887,12 @@ Message log
} catch (e) {
// Zero out sensitive byte arrays if they were created
if (mnemonicBytes) {
- for (let i = 0; i < mnemonicBytes.length; i++) mnemonicBytes[i] = 0;
+ for (let i = 0; i < mnemonicBytes.length; i++)
+ mnemonicBytes[i] = 0;
}
if (encryptedBytes) {
- for (let i = 0; i < encryptedBytes.length; i++) encryptedBytes[i] = 0;
+ for (let i = 0; i < encryptedBytes.length; i++)
+ encryptedBytes[i] = 0;
}
// Clear passphrase fields for security
diff --git a/export/index.test.js b/export/index.test.js
index beb25fd..dd13277 100644
--- a/export/index.test.js
+++ b/export/index.test.js
@@ -466,7 +466,8 @@ describe("TKHQ", () => {
});
it("decrypts data encrypted by encryptWithPassphrase correctly", async () => {
- const originalText = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
+ const originalText =
+ "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
const plaintext = new TextEncoder().encode(originalText);
const passphrase = "mySecurePassphrase!";
@@ -486,7 +487,10 @@ describe("TKHQ", () => {
const correctPassphrase = "correctPassphrase";
const wrongPassphrase = "wrongPassphrase";
- const encrypted = await TKHQ.encryptWithPassphrase(plaintext, correctPassphrase);
+ const encrypted = await TKHQ.encryptWithPassphrase(
+ plaintext,
+ correctPassphrase
+ );
// Attempting to decrypt with wrong passphrase should throw
await expect(
@@ -523,10 +527,15 @@ describe("TKHQ", () => {
const mnemonicBytes = new TextEncoder().encode(mnemonic);
// Encrypt
- const encrypted = await TKHQ.encryptWithPassphrase(mnemonicBytes, passphrase);
+ const encrypted = await TKHQ.encryptWithPassphrase(
+ mnemonicBytes,
+ passphrase
+ );
// Convert to base64 (as would be done in displayPassphraseForm)
- const encryptedBase64 = btoa(Array.from(encrypted, (b) => String.fromCharCode(b)).join(""));
+ const encryptedBase64 = btoa(
+ Array.from(encrypted, (b) => String.fromCharCode(b)).join("")
+ );
expect(typeof encryptedBase64).toBe("string");
expect(encryptedBase64.length).toBeGreaterThan(0);
@@ -538,7 +547,10 @@ describe("TKHQ", () => {
);
// Decrypt
- const decrypted = await TKHQ.decryptWithPassphrase(encryptedFromBase64, passphrase);
+ const decrypted = await TKHQ.decryptWithPassphrase(
+ encryptedFromBase64,
+ passphrase
+ );
const decryptedMnemonic = new TextDecoder().decode(decrypted);
expect(decryptedMnemonic).toBe(mnemonic);
@@ -586,7 +598,10 @@ describe("Passphrase Form Validation", () => {
// Helper to submit form (triggers validation)
function submitForm(elements) {
- const event = new dom.window.Event("submit", { bubbles: true, cancelable: true });
+ const event = new dom.window.Event("submit", {
+ bubbles: true,
+ cancelable: true,
+ });
elements.formDiv.dispatchEvent(event);
}
diff --git a/export/package-lock.json b/export/package-lock.json
index e34d4fe..782df70 100644
--- a/export/package-lock.json
+++ b/export/package-lock.json
@@ -72,7 +72,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz",
"integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==",
"dev": true,
- "peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.22.13",
@@ -2080,7 +2079,6 @@
"resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz",
"integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==",
"dev": true,
- "peer": true,
"dependencies": {
"@jest/environment": "^29.7.0",
"@jest/expect": "^29.7.0",
@@ -3085,7 +3083,6 @@
"url": "https://github.com/sponsors/ai"
}
],
- "peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001541",
"electron-to-chromium": "^1.4.535",
@@ -5219,7 +5216,6 @@
"resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
"dev": true,
- "peer": true,
"dependencies": {
"@jest/core": "^29.7.0",
"@jest/types": "^29.6.3",
@@ -8380,7 +8376,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz",
"integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==",
"dev": true,
- "peer": true,
"requires": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.22.13",
@@ -9765,7 +9760,6 @@
"resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz",
"integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==",
"dev": true,
- "peer": true,
"requires": {
"@jest/environment": "^29.7.0",
"@jest/expect": "^29.7.0",
@@ -10539,7 +10533,6 @@
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz",
"integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==",
"dev": true,
- "peer": true,
"requires": {
"caniuse-lite": "^1.0.30001541",
"electron-to-chromium": "^1.4.535",
@@ -12036,7 +12029,6 @@
"resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
"dev": true,
- "peer": true,
"requires": {
"@jest/core": "^29.7.0",
"@jest/types": "^29.6.3",
diff --git a/import/package-lock.json b/import/package-lock.json
index 6851efa..a8ad0f5 100644
--- a/import/package-lock.json
+++ b/import/package-lock.json
@@ -84,7 +84,6 @@
"integrity": "sha512-97z/ju/Jy1rZmDxybphrBuI+jtJjFVoz7Mr9yUQVVVi+DNZE333uFQeMOqcCIy1x3WYBIbWftUSLmbNXNT7qFQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.22.13",
@@ -2014,7 +2013,6 @@
"version": "29.7.0",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@jest/environment": "^29.7.0",
"@jest/expect": "^29.7.0",
@@ -3142,7 +3140,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -3207,7 +3204,6 @@
"integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
@@ -3793,7 +3789,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.25",
"caniuse-lite": "^1.0.30001754",
@@ -7198,7 +7193,6 @@
"version": "29.7.0",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@jest/core": "^29.7.0",
"@jest/types": "^29.6.3",
@@ -9118,7 +9112,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -10837,8 +10830,7 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
- "license": "0BSD",
- "peer": true
+ "license": "0BSD"
},
"node_modules/type-check": {
"version": "0.4.0",
@@ -11118,7 +11110,6 @@
"integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.8",
@@ -11168,7 +11159,6 @@
"integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@discoveryjs/json-ext": "^0.5.0",
"@webpack-cli/configtest": "^2.1.1",
From 8b7ad318824c63c3e8335f2cabea60d06bf04ddf Mon Sep 17 00:00:00 2001
From: Ethan Konkolowicz Message log
+ Enter a passphrase to encrypt your export before displaying it. +
+ + +