$ node recover.js examples/keystore.key examples/passwords.txt
Trying 160 permutations on _usKeys0 from 4 nr. of passwords
0 of 160: Trying on _usKeys0 keystore's user secret the following passwords Secret14
1 of 160: Trying on _usKeys0 keystore's user secret the following passwords Secret1jl34
2 of 160: Trying on _usKeys0 keystore's user secret the following passwords Secret1234
3 of 160: Trying on _usKeys0 keystore's user secret the following passwords Secreto234
4 of 160: Trying on _usKeys0 keystore's user secret the following passwords Secret1jl34,Secret14
5 of 160: Trying on _usKeys0 keystore's user secret the following passwords Secret1234,Secret14
6 of 160: Trying on _usKeys0 keystore's user secret the following passwords Secreto234,Secret14
7 of 160: Trying on _usKeys0 keystore's user secret the following passwords Secret14,Secret1jl34
8 of 160: Trying on _usKeys0 keystore's user secret the following passwords Secret1234,Secret1jl34
9 of 160: Trying on _usKeys0 keystore's user secret the following passwords Secreto234,Secret1jl34
Heureka 403e4a55591c9f0665437a13dda3d6ca698cb28f3ff3cfdf79d62c7156900546d963ef6126d62f71f6073d159da9e419413c5627705513f2ac645efa3171be85
[
{
"has-valid-encryption": true,
"encryption-passwords": "Secreto234,Secret1jl34",
"decrypted-masterkey": "403e4a55591c9f0665437a13dda3d6ca698cb28f3ff3cfdf79d62c7156900546d963ef6126d62f71f6073d159da9e419413c5627705513f2ac645efa3171be85055b42d4a95f19cb34b516a160a306c0eaef398e70ea91da450ccb2a7819e95b8c000436b43d5de6b0dd189cbfb0fc9ff954809abcb574d994cb5fafaf56b781"
}
]
#!/usr/bin/env node
// Install:
//
// npm i cbor [email protected]
//
// Usage:
// [node] ./index.js <KESTORE FILE> <PASSWORDS FILE>
//
// Description
// This js tries all candidates passwords as permutation with repetation but
// skipping two same consecutive password
const cbor = require('cbor');
const fs = require('fs');
const path = require('path');
const cardano = require('cardano-crypto.js')
const [_1, _2, keystorePath, passwordPath] = process.argv
const keystoreBytes = fs.readFileSync(path.isAbsolute(keystorePath) ?
keystorePath :
path.join(__dirname, keystorePath))
const passwordList = fs.readFileSync(path.isAbsolute(passwordPath) ?
passwordPath :
path.join(__dirname, passwordPath)).toString().split(/\r\n|\n/)
const lastItem = passwordList.pop()
const passwords = lastItem === '' ? passwordList : passwordList + lastItem
decodeKeystore(keystoreBytes, passwords)
.then(displayInformation)
.then(console.log)
.catch(console.exception);
async function toEncryptedSecretKey([encryptedPayload, passphraseHash], source, passwords) {
// The payload is a concatenation of the private key, the public key
// and the chain-code:
//
// +---------------------------------+-----------------------+-----------------------+
// | Extended Private Key (64 bytes) | Public Key (32 bytes) | Chain Code (32 bytes) |
// +---------------------------------+-----------------------+-----------------------+
// <------------ ENCRYPTED ---------->
//
const esk = await encryptedPayload.slice(0, 64);
const xpub = await encryptedPayload.slice(64, 128);
const pk = await xpub.slice(0, 32);
const cc = await encryptedPayload.slice(96);
// Validate master private key encryption
const {
hasValidEncryption,
decryptedSecret,
encryptionPasswords
} = await isEncryptionValid(esk, pk, passwords, source, cc)
return {
// Whether the encrypted master secret is decryptable
// Yes or no.
hasValidEncryption,
// The password to decrypt the master secret. if it's '' then then encrypted master secret is already decrypte
// undefined when the walled has invalid encryption
decryptedMasterKey: Buffer.concat([decryptedSecret, xpub]),
// Chain of passwords do decrypt the encrypted master key
encryptionPasswords,
};
}
// The keystore is "just" a CBOR-encoded 'UserSecret' as detailed below.
async function decodeKeystore(bytes, passwords) {
return await cbor.decodeAll(bytes).then(async (obj) => {
/**
* The original 'UserSecret' from cardano-sl looks like this:
*
* ```hs
* data UserSecret = UserSecret
* { _usVss :: Maybe VssKeyPair
* , _usPrimKey :: Maybe SecretKey
* , _usKeys :: [EncryptedSecretKey]
* , _usWalletSet :: Maybe WalletUserSecret
* , _usPath :: FilePath
* , _usLock :: Maybe FileLock
* }
*
* data WalletUserSecret = WalletUserSecret
* { _wusRootKey :: EncryptedSecretKey
* , _wusWalletName :: Text
* , _wusAccounts :: [(Word32, Text)]
* , _wusAddrs :: [(Word32, Word32)]
* }
* ```
*
* We are interested in:
* - usKeys:
* which is where keys have been stored since ~2018
*
* - usWalletSet
* which seems to have been used in earlier version; at least the
* wallet from the time did allow to restore so-called 'wallets'
* from keys coming from that 'WalletUserSecret'
*/
const usKeys = obj[0][2].map((x, idx) => toEncryptedSecretKey(x, `_usKeys${idx}`, passwords));
const usWalletSet = obj[0][3].map((x, idx) => toEncryptedSecretKey(x[0], `_usWalletSet${idx}`, passwords));
// Shows all wallet when legacy address is not provided
// or the wallet details if the legacy address belongs to the wallet
// or does not show any wallet when the legacy address does not belong to any wallet.
return (await Promise.all(usKeys.concat(usWalletSet))); //.filter((w) => w.isWalletAddress == true || w.isWalletAddress === undefined);
});
}
function displayInformation(keystore) {
const display = ({
hasValidEncryption,
decryptedMasterKey,
encryptionPasswords,
}) => {
return {
"has-valid-encryption": hasValidEncryption,
// It can either be the user provided or empty if no encryption occured.
"encryption-passwords": encryptionPasswords.toString(),
"decrypted-masterkey": decryptedMasterKey.toString('hex'),
}
};
return JSON.stringify(keystore.map(display), null, 4);
}
// A master private key encryption is valid when the decrypted private key
// can regenerate the stored root public key.
async function isEncryptionValid(xprv, pub, passwords, source, cc) {
// NOTE: Check whether that the stored master public key is the same
// with the generated from the stored master private key.
// This ensures that the master secret is not encrypted independently whether it
// has an empty or non-empty password based hash.
// This is enough as address derivation will do an additional check too.
const isDecrypted = await validatePublickey(await xprv, await pub)
if (isDecrypted) {
return {
hasValidEncryption: true,
decryptedSecret: xprv,
encryptionPasswords: ['']
}
} else {
// Calculate all possible permutations with repetition first then sort it
///////////////////////////////////////////////////////////////////////////////
const len = passwords.length
let allPermutations = []
for (l = 0; l < len; l++) {
/// Permutations without repetition
/// const r = permute(passwords, len - i)
/// Permutations with repetation
const r = await permuteWithRepetation(passwords, len - l)
allPermutations = allPermutations.concat(r)
}
// Sort All permutations, meaning less passwords tried first.
allPermutations.sort((a, b) => a.length - b.length)
//allPermutations.forEach((val, idx) => console.log(`${idx}: ${val}`));
plen = allPermutations.length
console.log(`Trying ${plen} permutations on ${source} from ${len} nr. of passwords`)
for (i = 0; i < plen; i++) {
let passwordList = allPermutations[i]
let len = passwordList.length
dsk = xprv
console.log(`${i+1} of ${plen}: Trying on ${source} keystore's user secret the following passwords ${passwordList.toString()}`)
for (j = 0; j < len; j++) {
dsk = await cardano.cardanoMemoryCombine(dsk, passwordList[j])
}
//console.log(`DSK for ${passwordList.toString()}: ${Buffer.concat([dsk, pub, cc]).toString('hex')}`)
if (validatePublickey(dsk, pub)) {
console.log(`Heureka ${dsk.toString('hex')}`)
return {
hasValidEncryption: true,
decryptedSecret: dsk,
encryptionPasswords: passwordList
}
}
}
}
//If it's failed to decrypt then it returns with the original xpriv.
return {
hasValidEncryption: false,
decryptedSecret: xprv,
encryptionPasswords: []
}
}
function validatePublickey(xprv, pub) {
const genPub = cardano.toPublic(xprv)
return Buffer.compare(pub, genPub) == 0
}
/// Permitation without repetation.
function permute(list, size = list.length) {
if (size > list.length) return []
else if (size == 1) return list.map(d => [d])
return list.flatMap(d => permute(list.filter(a => a !== d), size - 1).map(item => [d, ...item]));
}
/// Permutation with repetation
/// It filters out the permutations with consecutive elements e.g.,:
/// [a, a, b], [a, b, c, c, d, e] etc.
function permuteWithRepetation(list, size = list.length, filter = x => {
for (k = 1; k < x.length; k++)
if (x[k - 1] === x[k]) return true
}) {
const len = list.length;
if (size < 1 || size > len)
throw Error();
result = Array()
indexes = Array(len).fill(0);
total = Math.pow(len, size);
elems = Array(size).fill('');
while (total-- > 0) {
for (i = 0; i < len - (len - size); i++) {
elems[i] = list[indexes[i]]
}
if (!filter(elems)) result = result.concat([
[...elems]
])
for (i = 0; i < len; i++) {
if (indexes[i] >= len - 1) {
indexes[i] = 0;
} else {
indexes[i]++;
break;
}
}
}
return result
}