From da0d2d4d0d8617d382c9206ee269b40838be57f0 Mon Sep 17 00:00:00 2001 From: Pal Dorogi <1113398+ilap@users.noreply.github.com> Date: Thu, 11 Aug 2022 12:12:45 +1000 Subject: [PATCH 1/6] Added encrypted key validation --- tools/inspect-keystore/index.js | 52 ++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/tools/inspect-keystore/index.js b/tools/inspect-keystore/index.js index 24a7076..d48b8aa 100755 --- a/tools/inspect-keystore/index.js +++ b/tools/inspect-keystore/index.js @@ -1,5 +1,9 @@ #!/usr/bin/env node +// Install: +// +// npm i cbor bech32 cardano-crypto.js@6.1.1 +// // Usage: // // ./index.js [FILEPATH] @@ -8,24 +12,26 @@ // // ./index.js examples/secret.key + const cbor = require('cbor'); const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); const { bech32 } = require('bech32'); +const cardano = require('cardano-crypto.js') -const [_1, _2, keystorePath] = process.argv; +const [_1, _2, keystorePath, byronAddress] = process.argv; -const bytes = fs.readFileSync(path.isAbsolute(keystorePath) - ? keystorePath - : path.join(__dirname, keystorePath)); +const bytes = fs.readFileSync(path.isAbsolute(keystorePath) ? + keystorePath : + path.join(__dirname, keystorePath)); decodeKeystore(bytes) - .then(displayInformation) + .then(validateKeystore) .then(console.log) .catch(console.exception); -function toEncryptedSecretKey ([encryptedPayload, passphraseHash], source) { +function toEncryptedSecretKey([encryptedPayload, passphraseHash], source) { const isEmptyPassphrase = $isEmptyPassphrase(passphraseHash); // The payload is a concatenation of the private key, the public key @@ -58,10 +64,14 @@ function toEncryptedSecretKey ([encryptedPayload, passphraseHash], source) { // Thus, is possible to know if a passphrase is an "empty passphrase" by comparing it with // a CBOR-serialized empty bytestring (`0x40`). The salt used for encryption is embedded in // passphrase. -function $isEmptyPassphrase (pwd) { +function $isEmptyPassphrase(pwd) { const cborEmptyBytes = Buffer.from('40', 'hex'); const [logN, r, p, salt, hashA] = pwd.toString('utf8').split('|'); - const opts = { N: 2 ** Number(logN), r: Number(r), p: Number(p) }; + const opts = { + N: 2 ** Number(logN), + r: Number(r), + p: Number(p) + }; const hashB = crypto .scryptSync(cborEmptyBytes, Buffer.from(salt, 'base64'), 32, opts) .toString('base64'); @@ -69,7 +79,7 @@ function $isEmptyPassphrase (pwd) { } // The keystore is "just" a CBOR-encoded 'UserSecret' as detailed below. -async function decodeKeystore (bytes) { +async function decodeKeystore(bytes) { return cbor.decodeAll(bytes).then((obj) => { /** * The original 'UserSecret' from cardano-sl looks like this: @@ -108,12 +118,20 @@ async function decodeKeystore (bytes) { } function displayInformation(keystore) { - const display = ({ xprv, xpub, cc, isEmptyPassphrase, source }) => { + const display = ({ + xprv, + xpub, + cc, + isEmptyPassphrase, + source, + hasValidKey + }) => { return { "encrypted-root-private-key": encodeBech32("root_xsk", Buffer.concat([xprv, cc])), "root-public-key": encodeBech32("root_xvk", Buffer.concat([xpub, cc])), source, "is-empty-passphrase": isEmptyPassphrase, + "has-valid-key": hasValidKey } }; return JSON.stringify(keystore.map(display), null, 4); @@ -124,3 +142,17 @@ function encodeBech32(prefix, bytes) { const MAX_LENGTH = 999; // long-enough, Cardano uses bech32 for long strings. return bech32.encode(prefix, words, MAX_LENGTH); } + +function validateKeystore(keystore) { + const validated = (key) => { + const generated = cardano.toPublic(key.xprv) + + key.hasValidKey = key.isEmptyPassphrase ? + (Buffer.compare(key.xpub, generated) == 0 ? "true" : "false") : + "unsure, more verification required" + + return key; + } + + return displayInformation(keystore.map(validated)) +} \ No newline at end of file From 4797550f4005f8384b3875cbcd1a382da96d3831 Mon Sep 17 00:00:00 2001 From: Pal Dorogi <1113398+ilap@users.noreply.github.com> Date: Thu, 11 Aug 2022 17:53:22 +1000 Subject: [PATCH 2/6] Added address's path validation --- tools/inspect-keystore/index.js | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/tools/inspect-keystore/index.js b/tools/inspect-keystore/index.js index d48b8aa..60fb08e 100755 --- a/tools/inspect-keystore/index.js +++ b/tools/inspect-keystore/index.js @@ -17,17 +17,20 @@ const cbor = require('cbor'); const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); -const { bech32 } = require('bech32'); +const { + bech32 +} = require('bech32'); const cardano = require('cardano-crypto.js') -const [_1, _2, keystorePath, byronAddress] = process.argv; + +const [_1, _2, keystorePath, candidatePassword, byronAddress] = process.argv; const bytes = fs.readFileSync(path.isAbsolute(keystorePath) ? keystorePath : path.join(__dirname, keystorePath)); decodeKeystore(bytes) - .then(validateKeystore) + .then((keystore) => validateKeystore(keystore, candidatePassword, byronAddress)) .then(console.log) .catch(console.exception); @@ -143,16 +146,27 @@ function encodeBech32(prefix, bytes) { return bech32.encode(prefix, words, MAX_LENGTH); } -function validateKeystore(keystore) { - const validated = (key) => { - const generated = cardano.toPublic(key.xprv) +async function validateKeystore(keystore,candidatePassword, byronaddress) { + const validated = async (key) => { + const hdp = await cardano.xpubToHdPassphrase(Buffer.concat([key.xpub, key.cc])) + const addrBuf = cardano.addressToBuffer(byronaddress) + + try { + const dp = cardano.getBootstrapAddressDerivationPath(addrBuf, hdp) + console.log(`${byronaddress.path} versus. ${dp}`) + } catch (e) { + console.log(`Address ${byronAddress} does not belong to root public key examined.`) + } + const generated = cardano.toPublic(key.xprv) key.hasValidKey = key.isEmptyPassphrase ? (Buffer.compare(key.xpub, generated) == 0 ? "true" : "false") : "unsure, more verification required" - return key; + return await key; } - return displayInformation(keystore.map(validated)) + const asyncRes = await Promise.all(keystore.map(validated)); + + return displayInformation(asyncRes) } \ No newline at end of file From 75060e184e9c3863ffa7ef26f83ce582d3329f37 Mon Sep 17 00:00:00 2001 From: Pal Dorogi Date: Thu, 11 Aug 2022 20:32:46 +1000 Subject: [PATCH 3/6] Added initial decryption check --- tools/inspect-keystore/index.js | 78 +++++++++++++++++++++++++-------- 1 file changed, 60 insertions(+), 18 deletions(-) diff --git a/tools/inspect-keystore/index.js b/tools/inspect-keystore/index.js index 60fb08e..3b52748 100755 --- a/tools/inspect-keystore/index.js +++ b/tools/inspect-keystore/index.js @@ -12,17 +12,13 @@ // // ./index.js examples/secret.key - const cbor = require('cbor'); const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); -const { - bech32 -} = require('bech32'); +const { bech32 } = require('bech32'); const cardano = require('cardano-crypto.js') - const [_1, _2, keystorePath, candidatePassword, byronAddress] = process.argv; const bytes = fs.readFileSync(path.isAbsolute(keystorePath) ? @@ -127,14 +123,18 @@ function displayInformation(keystore) { cc, isEmptyPassphrase, source, - hasValidKey + hasValidKey, + address, + path }) => { return { "encrypted-root-private-key": encodeBech32("root_xsk", Buffer.concat([xprv, cc])), "root-public-key": encodeBech32("root_xvk", Buffer.concat([xpub, cc])), source, "is-empty-passphrase": isEmptyPassphrase, - "has-valid-key": hasValidKey + "has-valid-key": hasValidKey, + "address": address, + "path": path } }; return JSON.stringify(keystore.map(display), null, 4); @@ -146,27 +146,69 @@ function encodeBech32(prefix, bytes) { return bech32.encode(prefix, words, MAX_LENGTH); } -async function validateKeystore(keystore,candidatePassword, byronaddress) { - const validated = async (key) => { - const hdp = await cardano.xpubToHdPassphrase(Buffer.concat([key.xpub, key.cc])) - const addrBuf = cardano.addressToBuffer(byronaddress) +async function validateKeystore(keystore, userPwd, byronaddress) { + const validated = async (key) => { + /* + * Steps for wallet validation: + * 0. Check whether the user's provide address belongs to this address. + * 1. is the current wallet is base on _usKeys or _usWalletSet? + * a. if _usWallets go to point 3. + * 2. Is the wallet has and empty-hash? + * a. if no, try to decrypt the encrypted secret key with the user's provided password. + * 3. Can the secret key regenerate it's stored public key? + * a. if no, then try to decrypt the encrypted secret key with the user's provided password. + * 4. Created a wallet recovery key secret.key based on the secret key. + */ + // TODO: 0. If the byron address is defined, check whether it belongs to + // the wallet or not. try { - const dp = cardano.getBootstrapAddressDerivationPath(addrBuf, hdp) - console.log(`${byronaddress.path} versus. ${dp}`) + /// 1st check. Check whether + const hdp = await cardano.xpubToHdPassphrase(Buffer.concat([key.xpub, key.cc])) + const addrBuf = await cardano.addressToBuffer(byronaddress) + const path = cardano.getBootstrapAddressDerivationPath(addrBuf, hdp) + key.address = byronaddress + key.path = path + // console.log(`${byronaddress} versus. ${dp}`) } catch (e) { - console.log(`Address ${byronAddress} does not belong to root public key examined.`) + // console.log(`Address ${byronAddress} does not belong to root public key examined. ${e}`) + key.address = "" + key.path = [] } - const generated = cardano.toPublic(key.xprv) + // TODO: 1. is it _usWallet or _usWalletSet based wallet? + + + // TODO: 2. Does the wallet has empty password based `passwordHash` + + // NOTE: 3. Check whether that the stored master public key is the same + // with the generated from the store master private key. This ensures that + // the master secret is not encrypted. + const isDecrypted = checkEncryption(key.xprv, key.xpub) key.hasValidKey = key.isEmptyPassphrase ? - (Buffer.compare(key.xpub, generated) == 0 ? "true" : "false") : + (isDecrypted ? "true" : "false") : "unsure, more verification required" - return await key; + // TODO: 3.a. Try to decrypt the encrypted sk with the user's provided password. + if (!isDecrypted) { + // FIXME: Vacumlabs cardano memory combine seems to be add and additional blake2b_512 stratching. + // + const prv = await cardano.cardanoMemoryCombine(key.xprv, userPwd) + if (checkEncryption(prv, key.xpub)) { + console.log(`The decryption was successfull`) + } else { + console.log(`The decryption was un-successfull`) + } + } + return key; } const asyncRes = await Promise.all(keystore.map(validated)); return displayInformation(asyncRes) -} \ No newline at end of file +} + +function checkEncryption(prv, pub) { + const genPub = cardano.toPublic(prv) + return Buffer.compare(pub, genPub) == 0 +} From 735418bd3b44a714043472f247bda065d9249ef8 Mon Sep 17 00:00:00 2001 From: Pal Dorogi <1113398+ilap@users.noreply.github.com> Date: Fri, 12 Aug 2022 16:35:10 +1000 Subject: [PATCH 4/6] Fixed some typos --- tools/inspect-keystore/index.js | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/tools/inspect-keystore/index.js b/tools/inspect-keystore/index.js index 3b52748..232c87b 100755 --- a/tools/inspect-keystore/index.js +++ b/tools/inspect-keystore/index.js @@ -146,19 +146,22 @@ function encodeBech32(prefix, bytes) { return bech32.encode(prefix, words, MAX_LENGTH); } + +/* + * Steps for wallet validation: + * 0. Check whether the user's provided address, if there is any, belongs to this Wallet. + * 1. Is the current wallet based on `_usKeys` or `_usWalletSet`? + * a. If _usWallets go to point 3. + * 2. Is the wallet has and empty-hash? + * a. if no, try to decrypt the encrypted secret key with the user's provided password. + * 3. Can the secret key regenerate its stored public key? + * a. if no, then try to decrypt the encrypted secret key with the user's provided password, + * b. as it means that the private key in the master secret is encrypted or corrupted or + * c. the public key is corrupted, etc. + * 4. Create a wallet recovery key secret.key based on the secret key. + */ async function validateKeystore(keystore, userPwd, byronaddress) { const validated = async (key) => { - /* - * Steps for wallet validation: - * 0. Check whether the user's provide address belongs to this address. - * 1. is the current wallet is base on _usKeys or _usWalletSet? - * a. if _usWallets go to point 3. - * 2. Is the wallet has and empty-hash? - * a. if no, try to decrypt the encrypted secret key with the user's provided password. - * 3. Can the secret key regenerate it's stored public key? - * a. if no, then try to decrypt the encrypted secret key with the user's provided password. - * 4. Created a wallet recovery key secret.key based on the secret key. - */ // TODO: 0. If the byron address is defined, check whether it belongs to // the wallet or not. From 54638ea3471f5e90345e9ee7d7af641c275dcd0a Mon Sep 17 00:00:00 2001 From: Pal Dorogi Date: Sat, 13 Aug 2022 19:24:23 +1000 Subject: [PATCH 5/6] Added address validation and key encryption features --- tools/inspect-keystore/README.md | 48 ++- .../inspect-keystore/examples/iog_secret.key | Bin 0 -> 1601 bytes tools/inspect-keystore/index.js | 279 ++++++++++++------ 3 files changed, 232 insertions(+), 95 deletions(-) create mode 100644 tools/inspect-keystore/examples/iog_secret.key diff --git a/tools/inspect-keystore/README.md b/tools/inspect-keystore/README.md index 7baf408..c29a01e 100644 --- a/tools/inspect-keystore/README.md +++ b/tools/inspect-keystore/README.md @@ -14,8 +14,9 @@ yarn yarn inspect-keystore [FILEPATH] ``` -## Example +## Examples +### No address and legacy address provided ``` yarn inspect-keystore examples/secret.key ``` @@ -26,25 +27,62 @@ yarn inspect-keystore examples/secret.key "encrypted-root-private-key": "root_xsk1...", "root-public-key": "root_xvk1fv6wc376lxm7h34akurxyfskg5wqqr59h2quw3y6usm23jal824e3ewvwpyh00g3hv9634d42ud8q4cyewfnf5n6qjzn9645nf5cctg4d3elm", "source": "_usKeys", - "is-empty-passphrase": false + "is-empty-passphrase": false, + "has-valid-encryption": false, + "encryption-password": "" }, { "encrypted-root-private-key": "root_xsk1...", "root-public-key": "root_xvk16vm9zdmhx8swt8szxdp954f9w7pvttd2w6x6q4nlg0qudsxpsanw6zj50we5adrjcystzu9gvsp8nqedmuvqq2y87q7pth49juz5ytg5cxpzz", "source": "_usKeys", - "is-empty-passphrase": true + "is-empty-passphrase": true, + "has-valid-encryption": true, + "encryption-password": "" }, { "encrypted-root-private-key": "root_xsk1...", "root-public-key": "root_xvk160gu7m79v4rkc9ejdwhtmh8nr30rgzd6d9aehuz2ckyvkkvx3s3dqslqydarhghtze732pxaj0mc3lz2wfgr5fsu7vu26ljvlzph4lc292vae", "source": "_usKeys", - "is-empty-passphrase": true + "is-empty-passphrase": true, + "has-valid-encryption": true, + "encryption-password": "" }, { "encrypted-root-private-key": "root_xsk1...", "root-public-key": "root_xvk122hdyt8065l2046mtqz2m42wv57zrcpll47qrju0dspn7lfc8nqjwprpfrvf5xpgka4ak344dah7cpknrdd5ztwx326ttlazgx88arg9pcjqy", "source": "_usKeys", - "is-empty-passphrase": false + "is-empty-passphrase": false, + "has-valid-encryption": false, + "encryption-password": "" + } +] +``` + +### No password but legacy address provided + +``` bash +yarn inspect-keystore examples/iog_secret.key "" "DdzFFzCqrht3HPYUiN4jnDyXBaWTMdikTZxdnojP3kxHBuvFB7QjbaYriFFfPLqHY622aCXAqKrq34xNNe1rvkW4uAGFUDtDrX4yQRaR" +# Or on macOS or Windows, you must change working directory to +# inspect-keystore directory/folder. + +node index.js examples/iog_secret.key "" "DdzFFzCqrht3HPYUiN4jnDyXBaWTMdikTZxdnojP3kxHBuvFB7QjbaYriFFfPLqHY622aCXAqKrq34xNNe1rvkW4uAGFUDtDrX4yQRaR" +``` + +``` json +[ + { + "encrypted-root-private-key": "root_xsk1...", + "root-public-key": "root_xvk1jmuul53lucc9sat9v0v44eez0tqs6d7yhky8xwsxggx8vcvaww8artrqv22780tsa20k33ud2pdd2z5lrhjc3pzujcsc5m5fjqaccug2qv093", + "source": "_usKeys", + "is-empty-passphrase": true, + "has-valid-encryption": true, + "encryption-password": "", + "is-wallet-address": true, + "address-derivation-path": [ + 2147483648, + 3730959219 + ], + "is-address-from-decrypted-key": true } ] ``` diff --git a/tools/inspect-keystore/examples/iog_secret.key b/tools/inspect-keystore/examples/iog_secret.key new file mode 100644 index 0000000000000000000000000000000000000000..88084549e9fa8f99d218c211d612743811dac1b8 GIT binary patch literal 1601 zcmYk(`Cn570040DlYj_nnuL~=^zo>DkhhBsl#2H@#Cp@C3!efGAAD6G+jLoVSSvl_bCMCwxEO!hBhB_nkBS+>g$V5Y9%e z^ou4ag-v55{ekeVa(C#3=ULynn2D`|yIa5DM1Fm%?Wfu2 z_ZZhkc)gW6W5MP;4Q=S^Qe(?r?VQJ{!uCWzr#SzYS7(Gb*fr^h%U5|qNWK!g|0fl# zdb03fM#9dIb*(ViUR@Tx3wzoV60VtD6W!Nyw6wNBme*=i-oHbgczel;d4XJ#Q|ez9 z+a>t!Kf^cq?hGQTf0 z@$KWy+0~0aZJRwYXE5``zP~zp!WaAI@7;YsASgPRJ-%r!Ddb{qX3Ooeb=Urey_+ZA z9TZqMe?D@n>V4n%#r{w30y2BH`nqB?#&bS`*4e(*mKY3%JCxP7vP<7?R8`$+#{#vw zt`ec*a#-!sKeiTT`j%}sZ7W|>Y?%g4mP5QaCJHlMR+uPAV{#m9$iXrbbtyEMMfIex zVUnDJilqd8oc1*oFw#vlf=ExnJUGh35HlB+t00zGk154ur;LU00incB^59J86m$iD zWMDx>=$luUH&T4n4^~Q9zDE-iJ$VbOwYi6{e8V?kKpEg4Mm|{XbsF3e4b>6Uv6`z5 zhh`n$gD+`z?WowEYD{Pyt8hz>G^d=74*O~@bUgh0IzPFO7}fBoYz_#SxAiStZ#+4< zg%K_i#?Fl>k)i+n*56GsAt}DTp=nTv1X$%dM_L*rB$>ogvn7S$5lH14RLe2jIH*(X zaGOw-))uGNPK|1q0AOQUA*D#BW^0m-sE)3qi{cfqK!}=PtBJ2Q+DtqfH%@7og63f* z=2?}U2MOKpJS zKOw1O;zs>HUE^yGWQ}akm~rv}Rv0t>&dZqRXm3Dr{mYrdrzedY-`rkD!A3{sLDn-v zct^fw)+03NgL0%v;-4}9=+4TrXlOdr4zl$My@QAYYL7%t^~6h1t_TK2C?F-%b6jSK zXXGlisZyyn?KPAHpoq+qDs*6W1%f4z*D)+ampxU&RJxgQDvuja#p9LnIT#ejn}W{# z*J_0yY^WZuGu8$9f6QH7*ZHNO`T6ad+s6DN)76gH6P2{8<(5yyi(YIjcNBeQc^Z9l z@saPy$IfkDNo`qj@np%_?(eUGGuaFG4t8QT^@1a*!G9hW)f^gfp58^ePZ_;*sC&~< z)waEw7G2wg;tvOIHjl(+|LlCV>V^0e;rmW9gcKHC&4|xtQ&5&h0U2R(3TDDRE~TADhBav-hs8iQIP@eb zBYg^5UVltYe8|}{PXrBRHue-mEkZ+t?JYswTTBJTC;!WuXShTm?1;%P<^7Q!b$;3D zg~C7_0!Dj(Eki>Gzv;w-SFLT39UeGS+LJNX?+l{;K+d|iyLaG~@5!1w``+q#9Fh1T zaAWQovPAsg#d{VjBN_ [USERPASSWORD] [LEGACYADDRESS] + // -// Example: +// Example #1: no user password and legacy addresses are provided +// It will list all wallets when user password and legacy addres are not provided. +// +// ./index.js "examples/iog_secret_iog.key "" "DdzFFzCqrht3HPYUiN4jnDyXBaWTMdikTZxdnojP3kxHBuvFB7QjbaYriFFfPLqHY622aCXAqKrq34xNNe1rvkW4uAGFUDtDrX4yQRaR" +// +// Example #2: no user password and legacy addresses are provided +// It will list only the wallet in which the legacy address belongs to. You must submit a +// user password, even if the user does not have any (apply an empty string in that case) +// +// node ./index.js "examples/iog_secret_iog.key "" "DdzFFzCqrht3HPYUiN4jnDyXBaWTMdikTZxdnojP3kxHBuvFB7QjbaYriFFfPLqHY622aCXAqKrq34xNNe1rvkW4uAGFUDtDrX4yQRaR" // -// ./index.js examples/secret.key - const cbor = require('cbor'); const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); -const { bech32 } = require('bech32'); +const { + bech32 +} = require('bech32'); const cardano = require('cardano-crypto.js') -const [_1, _2, keystorePath, candidatePassword, byronAddress] = process.argv; +const MAGIC = 764824073; // Mainnet MAGIC + +// The user's password is for encryption/decrytpion of the master private key. +const [_1, _2, keystorePath, encryptionPassword, legacyAddress] = process.argv; const bytes = fs.readFileSync(path.isAbsolute(keystorePath) ? keystorePath : path.join(__dirname, keystorePath)); -decodeKeystore(bytes) - .then((keystore) => validateKeystore(keystore, candidatePassword, byronAddress)) +const restoreParams = { + encryptionPassword: encryptionPassword ? encryptionPassword : '', + legacyAddress: legacyAddress ? legacyAddress : '' +} + +decodeKeystore(bytes, restoreParams) + .then(displayInformation) .then(console.log) .catch(console.exception); -function toEncryptedSecretKey([encryptedPayload, passphraseHash], source) { - const isEmptyPassphrase = $isEmptyPassphrase(passphraseHash); +async function toEncryptedSecretKey([encryptedPayload, passphraseHash], source, restoreParams) { + + const isEmptyPassphrase = isValidPassphrase(passphraseHash, ""); // The payload is a concatenation of the private key, the public key // and the chain-code: @@ -41,18 +60,63 @@ function toEncryptedSecretKey([encryptedPayload, passphraseHash], source) { // +---------------------------------+-----------------------+-----------------------+ // <------------ ENCRYPTED ----------> // - const xprv = encryptedPayload.slice(0, 64); - const xpub = encryptedPayload.slice(64, 96); + const esk = encryptedPayload.slice(0, 64); + const xpub = encryptedPayload.slice(64, 128); + const pk = xpub.slice(0, 32); const cc = encryptedPayload.slice(96); + // Validate master private key encryption + const { + hasValidEncryption, + decryptedSecret, + encryptionPassword + } = await isEncryptionValid(esk, pk, isEmptyPassphrase, restoreParams) + + // TODO: Check an edge case when the stored master public key cannot decrypt the derivation path + // but the master public key from a decrypted secret key can. It should never happen in normal circumstances. + // Check whether the address belongs to this wallet or not. + const derivationParams = restoreParams.legacyAddress != '' ? await validateAddress(xpub, restoreParams.legacyAddress) : { + path: undefined, + passPhrase: undefined + } + const isWalletAddress = derivationParams.path ? derivationParams.path.length !== 0 : derivationParams.path + + // TODO: Check edge cases when the address could not be derived either from + // the stored or the generated master secret. It can happen when the + // derivation indexes are wrong, or if the chaincode is invalid or when the + // address was generated by some other private key. + /// These are very unlikely, but who knows? + // + // It returns undefined when address is not defined. + // `pub` is god for concatenation as the decrypted and encrypted private key + // must have the same public key, + const addressFromDecryptedKey = isWalletAddress === true ? validateDerivedAddress(Buffer.concat([decryptedSecret, pk, cc]), derivationParams, restoreParams) : undefined + + // Validate derivation return { - xprv, + // The XPrv(64+32) and XPub(32+32) is the extended secret/private key and + // the normal Ed25519 public key concatenated with the corresponding chain + // code. + // See, details here: https://github.com/ilap/ShelleyStuffs#shelley-signing-keys + xprv: Buffer.concat([esk, cc]), xpub, - cc, encryptedPayload, passphraseHash, isEmptyPassphrase, source, + // 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 + encryptionPassword, + // Whether the address belong to this wallet or not + // Yes, not, and undefined when the user did not provide a byron address. + isWalletAddress, + path: derivationParams.path, + // Whether it was derived from encrypted or decrypted master key + // Yes, no and undefined when the address does not belong to this wallet + addressFromDecryptedKey, }; } @@ -63,23 +127,25 @@ function toEncryptedSecretKey([encryptedPayload, passphraseHash], source) { // Thus, is possible to know if a passphrase is an "empty passphrase" by comparing it with // a CBOR-serialized empty bytestring (`0x40`). The salt used for encryption is embedded in // passphrase. -function $isEmptyPassphrase(pwd) { - const cborEmptyBytes = Buffer.from('40', 'hex'); - const [logN, r, p, salt, hashA] = pwd.toString('utf8').split('|'); +function isValidPassphrase(passwordHash, encryptionPassword) { + + const cborBytes = cbor.encode(Buffer.from(encryptionPassword)) + const [logN, r, p, salt, keystoreHash] = passwordHash.toString('utf8').split('|'); const opts = { N: 2 ** Number(logN), r: Number(r), p: Number(p) }; - const hashB = crypto - .scryptSync(cborEmptyBytes, Buffer.from(salt, 'base64'), 32, opts) + const encryptionHash = crypto + .scryptSync(cborBytes, Buffer.from(salt, 'base64'), 32, opts) .toString('base64'); - return hashA === hashB; + + return keystoreHash === encryptionHash; } // The keystore is "just" a CBOR-encoded 'UserSecret' as detailed below. -async function decodeKeystore(bytes) { - return cbor.decodeAll(bytes).then((obj) => { +async function decodeKeystore(bytes, restoreParams) { + return await cbor.decodeAll(bytes).then(async (obj) => { /** * The original 'UserSecret' from cardano-sl looks like this: * @@ -110,9 +176,13 @@ async function decodeKeystore(bytes) { * wallet from the time did allow to restore so-called 'wallets' * from keys coming from that 'WalletUserSecret' */ - const usKeys = obj[0][2].map((x) => toEncryptedSecretKey(x, "_usKeys")); - const usWalletSet = obj[0][3].map((x) => toEncryptedSecretKey(x[0], "_usWalletSet")); - return usKeys.concat(usWalletSet); + const usKeys = obj[0][2].map((x) => toEncryptedSecretKey(x, "_usKeys", restoreParams)); + const usWalletSet = obj[0][3].map((x) => toEncryptedSecretKey(x[0], "_usWalletSet", restoreParams)); + + // 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); }); } @@ -120,21 +190,34 @@ function displayInformation(keystore) { const display = ({ xprv, xpub, - cc, + encryptedPayload, + passphraseHash, isEmptyPassphrase, source, - hasValidKey, - address, - path + hasValidEncryption, + encryptionPassword, + isWalletAddress, + path, + addressFromDecryptedKey, }) => { return { - "encrypted-root-private-key": encodeBech32("root_xsk", Buffer.concat([xprv, cc])), - "root-public-key": encodeBech32("root_xvk", Buffer.concat([xpub, cc])), + "encrypted-root-private-key": encodeBech32("root_xsk", xprv), + "root-public-key": encodeBech32("root_xvk", xpub), source, "is-empty-passphrase": isEmptyPassphrase, - "has-valid-key": hasValidKey, - "address": address, - "path": path + "has-valid-encryption": hasValidEncryption, + // It can either be the user provided or empty if no encryption occured. + "encryption-password": encryptionPassword, + "is-wallet-address": isWalletAddress, + "address-derivation-path": path, + // It means either the address generated from the + // - encrypted master private key or from the + // - decrypted one. + // It important when the paswhorHash has an empty passwordHash, but the + // master private key does not regenerate the stored public key. That + // means some glitch could happened in the past during the Daedalus + // updates etc. + "is-address-from-decrypted-key": addressFromDecryptedKey } }; return JSON.stringify(keystore.map(display), null, 4); @@ -146,72 +229,88 @@ function encodeBech32(prefix, bytes) { return bech32.encode(prefix, words, MAX_LENGTH); } +// A master private key encryption is valid when the decrypted private key +// can regenerate the stored root public key. +async function isEncryptionValid(xprv, pub, isEmptyPassphrase, restoreParams) { + // 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 = validatePublickey(xprv, pub) -/* - * Steps for wallet validation: - * 0. Check whether the user's provided address, if there is any, belongs to this Wallet. - * 1. Is the current wallet based on `_usKeys` or `_usWalletSet`? - * a. If _usWallets go to point 3. - * 2. Is the wallet has and empty-hash? - * a. if no, try to decrypt the encrypted secret key with the user's provided password. - * 3. Can the secret key regenerate its stored public key? - * a. if no, then try to decrypt the encrypted secret key with the user's provided password, - * b. as it means that the private key in the master secret is encrypted or corrupted or - * c. the public key is corrupted, etc. - * 4. Create a wallet recovery key secret.key based on the secret key. - */ -async function validateKeystore(keystore, userPwd, byronaddress) { - const validated = async (key) => { - - // TODO: 0. If the byron address is defined, check whether it belongs to - // the wallet or not. - try { - /// 1st check. Check whether - const hdp = await cardano.xpubToHdPassphrase(Buffer.concat([key.xpub, key.cc])) - const addrBuf = await cardano.addressToBuffer(byronaddress) - const path = cardano.getBootstrapAddressDerivationPath(addrBuf, hdp) - key.address = byronaddress - key.path = path - // console.log(`${byronaddress} versus. ${dp}`) - } catch (e) { - // console.log(`Address ${byronAddress} does not belong to root public key examined. ${e}`) - key.address = "" - key.path = [] + if (isDecrypted) { + return { + hasValidEncryption: true, + decryptedSecret: xprv, + encryptionPassword: '' } + } else { - // TODO: 1. is it _usWallet or _usWalletSet based wallet? - - - // TODO: 2. Does the wallet has empty password based `passwordHash` + // FIXME: Vacumlabs cardano memory combine seems stretching the user's password with blake2b_512. + // Check what old cardano-wallet used and add the version of memory_combine too. + const decrypted = await cardano.cardanoMemoryCombine(xprv, restoreParams.encryptionPassword) + if (validatePublickey(decrypted, pub)) { + return { + hasValidEncryption: true, + decryptedSecret: decrypted, + encryptionPassword: restoreParams.encryptionPassword + } + } else { + // It returns with the original xpriv + return { + hasValidEncryption: false, + decryptedSecret: xprv, + encryptionPassword: restoreParams.encryptionPassword + } + } + } +} - // NOTE: 3. Check whether that the stored master public key is the same - // with the generated from the store master private key. This ensures that - // the master secret is not encrypted. - const isDecrypted = checkEncryption(key.xprv, key.xpub) - key.hasValidKey = key.isEmptyPassphrase ? - (isDecrypted ? "true" : "false") : - "unsure, more verification required" +// It returns undefined if the address is an empty string or invalid. +async function validateAddress(xpub, legacyAddress) { + // This handles the undefined or empty string address + try { + const addrBuf = await cardano.addressToBuffer(legacyAddress) + // derivation password + const hdPassphrase = await cardano.xpubToHdPassphrase(xpub) + const path = cardano.getBootstrapAddressDerivationPath(addrBuf, hdPassphrase) - // TODO: 3.a. Try to decrypt the encrypted sk with the user's provided password. - if (!isDecrypted) { - // FIXME: Vacumlabs cardano memory combine seems to be add and additional blake2b_512 stratching. - // - const prv = await cardano.cardanoMemoryCombine(key.xprv, userPwd) - if (checkEncryption(prv, key.xpub)) { - console.log(`The decryption was successfull`) - } else { - console.log(`The decryption was un-successfull`) - } + return { + path: path, + passPhrase: hdPassphrase + } + } catch (e) { + return { + path: [], + passPhrase: undefined } - return key; } +} - const asyncRes = await Promise.all(keystore.map(validated)); +// FIXME: Some old version of cardano-sl used the password for key derivation too. +function validateDerivedAddress(xprv, deriv, restoreParams) { + // Byron addresses use V1 e.g. 1 scheme for validation + const scheme = 1; // 1 for legacy, 2 for icarus. - return displayInformation(asyncRes) + // Derive the addres keypair from the master secret key using the retrieved path + // from the address and the legacy derivation scheme + const addrXPrv = derivePrivAddrKey(xprv, deriv.path, scheme) + const addrXpub = addrXPrv.slice(64, 128) + + const packedAddress = cardano.packBootstrapAddress(deriv.path, addrXpub, deriv.passPhrase, scheme, MAGIC) + const generateAddress = cardano.base58.encode(packedAddress) + return generateAddress == restoreParams.legacyAddress } -function checkEncryption(prv, pub) { - const genPub = cardano.toPublic(prv) +function validatePublickey(xprv, pub) { + const genPub = cardano.toPublic(xprv) return Buffer.compare(pub, genPub) == 0 } + +// m/0'/0' is the derivation path for legacy (`Ddz...`) addresses. +// Icarus addresses (`Ae2...`) use BIP44 +function derivePrivAddrKey(rootPriv, path, scheme) { + const acct = cardano.derivePrivate(rootPriv, path[0], scheme) + return cardano.derivePrivate(acct, path[1], scheme) +} \ No newline at end of file From 7303ea4b870a21d3114675fe7ec36edef1d7f441 Mon Sep 17 00:00:00 2001 From: Pal Dorogi <1113398+ilap@users.noreply.github.com> Date: Thu, 18 Aug 2022 11:48:44 +1000 Subject: [PATCH 6/6] Add passwordHash check by the encryption password --- tools/inspect-keystore/.gitignore | 5 +++++ tools/inspect-keystore/index.js | 11 +++++++++-- tools/inspect-keystore/package.json | 1 + tools/inspect-keystore/yarn.lock | 20 -------------------- 4 files changed, 15 insertions(+), 22 deletions(-) delete mode 100644 tools/inspect-keystore/yarn.lock diff --git a/tools/inspect-keystore/.gitignore b/tools/inspect-keystore/.gitignore index 3e8350c..84ac381 100644 --- a/tools/inspect-keystore/.gitignore +++ b/tools/inspect-keystore/.gitignore @@ -42,6 +42,7 @@ build/Release # Dependency directories node_modules/ jspm_packages/ +package-lock.json # Snowpack dependency directory (https://snowpack.dev/) web_modules/ @@ -73,6 +74,9 @@ web_modules/ # Yarn Integrity file .yarn-integrity +# Yarn lock file +yarn.lock + # dotenv environment variable files .env .env.development.local @@ -121,6 +125,7 @@ dist # Stores VSCode versions used for testing VSCode extensions .vscode-test +.vscode # yarn v2 .yarn/cache diff --git a/tools/inspect-keystore/index.js b/tools/inspect-keystore/index.js index ca0ee99..0050aa1 100755 --- a/tools/inspect-keystore/index.js +++ b/tools/inspect-keystore/index.js @@ -52,6 +52,8 @@ async function toEncryptedSecretKey([encryptedPayload, passphraseHash], source, const isEmptyPassphrase = isValidPassphrase(passphraseHash, ""); + const canRegenerateHash = !isEmptyPassphrase ? isValidPassphrase(passphraseHash, restoreParams.encryptionPassword) : undefined + // The payload is a concatenation of the private key, the public key // and the chain-code: // @@ -70,7 +72,7 @@ async function toEncryptedSecretKey([encryptedPayload, passphraseHash], source, hasValidEncryption, decryptedSecret, encryptionPassword - } = await isEncryptionValid(esk, pk, isEmptyPassphrase, restoreParams) + } = await isEncryptionValid(esk, pk, restoreParams) // TODO: Check an edge case when the stored master public key cannot decrypt the derivation path // but the master public key from a decrypted secret key can. It should never happen in normal circumstances. @@ -103,6 +105,9 @@ async function toEncryptedSecretKey([encryptedPayload, passphraseHash], source, encryptedPayload, passphraseHash, isEmptyPassphrase, + // When it's not empty passhprase we try to regenerate the hash of the stored passwordHash + // If it's faling then it means the provided passwrod cannot regenerate the hash in keystore. + canRegenerateHash, source, // Whether the encrypted master secret is decryptable // Yes or no. @@ -193,6 +198,7 @@ function displayInformation(keystore) { encryptedPayload, passphraseHash, isEmptyPassphrase, + canRegenerateHash, source, hasValidEncryption, encryptionPassword, @@ -205,6 +211,7 @@ function displayInformation(keystore) { "root-public-key": encodeBech32("root_xvk", xpub), source, "is-empty-passphrase": isEmptyPassphrase, + "can-regenerate-hash": canRegenerateHash, "has-valid-encryption": hasValidEncryption, // It can either be the user provided or empty if no encryption occured. "encryption-password": encryptionPassword, @@ -231,7 +238,7 @@ function encodeBech32(prefix, bytes) { // A master private key encryption is valid when the decrypted private key // can regenerate the stored root public key. -async function isEncryptionValid(xprv, pub, isEmptyPassphrase, restoreParams) { +async function isEncryptionValid(xprv, pub, restoreParams) { // 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 diff --git a/tools/inspect-keystore/package.json b/tools/inspect-keystore/package.json index 9954165..e302dab 100644 --- a/tools/inspect-keystore/package.json +++ b/tools/inspect-keystore/package.json @@ -18,6 +18,7 @@ "homepage": "https://github.com/CardanoSolutions/ByronWalletRecovery#readme", "dependencies": { "bech32": "^2.0.0", + "cardano-crypto.js": "^6.1.1", "cbor": "^8.1.0" } } diff --git a/tools/inspect-keystore/yarn.lock b/tools/inspect-keystore/yarn.lock deleted file mode 100644 index e902c2d..0000000 --- a/tools/inspect-keystore/yarn.lock +++ /dev/null @@ -1,20 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -bech32@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/bech32/-/bech32-2.0.0.tgz#078d3686535075c8c79709f054b1b226a133b355" - integrity sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg== - -cbor@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/cbor/-/cbor-8.1.0.tgz#cfc56437e770b73417a2ecbfc9caf6b771af60d5" - integrity sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg== - dependencies: - nofilter "^3.1.0" - -nofilter@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/nofilter/-/nofilter-3.1.0.tgz#c757ba68801d41ff930ba2ec55bab52ca184aa66" - integrity sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==