Skip to content

Conversation

@fainashalts
Copy link

@fainashalts fainashalts commented Jan 6, 2026

Add passphrase-encrypted wallet export flow

Note: this PR was initially generated by Cursor using Claude Opus 4.5 based on the details provided in REQ-275. I have since made multiple edits and fixes. This is step 1, with additional work needed in the SDK and an example to be enumerated in mono.

Summary

This PR adds a new encrypted wallet export flow that allows users to encrypt their wallet mnemonic with a passphrase before it leaves the iframe. Instead of displaying the plaintext mnemonic in the DOM, users are prompted to enter and confirm a passphrase, and the encrypted result is sent to the parent frame as base64-encoded data.

Changes

New Encryption Utilities (TKHQ Module)

  • encryptWithPassphrase(buf, passphrase) - Encrypts a Uint8Array using:

    • PBKDF2 key derivation (100,000 iterations, SHA-256)
    • AES-GCM-256 encryption
    • Returns concatenated salt (16 bytes) || iv (12 bytes) || ciphertext
  • decryptWithPassphrase(encryptedBuf, passphrase) - Decrypts data encrypted by the above function

New Message Type

  • INJECT_WALLET_EXPORT_BUNDLE_ENCRYPTED - New message type that triggers the passphrase-protected export flow instead of displaying the mnemonic directly

New UI Components

  • displayPassphraseForm(mnemonic, requestId) - Renders a form with:
    • Password input field
    • Password confirmation field
    • Validation (minimum 8 characters, passwords must match)
    • Error message display
    • "Encrypt & Export" submit button

displayPassphraseForm(mnemonic, requestId) - Renders a <form> with:

  • Password input field with autocomplete="new-password" and required
  • Password confirmation field with autocomplete="new-password" and required
  • Passphrase strength indicator (Weak / Medium / Strong)
  • Validation (minimum 8 characters, passwords must match)
  • Error message display
  • "Encrypt & Export" submit button with state management (disabled during async operation)

New Output Message

  • ENCRYPTED_WALLET_EXPORT - Sent to parent frame with base64-encoded encrypted wallet data upon successful encryption

Styling

  • Added CSS styles for the passphrase form container, inputs, button, and error messages

Testing

Encryption tests (5):
✅ Encrypts data with passphrase correctly
✅ Decrypts data encrypted by encryptWithPassphrase correctly
✅ Fails to decrypt with wrong passphrase
✅ Produces different ciphertext for same plaintext (random salt/IV)
✅ Handles encryption of wallet mnemonic end-to-end
Passphrase Form Validation tests (10):
✅ Shows error when passphrase is too short
✅ Shows error when passphrase is exactly 7 characters
✅ Accepts passphrase with exactly 8 characters
✅ Shows error when passphrases do not match
✅ Shows length error before mismatch error
✅ Hides error message on successful validation
✅ Accepts empty confirmation when passphrase is too short (length check first)
✅ Validates with special characters in passphrase
✅ Validates with unicode characters in passphrase
✅ Is case-sensitive when comparing passphrases

All 33 tests passing.

Usage

Parent frame sends:

iframe.postMessage({
  type: "INJECT_WALLET_EXPORT_BUNDLE_ENCRYPTED",
  value: bundleString,
  organizationId: orgId,
  requestId: requestId
});

Parent frame receives (after user enters passphrase):

// On success:
{ type: "ENCRYPTED_WALLET_EXPORT", value: "<base64-encoded-encrypted-data>", requestId: "..." }

// On error:
{ type: "ERROR", value: "<error-message>", requestId: "..." }

Security Notes

  • Passphrase never leaves the iframe
  • Uses Web Crypto API for all cryptographic operations
  • PBKDF2 with 600k iterations provides reasonable protection against brute-force attacks
  • Random salt and IV ensure identical passphrases produce different ciphertexts
  • Embedded key is reset after bundle decryption (consistent with existing behavior)
  • Sensitive data (mnemonic bytes, encrypted bytes, passphrase inputs) is zeroed/cleared from memory and the DOM after encryption
  • Base64 encoding uses Array.from instead of String.fromCharCode.apply to prevent stack overflow on large payloads

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a passphrase-encrypted wallet export flow that encrypts wallet mnemonics with a user-provided passphrase before transmission from the iframe. Users enter and confirm a passphrase through a new UI form, and the encrypted data is sent to the parent frame as base64-encoded content instead of plaintext.

Key changes:

  • Implements AES-GCM-256 encryption with PBKDF2 key derivation (100,000 iterations)
  • Adds a new INJECT_WALLET_EXPORT_BUNDLE_ENCRYPTED message type and corresponding handler
  • Creates a passphrase form UI with validation (8-character minimum, matching confirmation)

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 11 comments.

File Description
export/index.template.html Adds encryption/decryption utilities, passphrase form UI with styling, and message handler for encrypted wallet export flow
export/index.test.js Adds 5 unit tests covering encryption, decryption, wrong passphrase handling, salt/IV randomness, and end-to-end base64 encoding

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@fainashalts fainashalts force-pushed the encrypt-wallet-export branch 2 times, most recently from 815a958 to ec9eac5 Compare February 7, 2026 05:46
@fainashalts fainashalts force-pushed the encrypt-wallet-export branch from ec9eac5 to 8773586 Compare February 7, 2026 05:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant