Skip to content

Instantly share code, notes, and snippets.

@ilap
Last active October 2, 2022 03:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ilap/52ac8d862e8667eab1675e2e3f7cc64a to your computer and use it in GitHub Desktop.
Save ilap/52ac8d862e8667eab1675e2e3f7cc64a to your computer and use it in GitHub Desktop.
Recover Byron with permutations

Install

$ mkdir recover && cd recover
# 1. Save the recover.js javascript below as recover.js

# 2. Install prereqs
$ npm i cbor cardano-crypto.js@6.1.1

# 3. Create a password file with the password candidates, 1-per-line
# I would say do not use too many passwords as permutations are kind of exponential. 
# Max 7 would be fine.
$ cat password.lst
Paass1
Password2
Pssse
$

# 4. Run the recover.js and pray
$ node your_keystore.key password.lst

Example output

$ 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"
    }
]

recover.js

#!/usr/bin/env node

// Install:
//
// npm i cbor cardano-crypto.js@6.1.1
//
// 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
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment