Skip to content

Commit 9498da9

Browse files
committed
Merge pull-request #1200
2 parents e7a0d40 + 26de25b commit 9498da9

File tree

31 files changed

+1543
-248
lines changed

31 files changed

+1543
-248
lines changed

.changeset/proud-streets-exist.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@turnkey/iframe-stamper": minor
3+
---
4+
5+
Added the ability to override the iframe's embedded key pair using a Turnkey P256 private key exported and encrypted to the iframe's embedded key pair.

examples/disaster-recovery/README.md

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ pnpm start
2727
```
2828

2929
The interactive CLI provides a menu-driven interface for all disaster recovery operations:
30+
3031
- Generate test wallets for testing/PoC
3132
- Path 1: Import wallets and sweep funds
3233
- Path 2: Set up and recover from encryption escrow
@@ -67,11 +68,13 @@ pnpm run generate-test-wallet
6768
```
6869

6970
This will:
71+
7072
1. Generate a random 12 or 24 word mnemonic, OR a raw private key
7173
2. Show the derived Ethereum address
7274
3. Optionally save to a JSON file for reference
7375

7476
**Testing flow:**
77+
7578
1. Generate a test wallet
7679
2. Fund it with Sepolia testnet ETH from a faucet:
7780
- https://sepoliafaucet.com
@@ -87,12 +90,12 @@ This will:
8790

8891
### Why This Approach
8992

90-
| Benefit | Description |
91-
|---------|-------------|
92-
| Immediate Recovery | Keys are operational within Turnkey instantly—no decryption ceremonies |
93-
| Asset Agnostic | Single path for Bitcoin, Ethereum, Solana, and any SECP256k1/Ed25519 chains |
94-
| Fund Sweeping | SDK enables rapid fund movement without custom tooling |
95-
| Policy Controls | Full access to quorum policies, whitelisting, and transaction limits |
93+
| Benefit | Description |
94+
| ------------------ | --------------------------------------------------------------------------- |
95+
| Immediate Recovery | Keys are operational within Turnkey instantly—no decryption ceremonies |
96+
| Asset Agnostic | Single path for Bitcoin, Ethereum, Solana, and any SECP256k1/Ed25519 chains |
97+
| Fund Sweeping | SDK enables rapid fund movement without custom tooling |
98+
| Policy Controls | Full access to quorum policies, whitelisting, and transaction limits |
9699

97100
### Import HD Wallet (Mnemonic)
98101

@@ -103,6 +106,7 @@ pnpm run path1:import-wallet
103106
```
104107

105108
This will:
109+
106110
1. Initialize an import bundle (temporary encryption key from Turnkey's enclave)
107111
2. Prompt for your mnemonic seed phrase
108112
3. Encrypt the mnemonic to Turnkey's ephemeral public key
@@ -117,6 +121,7 @@ pnpm run path1:import-key
117121
```
118122

119123
Supports:
124+
120125
- Ethereum/EVM (SECP256K1)
121126
- Solana (ED25519)
122127
- Bitcoin (SECP256K1)
@@ -134,6 +139,7 @@ pnpm run path1:sweep-funds
134139
```
135140

136141
Features:
142+
137143
- Network selection (Sepolia testnet or Mainnet)
138144
- Gas estimation and balance checking
139145
- Confirmation prompts for safety
@@ -183,6 +189,7 @@ pnpm run path2:setup
183189
```
184190

