In this guide, we'll share how you can recover your digital assets independently from the Levain platform.
This should be used in recovery scenarios, and you need to have access to your key card that was provided to you during the wallet creation process.
Setup
First, you'll need to have:
Access to a laptop where you are comfortable with decrypting private keys
Access to installing
node
v18.15.0 and aboveAccess to installing packages
@levain-contracts/simple-multi-sig-sdk
ethers
crypto-js
You can either make a fork of our example GitHub repository containing the recovery scripts, or create one on your own using our guide below. Whichever option you choose, we'll explain to you what goes into the recovery process.
Ensure you have the package.json
file at the root folder of your repository, with the following scripts.
{
"name": "levain-recovery-demo",
"engines": {
"node": "18.15.0"
},
"version": "1.0.0",
"description": "This recovery example helps you to decrypt your private keys in your keycard and recover your digital assets from your multi-sig wallet.",
"main": "decrypt.ts",
"scripts": {
"start": "npx ts-node decrypt.ts",
"decrypt": "npx ts-node decrypt.ts",
"recover": "npx ts-node recover.ts"
},
"dependencies": {
"@levain-contracts/simple-multi-sig-sdk": "^0.2.4",
"@noble/hashes": "^1.3.0",
"crypto-js": "^4.1.1",
"ethers": "^5.7.2"
},
"devDependencies": {
"@types/crypto-js": "^4.1.1"
},
"author": "",
"license": "ISC"
}
Next, install the dependencies in the location where you placed the package.json
file by running npm i
in the terminal.
Decrypting the private keys
You'll first have to decrypt the private keys from your key card that you've printed out during the wallet creation process.
You'll have to find a way to transfer the QR code data back to the device that you're performing the funds recovery -- one way is by scanning the QR code using your laptop camera.
Once you've the contents of the QR codes -- i.e. Keys 1 and 2 -- you should input the details into the area marked within the decrypt.ts
file.
// filename: decrypt.ts
import { sha256 } from '@noble/hashes/sha256';
import * as CryptoJS from 'crypto-js';
const AES_256_CBC = 'AES_256_CBC';
/**
* Key derivation spec we arbitrarily set and considered secured.
*
* @WARNING DO NOT change to use AES256 (CryptoJS) default key derivation function which is EvpKDF with hash iteration = 1.
* @WARNING reducing this configuration directly reduce password brute force difficulty.
*/
const HASHER = 'sha256';
const PBKDF2 = 'pbkdf2';
const KEY_SIZE = 8; // words = 256 bits
const HASH_ITER = 10_000;
export interface CipherBlob {
/**
* Cipher algorithm: AES256 with mode = CBC.
*/
cipher: typeof AES_256_CBC;
/**
* Key spec: method to derive fix length key
*/
keyDerivation: typeof PBKDF2;
/**
* Key spec: key derivation hashing algorithm
*/
keyHasher: typeof HASHER;
/**
* Key spec: key derivation iteration
*/
iter: typeof HASH_ITER;
/**
* Key spec: 32 bytes salt used for PBKDF2
*/
salt: string;
/**
* Cipher spec: 16 bytes initial vector used during encryption
*/
iv: string;
/**
* Cipher text, encryption result
*/
cipherText: string;
/**
* This is a dSHA256(plain) of the plain secret. Used for password correctness check during decryption process.
*/
hash: string;
}
function dSHA256(data: string): string {
const buf = Buffer.from(data, 'utf-8');
const dSHA = Buffer.from(sha256(sha256(buf)));
return dSHA.toString('hex').slice(0, 16);
}
/**
* Perform AES256 decryption by a key derived using pbkdf2.
*
* @param password utf-8 encoded string, input of pbkdf
* @param cipherBlob
* @returns {string} utf-8 encoded string, plain text before encryption.
*/
export function decrypt(password: string, cipherBlob: CipherBlob): string {
const { cipher, keyDerivation, keyHasher, cipherText, iv, salt, hash } = cipherBlob;
if (cipher !== AES_256_CBC || keyDerivation !== PBKDF2 || keyHasher !== HASHER) {
throw new Error('Unexpected cipher specification.');
}
const pw = CryptoJS.enc.Utf8.parse(password);
const s = CryptoJS.enc.Hex.parse(salt);
const key = CryptoJS.PBKDF2(pw, s, {
keySize: KEY_SIZE, // word count
iterations: HASH_ITER,
hasher: CryptoJS.algo.SHA256,
});
const ct = CryptoJS.enc.Base64.parse(cipherText);
const cp = CryptoJS.lib.CipherParams.create({ ciphertext: ct });
const plain = CryptoJS.AES.decrypt(cp, key, {
mode: CryptoJS.mode.CBC,
iv: CryptoJS.enc.Hex.parse(iv),
});
// pw verification by checking plain secret with known hash of true secret
if (dSHA256(password) !== hash) {
throw new Error('Invalid password');
}
// This will occassionally run into error with Malformed UTF-8 error for invalid password
return plain.toString(CryptoJS.enc.Utf8);
}
// Found in key card, please replace with your QR code contents
const jsonBackup1 = {
encryptedPrivateKey: {
cipher: 'AES_256_CBC' as typeof AES_256_CBC,
keyDerivation: 'pbkdf2' as typeof PBKDF2,
keyHasher: 'sha256' as typeof HASHER,
iter: 10000 as typeof HASH_ITER,
salt: 'SALT', // Replace this value
iv: 'IV', // Replace this value
cipherText: '', // Replace this value
hash: '', // Replace this value
},
publicKey: '0x',
};
// Found in key card, please replace with your QR code contents
const jsonBackup2 = {
encryptedPrivateKey: {
cipher: 'AES_256_CBC' as typeof AES_256_CBC,
keyDerivation: 'pbkdf2' as typeof PBKDF2,
keyHasher: 'sha256' as typeof HASHER,
iter: 10000 as typeof HASH_ITER,
salt: 'SALT', // Replace this value
iv: 'IV', // Replace this value
cipherText: '', // Replace this value
hash: '', // Replace this value
},
publicKey: '0x',
};
const walletPassword = 'YOUR_WALLET_PASSWORD'; // Replace with your wallet password
try {
const decryptedPrivateKey1 = decrypt(walletPassword, jsonBackup1.encryptedPrivateKey);
const decryptedPrivateKey2 = decrypt(walletPassword, jsonBackup2.encryptedPrivateKey);
console.log(`Your private key 1 is ${decryptedPrivateKey1}`);
console.log(`Your private key 2 is ${decryptedPrivateKey2}`);
} catch (err) {
// Handle errors for invalid passwords or invalid encryption key details
console.log('Invalid password or encryption details');
}
Following these steps should lead to the successful decryption of your private keys. If there are any exceptions or errors, it could likely be due to an incorrect password or invalid encryption details, so please verify those elements carefully.
Preparation for funds recovery
Now that you've successfully recovered your private keys, we'll outline the steps to interact with your SimpleMultiSig
smart contract and move your digital assets out of the wallet. Please note that you need access to an Ethereum node (e.g. Infura, Alchemy) and Ethers.js.
You will need to collect some pieces of information:
Ethereum address of the Levain contract wallet
The recovery address where you want to move your digital assets
Recover your funds
In the same project, create a recover.ts
file, which will be used to build a recovery transaction for native Ether.
typescript
import { providers, Wallet } from 'ethers';
import { SimpleMultiSigWallet, SimpleMultiSigWalletOwner } from '@levain-contracts/simple-multi-sig-sdk';
let wallet: SimpleMultiSigWallet;
const recoverFunds = async () => {
const provider = new providers.AlchemyProvider('goerli', '<EnterYourAlchemyAPIKey>'); // Connect to Ethereum provider, you should get an API key from www.alchemy.io
const executor = new Wallet('<EnterYourPrivateKey>').connect(provider); // Use a private key that have minimal amount of Ethers, enough to move funds out -- we recommend 0.05 ETH
console.log(`We'll recover your funds using ${executor.address}`);
const wallet = new SimpleMultiSigWallet({
address: '<EnterYourMultiSigWalletAddress>', // Replace with your multisig contract wallet address created with the Levain platform
chainId: provider.network.chainId,
owners: [
// replace with the owners' private keys, or extensions of SimpleMultiSigWalletOwner
// that provide the signing capability
// SimpleMultiSigWalletOwner.withPrivateKey('<YourPrivateKey>'),
SimpleMultiSigWalletOwner.withPrivateKey('<EnterYourPrivateKey1>'), // Replace with your private key 1 recovered from an earlier step
SimpleMultiSigWalletOwner.withPrivateKey('<EnterYourPrivateKey2>'), // Replace with your private key 1 recovered from an earlier step
],
executor,
});
console.log(`Current wallet's nonce is ${await wallet.getNonce()}`)
const tx = await wallet.execute({
destination: '<EnterYourRECOVERYAddress>', // Replace with your RECOVERY address i.e. the address you want to send your funds to
value: '100000000000000', // Amount in wei to transfer out
data: '0x',
gasLimit: '21000',
nonce: await wallet.getNonce(),
});
// eslint-disable-next-line no-console
console.log(`Your recovery transaction hash is ${tx.hash}, use this on public block explorers to check the status of your transaction.`);
console.log(`Note that once you've recovered your funds, we do not recommend you to use the contract wallet.`)
const txReceipt = await tx.wait(1);
};
recoverFunds();
Run this file using npm run recover
to execute the recovery. It is super important that you ensure you have access to the recovery address.
Once you successfully execute the transaction, the funds will be transferred to the destination address you specified. You should use a public block explorer like Etherscan to check the transaction status, since this recovery transaction will be done outside of the Levain platform.