diff --git a/docs/Introduction.md b/docs/Introduction.md
index 002b5732..ba97735c 100644
--- a/docs/Introduction.md
+++ b/docs/Introduction.md
@@ -20,6 +20,7 @@ At a structural level, a Turnkey Organization is comprised of a few core resourc
- Users: Humans or machines with access to an Organization
- Policies: Rules defining which users can take which actions within an Organization
- Private Keys: Crypto private keys, used to derive addresses and sign transactions
+- Wallets: A collection of crypto private keys that share a common seed
- Credentials: Used to verify Users - WebAuthn authenticators for human Users and API keys for API Users
diff --git a/docs/getting-started/Examples.md b/docs/getting-started/Examples.md
index 1ea8435d..ccb949f0 100644
--- a/docs/getting-started/Examples.md
+++ b/docs/getting-started/Examples.md
@@ -19,9 +19,11 @@ That said, we have built out several example services and applications to help i
| [`demo-ethers-passkeys`](https://github.com/tkhq/demo-ethers-passkeys) | A NextJS app that demonstrates how to use `@turnkey/ethers` to build a passkey-powered application |
| [`demo-viem-passkeys`](https://github.com/tkhq/demo-viem-passkeys) | A NextJS app that demonstrates how to use `@turnkey/viem` to build a passkey-powered application |
| [`deployer`](https://github.com/tkhq/sdk/tree/main/examples/deployer/) | Compile and deploy a smart contract |
+| [`email-recovery`](https://github.com/tkhq/sdk/tree/main/examples/email-recovery/) | A NextJS app that demonstrates how to use `@turnkey/iframe-stamper` to perform email |
| [`rebalancer`](https://github.com/tkhq/sdk/tree/main/examples/rebalancer/) | A demo application which showcases an example of how to use Turnkey for managing multiple types of keys & users |
| [`sweeper`](https://github.com/tkhq/sdk/tree/main/examples/sweeper/) | Sweep funds from one address to a different address |
| [`trading-runner`](https://github.com/tkhq/sdk/tree/main/examples/trading-runner/) | A sample application demonstrating a trading operation, using various private keys, users, and policies, powered by Uniswap |
+| [`wallet-export`](https://github.com/tkhq/sdk/tree/main/examples/wallet-export/) | A NextJS app that demonstrates how to use `@turnkey/iframe-stamper` to export a wallet as a mnemonic |
| [`with-ethers`](https://github.com/tkhq/sdk/tree/main/examples/with-ethers/) | Create a new Ethereum address, then sign and broadcast a transaction using the Ethers signer with Infura |
| [`with-viem`](https://github.com/tkhq/sdk/tree/main/examples/with-viem/) | Sign and broadcast a transaction using the Turnkey Custom Account and Infura |
| [`with-cosmjs`](https://github.com/tkhq/sdk/tree/main/examples/with-cosmjs/) | Create a new Cosmos address, then sign and broadcast a transaction on Celestia testnet using the CosmJS signer |
@@ -38,9 +40,11 @@ That said, we have built out several example services and applications to help i
A minimal consumer wallet app powered by Turnkey. Behind the scenes, it uses [`@turnkey/ethers`](https://www.npmjs.com/package/@turnkey/ethers) for signing and WalletConnect (v1) for accessing dapps.
-
+
+
+
See https://github.com/tkhq/demo-consumer-wallet for the code.
@@ -49,7 +53,9 @@ See https://github.com/tkhq/demo-consumer-wallet for the code.
A wallet application showing how users can register and authenticate using passkeys.
This demo uses the Turnkey API to create a new [Turnkey Sub-Organization](./Sub-Organizations.md) for each user, create a testnet Ethereum address and send a transaction on Sepolia (ETH testnet).
-
+
+
+
See https://wallet.tx.xyz (and https://github.com/tkhq/demo-passkey-wallet for the code).
@@ -57,7 +63,9 @@ See https://wallet.tx.xyz (and https://github.com/tkhq/demo-passkey-wallet for t
A simple application demonstrating how to create sub-organizations, create private keys, and sign with the [`@turnkey/ethers`](https://github.com/tkhq/sdk/tree/main/packages/ethers) signer, using passkeys.
-
+
+
+
See https://github.com/tkhq/demo-ethers-passkeys for the code.
@@ -65,7 +73,9 @@ See https://github.com/tkhq/demo-ethers-passkeys for the code.
A similar, simple application demonstrating how to create sub-organizations, create private keys, and sign with the [`@turnkey/viem`](https://github.com/tkhq/sdk/tree/main/packages/viem) signer, using passkeys.
-
+
+
+
See https://github.com/tkhq/demo-viem-passkeys for the code.
@@ -73,6 +83,8 @@ See https://github.com/tkhq/demo-viem-passkeys for the code.
A simple example using Turnkey and Figment to easily automate ETH staking.
-
+
+
+
See https://docs.figment.io/recipes/stake-eth-from-turnkey for the code.
\ No newline at end of file
diff --git a/docs/getting-started/Wallets.md b/docs/getting-started/Wallets.md
index 853fe29c..46081c40 100644
--- a/docs/getting-started/Wallets.md
+++ b/docs/getting-started/Wallets.md
@@ -59,3 +59,6 @@ Turnkey also supports raw private keys, but we recommend using Wallets since the
- Wallets can generate millions of addresses for various digital assets
- Wallets can be represented by a checksummed, mnemonic phrase making them easier to backup and recover
+## Export keys
+
+Exporting on Turnkey enables you or your end users to export a copy of a Wallet or Private Key from our system at any time. While most Turnkey users opt to keep Wallets within Turnkey's secure infrastructure, the export functionality means you are never locked into Turnkey, and gives you the freedom to design your own backup processes as you see fit. Check out our [Export Wallet guide](../integration-guides/export-wallets.md) to allow your users to securely export their wallets.
diff --git a/docs/getting-started/email-recovery.md b/docs/getting-started/email-recovery.md
index b9182cda..9a68539b 100644
--- a/docs/getting-started/email-recovery.md
+++ b/docs/getting-started/email-recovery.md
@@ -18,7 +18,9 @@ Email recovery starts with a new activity posted to Turnkey. This activity has t
This activity generates a new temporary API key pair (a "recovery credential"), saves the public key in organization data under the target user, and sends an email with the encrypted recovery credential:
-
+
+
+
Initiating a new email recovery require proper permissions via policies or being a parent organization. See [Authorization](#authorization) for more details.
@@ -33,7 +35,7 @@ Authorization for email recovery is based on our usual activity authorization: o
* `ACTIVITY_TYPE_RECOVER_USER` should be signed by the recovery credential sent via email. Even if not explicitly allowed by policy, a user is always able to add credentials to their own user. This includes adding a new authenticator when authenticated with a recovery credential. In other words, no special policy is needed to make this work: users are able to recover out-of-the-box.
-
+
@@ -63,7 +65,9 @@ If you _never_ want to have email recovery enabled, our `CREATE_SUB_ORGANIZATION
Unlike typical email recovery functionality, Turnkey's email recovery doesn't send unencrypted tokens via emails. This ensures no man-in-the-middle attack can happen: even if the content of the recovery email is leaked, an attacker wouldn't be able to decrypt the recovery credential. The following diagram summarizes the flow:
-
+
+
+
Our email recovery flow works by anchoring recovery in a **target encryption key** (TEK). This target encryption key is a standard P-256 key pair and can be created in many ways: completely offline, or online inside of script using the web crypto APIs.
diff --git a/docs/integration-guides/email-recovery-in-sub-organizations.md b/docs/integration-guides/email-recovery-in-sub-organizations.md
index 6026d1c5..1d75d5c8 100644
--- a/docs/integration-guides/email-recovery-in-sub-organizations.md
+++ b/docs/integration-guides/email-recovery-in-sub-organizations.md
@@ -31,7 +31,9 @@ In the rest of this guide we'll assume you are using these helpers.
Here's a diagram summarizing the email recovery flow step-by-step ([direct link](/img/email_recovery_steps.png)):
-
+
+
+
Let's review these steps in detail:
diff --git a/docs/integration-guides/export-wallets.md b/docs/integration-guides/export-wallets.md
new file mode 100644
index 00000000..c91f09bb
--- /dev/null
+++ b/docs/integration-guides/export-wallets.md
@@ -0,0 +1,123 @@
+---
+sidebar_position: 3
+description: Learn about Wallet Export on Turnkey
+slug: /integration-guides/export-wallets
+---
+# Export Wallet
+
+Turnkey's export functionality allows your end users to backup or transfer a [Wallet](../getting-started/Wallets.md) by securely viewing the wallet's [mnemonic phrase](https://learnmeabitcoin.com/technical/mnemonic). We engineered this feature to ensure that the user can export their mnemonic without exposing the mnemonic itself to Turnkey or your application.
+
+Follow along with the guide below to set up Wallet Export for your end users.
+
+## Before you start
+Make sure you have created a wallet for your user. Check out our [Quickstart guide](../getting-started/Quickstart.md) if you need help getting started.
+
+
+If you'd like to use a sub-organization as an end-user controlled wallet, follow our [Wallet integration guide](./sub-organizations-as-wallets.md).
+
+
+## Helper packages
+
+* We have released open-source code to create target encryption keys and decrypt exported wallet mnemonics. We've deployed a static HTML page hosted on `export.turnkey.com` meant to be embedded as an iframe element (see the code [here](https://github.com/tkhq/frames)). This ensures the mnemonics are encrypted to keys that the user has access to, but that your organization does not (because they live in the iframe, on a separate domain).
+* We have also built a package to help you insert this iframe and interact with it in the context of export: [`@turnkey/iframe-stamper`](https://www.npmjs.com/package/@turnkey/iframe-stamper)
+
+In the rest of this guide we'll assume you are using these helpers.
+
+## Export step-by-step
+
+Here's a diagram summarizing the wallet export flow step-by-step ([direct link](/img/wallet_export_steps.png)):
+
+
+
+
+
+Let's review these steps in detail:
+
+1. When a user on your application clicks "export", display a new export UI. We recommend setting this export UI as a new hosted page of your application that contains language explaining the security best practices users should follow once they've successfully exported their wallet. Remember: once the wallet has been exported, Turnkey can no longer ensure its security.
+
+ While the UI is in a loading state, your application uses [`@turnkey/iframe-stamper`](https://www.npmjs.com/package/@turnkey/iframe-stamper) to insert a new iframe element:
+ ```js
+ const iframeStamper = new IframeStamper({
+ iframeUrl: "https://export.turnkey.com",
+ // Configure how the iframe element is inserted on the page
+ iframeContainerId: "your-container",
+ iframeElementId: "turnkey-iframe",
+ });
+
+ // Inserts the iframe in the DOM. This creates the new encryption target key
+ const publicKey = await iframeStamper.init();
+
+ // Set state to not display iframe
+ let displayIframe = "none";
+
+ return (
+ // The iframe element can be hidden until the wallet is exported
+
+ );
+ ```
+2. Your code receives the iframe public key. Your application prompts the user to sign a new `EXPORT_WALLET` activity with the wallet ID and the iframe public key in the parameters.
+3. Your application polls for the activity response, which contains an export bundle. Remember: this export bundle is an encrypted mnemonic which can only be decrypted within the iframe.
+
+ Need help setting up async polling? Checkout our guide and helper [here](https://github.com/tkhq/sdk/tree/main/packages/http#withasyncpolling-helper).
+4. Your application injects the export bundle into the iframe for decryption and displays the iframe upon success:
+ ```js
+ // Inject export bundle into iframe
+ let success = await iframeStamper.injectWalletExportBundle(exportBundle);
+
+ if (success !== true) {
+ throw new Error("unexpected error while injecting export bundle");
+ }
+
+ // If successfully injected, update the state to display the iframe
+ iframeDisplay = "block";
+ ```
+
+Export is complete! The iframe now displays a numbered 3-column grid of words that form the mnemonic, directly to your end user.
+
+
+
+
+
+The exported wallet will remain stored within Turnkey’s infrastructure. In your Turnkey dashboard, the exported user Wallet will be flagged as “Exported”.
+
+## UI customization
+
+Everything is customizable in the export iframe except the 3-column grid of mnemonic words. Here's an example of how you can configure the styling of the iframe.
+```js
+const iframeCss = `
+iframe {
+ width: 400px;
+ height: 330px;
+ border: none;
+}
+`;
+
+return (
+
+
+
+);
+```
+
+## Private Keys
+
+Turnkey also supports exporting raw private keys. To implement export for private keys, follow the same steps above, but instead use the `EXPORT_PRIVATE_KEY` activity and the `injectKeyExportBundle` method from the [`@turnkey/iframe-stamper`](https://www.npmjs.com/package/@turnkey/iframe-stamper). At the end of a successful private key export, the iframe displays a hexadecimal-encoded raw private key.
+
+
+## Cryptographic details
+
+Turnkey's export functionality ensures that neither your application nor Turnkey can view the wallet mnemonic or private key.
+
+It works by anchoring export in a **target encryption key** (TEK). This target encryption key is a standard P-256 key pair and can be created in many ways: completely offline, or online inside of script using the web crypto APIs.
+
+ The following diagram summarizes the flow:
+
+
+
+
+
+The public part of this key pair is passed as a parameter inside of a signed `EXPORT_WALLET` or `EXPORT_PRIVATE_KEY` activity.
+
+Our enclave encrypts the wallet's mnemonic or raw private key to the user's TEK using the **Hybrid Public Key Encryption standard**, also known as **HPKE** or [RFC 9180](https://datatracker.ietf.org/doc/rfc9180/).
+
+Once the activity succeeds, the encrypted mnemonic or private key can be decrypted by the target public key offline or in an online script.
\ No newline at end of file
diff --git a/docs/passkeys/discoverable.md b/docs/passkeys/discoverable.md
index 6c954098..2db5ee96 100644
--- a/docs/passkeys/discoverable.md
+++ b/docs/passkeys/discoverable.md
@@ -22,8 +22,10 @@ With terminology out of the way, what is a "discoverable" credential compared to
A discoverable credential is a self-contained key pair, stored on the end-user's device. Discoverable credentials are preferred because keys are self-contained, can easily be synced and can be used across devices independently. Crucially for UX, the end-user is able to list their passkeys and choose which device/passkey they'd like to use:
-
-
+
+
+
+
With discoverable credentials you don't have to keep track of credential IDs. Your authentication flow can simply be: "prompt the user with passkey authentication", and let the browser or device native UX handle the rest! The downside is you lose some control over these prompts, because they will vary depending on your users' OS and browser.
diff --git a/docs/passkeys/integration.md b/docs/passkeys/integration.md
index 9f0eb87c..74bc90a7 100644
--- a/docs/passkeys/integration.md
+++ b/docs/passkeys/integration.md
@@ -10,7 +10,9 @@ sidebar_position: 2
A typical passkey flow is composed of 4 main steps, depicted below:
-
+
+
+
1. Your app frontend triggers a passkey prompt.
2. Your end-user uses their device to produce a signature with their passkey, and a signed request is produced.
diff --git a/docs/passkeys/introduction.md b/docs/passkeys/introduction.md
index 70ed9dab..5e448078 100644
--- a/docs/passkeys/introduction.md
+++ b/docs/passkeys/introduction.md
@@ -46,8 +46,10 @@ Support also varies by operating system: [this matrix](https://passkeys.dev/devi
We believe **it's time to move away from passwords** so we've built Turnkey without them. When you authenticate to Turnkey you'll be prompted to create a new passkey:
-
-
+
+
+
+
Authentication to Turnkey requires a passkey signature. No password needed!
diff --git a/docs/passkeys/options.md b/docs/passkeys/options.md
index 12982545..ca459054 100644
--- a/docs/passkeys/options.md
+++ b/docs/passkeys/options.md
@@ -20,7 +20,11 @@ This is the challenge signed by the end-user for registration. During registrati
Number of seconds before "giving up". The browser will simply show a timeout popup:
-
+
+
+
+
+
This UI isn't very helpful, so we recommend making the timeout long (5 minutes). The less your users see this, the better.
@@ -32,7 +36,9 @@ The `rp` options is an object with 2 fields: `id` and `name`.
`rp.id` will show up in the initial registration popup:
-
+
+
+
`rp.name` doesn't show up in the popup so can be set to anything. We recommend setting it to the correctly capitalized name of your app, in case browsers start showing it in their native UIs in the future.
@@ -53,7 +59,9 @@ Turnkey currently supports P256 only. In the near future Turnkey will support RS
The `user` field has three sub-fields:
- `id`: also known as "user handle", isn't visible to the end-user. We **strongly recommend setting this to a random value** (e.g. `const id = new Uint8Array(32); crypto.getRandomValues(id)`) to make sure a new passkey is created. Be aware: **if you accidentally set this value to an existing user handle, the corresponding passkey will be overridden!**. [This section of spec](https://www.w3.org/TR/webauthn-2/#dictionary-user-credential-params) is clear on the matter: "the user handle ought not be a constant value across different accounts, even for non-discoverable credentials".
- `name`: this will show up in the passkey list modal (see screenshot below). We recommend setting this to something the user will recognize: their email, the name of your app, or potentially leave this up to the user:
-
+
+
+
- `displayName`: as far as we can tell this doesn't show up in current browser UIs. It might show up in future iterations so it's best to populate this with the same value as `name`.
### `authenticatorSelection`
@@ -113,11 +121,15 @@ The credential ID needs to be passed as a buffer but is returned from registrati
If the wrong credential ID is specified, `transports: ["internal"]` is set, browsers error right away because they can enumerate internal credentials. Chrome, for example, displays the following error:
-
+
+
+
However, if the wrong credential ID is specified without `transports` set (or with other-than-internal `transports` set), browsers won't error right away because they can't enumerate external credentials. They will display an error once the user has pressed their security key or gone through the cross-device passkey flow:
-
+
+
+
### `attestation`
diff --git a/static/img/wallet_export_cryptography.png b/static/img/wallet_export_cryptography.png
new file mode 100644
index 00000000..43c1ee64
Binary files /dev/null and b/static/img/wallet_export_cryptography.png differ
diff --git a/static/img/wallet_export_mnemonic.png b/static/img/wallet_export_mnemonic.png
new file mode 100644
index 00000000..6a232a98
Binary files /dev/null and b/static/img/wallet_export_mnemonic.png differ
diff --git a/static/img/wallet_export_steps.png b/static/img/wallet_export_steps.png
new file mode 100644
index 00000000..e3e0ac8c
Binary files /dev/null and b/static/img/wallet_export_steps.png differ