185191
This will:
192+
186193
1. Create a P-256 encryption keypair in Turnkey (for encryption only, not on-chain)
187194
2. Prompt for your recovery material (mnemonic, credentials, or custom data)
188195
3. Encrypt the bundle with Turnkey's public key
@@ -200,18 +207,19 @@ pnpm run path2:recovery
200207
```
201208

202209
This will:
210+
203211
1. Generate a target keypair for receiving the exported key
204212
2. Export the encryption private key from Turnkey (may require quorum approval)
205213
3. Decrypt the export bundle to get the raw private key
206214
4. Load and decrypt your stored recovery bundle
207215

208216
### Security Properties
209217

210-
| Scenario | Impact |
211-
|----------|--------|
212-
| Customer infrastructure breach | Attacker gets encrypted blobs but cannot decrypt without Turnkey authentication |
213-
| Turnkey user breach | Attacker could access the encryption keypair but doesn't have the encrypted bundles |
214-
| Both breached | Both parties must be compromised simultaneously for full recovery |
218+
| Scenario | Impact |
219+
| ------------------------------ | ----------------------------------------------------------------------------------- |
220+
| Customer infrastructure breach | Attacker gets encrypted blobs but cannot decrypt without Turnkey authentication |
221+
| Turnkey user breach | Attacker could access the encryption keypair but doesn't have the encrypted bundles |
222+
| Both breached | Both parties must be compromised simultaneously for full recovery |
215223

216224
### Architecture
217225

@@ -253,15 +261,15 @@ This will:
253261

254262
## Comparison: Path 1 vs Path 2
255263

256-
| Factor | Path 1: Direct Import | Path 2: Encryption Escrow |
257-
|--------|----------------------|---------------------------|
258-
| Recovery Speed | Immediate—keys operational in Turnkey | Requires decryption ceremony |
259-
| Fund Sweeping | Built-in SDK support | Requires wallet tooling after decryption |
260-
| Key Storage | Turnkey holds wallet keys in enclave | Turnkey holds only encryption keys |
261-
| Material Location | Turnkey secure enclave | Your storage (encrypted) |
262-
| Complexity | Lower | Higher |
263-
| Best For | Enterprise DR, treasury backup | User recovery, compliance separation |
264-
| Security Model | Single party (authorized Turnkey users) | 2-of-2 (Turnkey auth + bundle access) |
264+
| Factor | Path 1: Direct Import | Path 2: Encryption Escrow |
265+
| ----------------- | --------------------------------------- | ---------------------------------------- |
266+
| Recovery Speed | Immediate—keys operational in Turnkey | Requires decryption ceremony |
267+
| Fund Sweeping | Built-in SDK support | Requires wallet tooling after decryption |
268+
| Key Storage | Turnkey holds wallet keys in enclave | Turnkey holds only encryption keys |
269+
| Material Location | Turnkey secure enclave | Your storage (encrypted) |
270+
| Complexity | Lower | Higher |
271+
| Best For | Enterprise DR, treasury backup | User recovery, compliance separation |
272+
| Security Model | Single party (authorized Turnkey users) | 2-of-2 (Turnkey auth + bundle access) |
265273

266274
---
267275

@@ -316,15 +324,19 @@ disaster-recovery/
316324
## Troubleshooting
317325

318326
### "Missing required environment variable"
327+
319328
Ensure all required variables are set in `.env.local`. Copy from `.env.local.example` and fill in your values.
320329

321330
### "Quorum approval required"
331+
322332
Your Turnkey organization has policies requiring multiple approvers. Coordinate with other key holders.
323333

324334
### "Not enough ETH to sweep"
335+
325336
The wallet balance is too low to cover gas costs. Fund the wallet or use a different network.
326337

327338
### Import fails with encryption error
339+
328340
Ensure you're using a valid BIP-39 mnemonic (12 or 24 words) or correctly formatted private key.
329341

330342
---

examples/disaster-recovery/src/interactive.ts

Lines changed: 56 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ function getTurnkeyClient(): Turnkey {
100100
if (!apiPublicKey || !apiPrivateKey || !organizationId) {
101101
throw new Error(
102102
"Missing required environment variables: API_PUBLIC_KEY, API_PRIVATE_KEY, ORGANIZATION_ID\n" +
103-
"Please copy .env.local.example to .env.local and fill in your credentials."
103+
"Please copy .env.local.example to .env.local and fill in your credentials.",
104104
);
105105
}
106106

@@ -214,9 +214,15 @@ async function mainMenu(): Promise<void> {
214214
message: "Press Enter to continue...",
215215
});
216216
console.clear();
217-
console.log("╔════════════════════════════════════════════════════════════╗");
218-
console.log("║ TURNKEY DISASTER RECOVERY TOOLKIT ║");
219-
console.log("╚════════════════════════════════════════════════════════════╝");
217+
console.log(
218+
"╔════════════════════════════════════════════════════════════╗",
219+
);
220+
console.log(
221+
"║ TURNKEY DISASTER RECOVERY TOOLKIT ║",
222+
);
223+
console.log(
224+
"╚════════════════════════════════════════════════════════════╝",
225+
);
220226
console.log();
221227
}
222228
}
@@ -399,7 +405,7 @@ async function generateMnemonicWallet(): Promise<{
399405
const row = words
400406
.slice(i, i + columns)
401407
.map(
402-
(word, idx) => `${String(i + idx + 1).padStart(2)}. ${word.padEnd(10)}`
408+
(word, idx) => `${String(i + idx + 1).padStart(2)}. ${word.padEnd(10)}`,
403409
)
404410
.join(" ");
405411
console.log(` ${row}`);
@@ -463,15 +469,15 @@ async function importHDWallet(): Promise<void> {
463469

464470
if (!organizationId || !userId) {
465471
throw new Error(
466-
"Missing required environment variables: ORGANIZATION_ID, USER_ID"
472+
"Missing required environment variables: ORGANIZATION_ID, USER_ID",
467473
);
468474
}
469475

470476
const turnkeyClient = getTurnkeyClient();
471477

472478
console.log("Step 1: Initialize import bundle from Turnkey");
473479
console.log(
474-
"This creates a temporary public key in Turnkey's enclave for encrypting your mnemonic."
480+
"This creates a temporary public key in Turnkey's enclave for encrypting your mnemonic.",
475481
);
476482
console.log();
477483

@@ -484,7 +490,7 @@ async function importHDWallet(): Promise<void> {
484490

485491
console.log("Step 2: Encrypt wallet material");
486492
console.log(
487-
"SECURITY WARNING: In production, perform this step on an air-gapped machine!"
493+
"SECURITY WARNING: In production, perform this step on an air-gapped machine!",
488494
);
489495
console.log();
490496

@@ -629,7 +635,7 @@ async function importPrivateKey(): Promise<void> {
629635

630636
if (!organizationId || !userId) {
631637
throw new Error(
632-
"Missing required environment variables: ORGANIZATION_ID, USER_ID"
638+
"Missing required environment variables: ORGANIZATION_ID, USER_ID",
633639
);
634640
}
635641

@@ -662,7 +668,7 @@ async function importPrivateKey(): Promise<void> {
662668

663669
console.log("Step 2: Encrypt private key material");
664670
console.log(
665-
"SECURITY WARNING: In production, perform this step on an air-gapped machine!"
671+
"SECURITY WARNING: In production, perform this step on an air-gapped machine!",
666672
);
667673
console.log();
668674

@@ -812,13 +818,13 @@ async function sweepFunds(): Promise<void> {
812818

813819
if (!signWith) {
814820
throw new Error(
815-
"Missing SIGN_WITH - set this to the imported wallet/key address in .env.local"
821+
"Missing SIGN_WITH - set this to the imported wallet/key address in .env.local",
816822
);
817823
}
818824

819825
if (!safeTreasury) {
820826
throw new Error(
821-
"Missing SAFE_TREASURY_ADDRESS - set this to your safe destination address in .env.local"
827+
"Missing SAFE_TREASURY_ADDRESS - set this to your safe destination address in .env.local",
822828
);
823829
}
824830

@@ -933,7 +939,9 @@ async function sweepFunds(): Promise<void> {
933939
console.log();
934940

935941
console.log("Waiting for transaction confirmation...");
936-
const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
942+
const receipt = await publicClient.waitForTransactionReceipt({
943+
hash: txHash,
944+
});
937945

938946
if (receipt.status === "success") {
939947
console.log();
@@ -976,7 +984,9 @@ async function escrowSetup(): Promise<void> {
976984
console.log("2. Encrypt your recovery material with Turnkey's public key");
977985
console.log("3. Save the encrypted bundle locally (you store this securely)");
978986
console.log();
979-
console.log("Note: The encrypted bundle should be stored in YOUR infrastructure,");
987+
console.log(
988+
"Note: The encrypted bundle should be stored in YOUR infrastructure,",
989+
);
980990
console.log("not on Turnkey. This creates a 2-of-2 security model.");
981991
console.log();
982992

@@ -1069,7 +1079,11 @@ async function escrowSetup(): Promise<void> {
10691079
let addMore = true;
10701080
while (addMore) {
10711081
const { service, apiKey } = await prompts([
1072-
{ type: "text", name: "service", message: "Service name (e.g., AWS, Alchemy):" },
1082+
{
1083+
type: "text",
1084+
name: "service",
1085+
message: "Service name (e.g., AWS, Alchemy):",
1086+
},
10731087
{ type: "password", name: "apiKey", message: "API Key/Secret:" },
10741088
]);
10751089

@@ -1141,8 +1155,8 @@ async function escrowSetup(): Promise<void> {
11411155
createdAt: new Date().toISOString(),
11421156
},
11431157
null,
1144-
2
1145-
)
1158+
2,
1159+
),
11461160
);
11471161

11481162
console.log();
@@ -1162,7 +1176,7 @@ async function escrowSetup(): Promise<void> {
11621176
}
11631177

11641178
async function createNewEncryptionKey(
1165-
turnkeyClient: Turnkey
1179+
turnkeyClient: Turnkey,
11661180
): Promise<{ privateKeyId: string; publicKey: string }> {
11671181
console.log();
11681182
console.log("Step 1: Create encryption keypair in Turnkey");
@@ -1258,7 +1272,7 @@ async function escrowRecovery(): Promise<void> {
12581272

12591273
const bundleContents = fs.readFileSync(
12601274
path.resolve(process.cwd(), bundlePath),
1261-
"utf-8"
1275+
"utf-8",
12621276
);
12631277
const storedBundle: StoredBundle = JSON.parse(bundleContents);
12641278

@@ -1271,7 +1285,7 @@ async function escrowRecovery(): Promise<void> {
12711285

12721286
if (!keyIdToUse) {
12731287
throw new Error(
1274-
"No encryption key ID found in bundle or ENCRYPTION_KEY_ID environment variable"
1288+
"No encryption key ID found in bundle or ENCRYPTION_KEY_ID environment variable",
12751289
);
12761290
}
12771291

@@ -1323,7 +1337,7 @@ async function escrowRecovery(): Promise<void> {
13231337

13241338
const decryptedRecoveryJson = await decryptWithPrivateKey(
13251339
encryptionPrivateKey,
1326-
storedBundle.encryptedData
1340+
storedBundle.encryptedData,
13271341
);
13281342

13291343
const recoveryBundle: RecoveryBundle = JSON.parse(decryptedRecoveryJson);
@@ -1334,7 +1348,9 @@ async function escrowRecovery(): Promise<void> {
13341348
console.log("═".repeat(60));
13351349
console.log();
13361350

1337-
console.log("WARNING: Sensitive data displayed below. Clear your terminal after use.");
1351+
console.log(
1352+
"WARNING: Sensitive data displayed below. Clear your terminal after use.",
1353+
);
13381354
console.log();
13391355

13401356
const { showData } = await prompts({
@@ -1352,8 +1368,14 @@ async function escrowRecovery(): Promise<void> {
13521368
console.log("Created at: ", recoveryBundle.createdAt);
13531369

13541370
if (recoveryBundle.metadata) {
1355-
console.log("Description:", recoveryBundle.metadata.description || "N/A");
1356-
console.log("Organization:", recoveryBundle.metadata.organization || "N/A");
1371+
console.log(
1372+
"Description:",
1373+
recoveryBundle.metadata.description || "N/A",
1374+
);
1375+
console.log(
1376+
"Organization:",
1377+
recoveryBundle.metadata.organization || "N/A",
1378+
);
13571379
}
13581380

13591381
console.log();
@@ -1376,7 +1398,10 @@ async function escrowRecovery(): Promise<void> {
13761398
console.log(` ${cred.service}: ${cred.apiKey}`);
13771399
}
13781400
console.log();
1379-
} else if (recoveryBundle.type === "custom" && recoveryBundle.data.custom) {
1401+
} else if (
1402+
recoveryBundle.type === "custom" &&
1403+
recoveryBundle.data.custom
1404+
) {
13801405
console.log();
13811406
console.log("CUSTOM DATA:");
13821407
console.log(recoveryBundle.data.custom);
@@ -1420,8 +1445,12 @@ async function escrowRecovery(): Promise<void> {
14201445
console.log("QUORUM APPROVAL REQUIRED");
14211446
console.log("═".repeat(60));
14221447
console.log();
1423-
console.log("This export requires additional approvals based on your policy.");
1424-
console.log("Please coordinate with other key holders to complete the export.");
1448+
console.log(
1449+
"This export requires additional approvals based on your policy.",
1450+
);
1451+
console.log(
1452+
"Please coordinate with other key holders to complete the export.",
1453+
);
14251454
console.log();
14261455
} else {
14271456
throw error;

0 commit comments

Comments
 (0)