Skip to main content
All CollectionsAdvanced topics
Recovering digital assets from your Self-Custody Wallet
Recovering digital assets from your Self-Custody Wallet
Fabrizio avatar
Written by Fabrizio
Updated over a week ago

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 above

  • Access 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.

Did this answer your question?