From f474ffaf720e09b17f5b3034e23cb1b18d2031a8 Mon Sep 17 00:00:00 2001 From: David C Goldenberg Date: Mon, 19 Aug 2024 20:37:12 -0400 Subject: [PATCH] Added BIP32Derive, which uses bip32 from bitcoinjs. Modified a few more operations. Code compiles with 20.3.0 --- package-lock.json | 219 ++++++++++++++++-- package.json | 2 + src/core/config/Categories.json | 4 +- src/core/lib/Bech32.mjs | 8 +- src/core/lib/Bitcoin.mjs | 165 +++++++++++++ src/core/operations/BIP32Derive.mjs | 93 ++++++++ src/core/operations/PrivateECKeyToPublic.mjs | 25 +- src/core/operations/PrivateKeyToWIF.mjs | 27 +-- .../operations/PublicKeyToETHStyleAddress.mjs | 87 +++++++ .../operations/PublicKeyToP2PKHAddress.mjs | 105 +++------ tests/operations/index.mjs | 1 + .../operations/tests/DecryptKeyStoreFile.mjs | 4 +- .../operations/tests/PrivateECKeyToPublic.mjs | 4 +- tests/operations/tests/PrivateKeyToWIF.mjs | 4 +- .../tests/PublicKeyToETHStyleAddress.mjs | 110 +++++++++ .../tests/PublicKeyToP2PKHAddress.mjs | 61 ++--- tests/operations/tests/SeedToMPK.mjs | 15 ++ 17 files changed, 764 insertions(+), 170 deletions(-) create mode 100644 src/core/operations/BIP32Derive.mjs create mode 100644 src/core/operations/PublicKeyToETHStyleAddress.mjs create mode 100644 tests/operations/tests/PublicKeyToETHStyleAddress.mjs diff --git a/package-lock.json b/package-lock.json index dddcd9cd..e335b539 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,12 +12,14 @@ "dependencies": { "@astronautlabs/amf": "^0.0.6", "@babel/polyfill": "^7.12.1", + "@bitcoinerlab/secp256k1": "^1.0.5", "@blu3r4y/lzma": "^2.3.3", "argon2-browser": "^1.18.0", "arrive": "^2.4.1", "avsc": "^5.7.7", "bcryptjs": "^2.4.3", "bignumber.js": "^9.1.1", + "bip32": "^4.0.0", "blakejs": "^1.2.1", "bootstrap": "4.6.2", "bootstrap-colorpicker": "^3.4.0", @@ -1906,6 +1908,15 @@ "node": ">=6.9.0" } }, + "node_modules/@bitcoinerlab/secp256k1": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@bitcoinerlab/secp256k1/-/secp256k1-1.0.5.tgz", + "integrity": "sha512-8gT+ukTCFN2rTxn4hD9Jq3k+UJwcprgYjfK/SQUSLgznXoIgsBnlPuARMkyyuEjycQK9VvnPiejKdszVTflh+w==", + "dependencies": { + "@noble/hashes": "^1.1.5", + "@noble/secp256k1": "^1.7.1" + } + }, "node_modules/@blu3r4y/lzma": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/@blu3r4y/lzma/-/lzma-2.3.3.tgz", @@ -2657,6 +2668,28 @@ "integrity": "sha512-GEBeGoXVmTYPtNC4Yq34vfgxf6mlFyEagxpsfH18Qe5BvctF2rprX+wI5dKBm9p5IqHo6ZOcXHCufOeP3cjuOw==", "dev": true }, + "node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/secp256k1": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz", + "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2749,6 +2782,17 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, + "node_modules/@scure/base": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", + "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, "node_modules/@testim/chrome-version": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@testim/chrome-version/-/chrome-version-1.1.3.tgz", @@ -3851,6 +3895,14 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base-x": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz", + "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -3931,6 +3983,20 @@ "node": ">=8" } }, + "node_modules/bip32": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/bip32/-/bip32-4.0.0.tgz", + "integrity": "sha512-aOGy88DDlVUhspIXJN+dVEtclhIsfAUppD43V0j40cPTld3pv/0X/MlrZSZ6jowIaQQzFwP8M6rFU2z2mVYjDQ==", + "dependencies": { + "@noble/hashes": "^1.2.0", + "@scure/base": "^1.1.1", + "typeforce": "^1.11.5", + "wif": "^2.0.6" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -4412,6 +4478,24 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "dependencies": { + "base-x": "^3.0.2" + } + }, + "node_modules/bs58check": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", + "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", + "dependencies": { + "bs58": "^4.0.0", + "create-hash": "^1.1.0", + "safe-buffer": "^5.1.2" + } + }, "node_modules/bson": { "version": "4.7.2", "resolved": "https://registry.npmjs.org/bson/-/bson-4.7.2.tgz", @@ -9458,9 +9542,9 @@ } }, "node_modules/jsonwebtoken/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "bin": { "semver": "bin/semver" } @@ -14444,9 +14528,9 @@ "dev": true }, "node_modules/protobufjs": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.3.tgz", - "integrity": "sha512-TtpvOqwB5Gdz/PQmOjgsrGH1nHjAQVCN7JG4A6r1sXRWESL5rNMAiRcBQlCAdKxZcAbstExQePYG8xof/JVRgg==", + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.5.tgz", + "integrity": "sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==", "hasInstallScript": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", @@ -15183,9 +15267,9 @@ } }, "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -16170,6 +16254,11 @@ "node": ">= 0.6" } }, + "node_modules/typeforce": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", + "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==" + }, "node_modules/ua-parser-js": { "version": "1.0.35", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.35.tgz", @@ -16962,6 +17051,14 @@ "node": ">=8" } }, + "node_modules/wif": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz", + "integrity": "sha512-HIanZn1zmduSF+BQhkE+YXIbEiH0xPr1012QbFEGB0xsKqJii0/SqJjyn8dFv6y36kOznMgMB+LGcbZTJ1xACQ==", + "dependencies": { + "bs58check": "<3.0.0" + } + }, "node_modules/winston": { "version": "2.4.7", "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.7.tgz", @@ -16998,9 +17095,9 @@ } }, "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "engines": { "node": ">=0.10.0" } @@ -18520,6 +18617,15 @@ "to-fast-properties": "^2.0.0" } }, + "@bitcoinerlab/secp256k1": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@bitcoinerlab/secp256k1/-/secp256k1-1.0.5.tgz", + "integrity": "sha512-8gT+ukTCFN2rTxn4hD9Jq3k+UJwcprgYjfK/SQUSLgznXoIgsBnlPuARMkyyuEjycQK9VvnPiejKdszVTflh+w==", + "requires": { + "@noble/hashes": "^1.1.5", + "@noble/secp256k1": "^1.7.1" + } + }, "@blu3r4y/lzma": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/@blu3r4y/lzma/-/lzma-2.3.3.tgz", @@ -19104,6 +19210,16 @@ "integrity": "sha512-GEBeGoXVmTYPtNC4Yq34vfgxf6mlFyEagxpsfH18Qe5BvctF2rprX+wI5dKBm9p5IqHo6ZOcXHCufOeP3cjuOw==", "dev": true }, + "@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==" + }, + "@noble/secp256k1": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz", + "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==" + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -19187,6 +19303,11 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, + "@scure/base": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", + "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==" + }, "@testim/chrome-version": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@testim/chrome-version/-/chrome-version-1.1.3.tgz", @@ -20145,6 +20266,14 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "base-x": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz", + "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -20201,6 +20330,17 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, + "bip32": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/bip32/-/bip32-4.0.0.tgz", + "integrity": "sha512-aOGy88DDlVUhspIXJN+dVEtclhIsfAUppD43V0j40cPTld3pv/0X/MlrZSZ6jowIaQQzFwP8M6rFU2z2mVYjDQ==", + "requires": { + "@noble/hashes": "^1.2.0", + "@scure/base": "^1.1.1", + "typeforce": "^1.11.5", + "wif": "^2.0.6" + } + }, "bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -20583,6 +20723,24 @@ "update-browserslist-db": "^1.0.11" } }, + "bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "requires": { + "base-x": "^3.0.2" + } + }, + "bs58check": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", + "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", + "requires": { + "bs58": "^4.0.0", + "create-hash": "^1.1.0", + "safe-buffer": "^5.1.2" + } + }, "bson": { "version": "4.7.2", "resolved": "https://registry.npmjs.org/bson/-/bson-4.7.2.tgz", @@ -24397,9 +24555,9 @@ }, "dependencies": { "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" } } }, @@ -28032,9 +28190,9 @@ } }, "protobufjs": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.3.tgz", - "integrity": "sha512-TtpvOqwB5Gdz/PQmOjgsrGH1nHjAQVCN7JG4A6r1sXRWESL5rNMAiRcBQlCAdKxZcAbstExQePYG8xof/JVRgg==", + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.5.tgz", + "integrity": "sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==", "requires": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -28614,9 +28772,9 @@ } }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true }, "send": { @@ -29413,6 +29571,11 @@ "mime-types": "~2.1.24" } }, + "typeforce": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", + "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==" + }, "ua-parser-js": { "version": "1.0.35", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.35.tgz", @@ -29997,6 +30160,14 @@ "string-width": "^4.0.0" } }, + "wif": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz", + "integrity": "sha512-HIanZn1zmduSF+BQhkE+YXIbEiH0xPr1012QbFEGB0xsKqJii0/SqJjyn8dFv6y36kOznMgMB+LGcbZTJ1xACQ==", + "requires": { + "bs58check": "<3.0.0" + } + }, "winston": { "version": "2.4.7", "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.7.tgz", @@ -30029,9 +30200,9 @@ } }, "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==" }, "worker-loader": { "version": "3.0.8", diff --git a/package.json b/package.json index 3dca5be4..68c18926 100644 --- a/package.json +++ b/package.json @@ -94,12 +94,14 @@ "dependencies": { "@astronautlabs/amf": "^0.0.6", "@babel/polyfill": "^7.12.1", + "@bitcoinerlab/secp256k1": "^1.0.5", "@blu3r4y/lzma": "^2.3.3", "argon2-browser": "^1.18.0", "arrive": "^2.4.1", "avsc": "^5.7.7", "bcryptjs": "^2.4.3", "bignumber.js": "^9.1.1", + "bip32": "^4.0.0", "blakejs": "^1.2.1", "bootstrap": "4.6.2", "bootstrap-colorpicker": "^3.4.0", diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index b478e891..9a07d35d 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -320,7 +320,9 @@ "Seedphrase To Seed", "Change Extended Key Version", "Seed To Master Key", - "Decrypt Keystore File" + "Decrypt Keystore File", + "BIP32Derive", + "Public Key To ETH Style Address" ] }, { diff --git a/src/core/lib/Bech32.mjs b/src/core/lib/Bech32.mjs index 4e6ddb38..9121eecd 100644 --- a/src/core/lib/Bech32.mjs +++ b/src/core/lib/Bech32.mjs @@ -13,9 +13,9 @@ * Javascript code below taken from: * https://github.com/geco/bech32-js/blob/master/bech32-js.js * Implements various segwit encoding / decoding functions. - * + * * MIT License - * + * * Copyright (c) 2019 geco * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,10 +23,10 @@ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE diff --git a/src/core/lib/Bitcoin.mjs b/src/core/lib/Bitcoin.mjs index 3a5ac538..c456599c 100644 --- a/src/core/lib/Bitcoin.mjs +++ b/src/core/lib/Bitcoin.mjs @@ -12,6 +12,126 @@ import {toHex} from "crypto-api/src/encoder/hex.mjs"; import Utils from "../Utils.mjs"; import OperationError from "../errors/OperationError.mjs"; +/** + * Validates the length of the passed in input as one of the allowable lengths. + * @param {*} input + * @param {*} allowableLengths + * @returns + */ +function validateLengths(input, allowableLengths) { + return allowableLengths.includes(input.length); +} + +/** + * Returns true if input is a valid hex string, false otherwise. + * @param {*} input + */ +function isHex(input) { + const re = /^[0-9A-Fa-f]{2,}$/g; + return re.test(input); +} + +/** + * Returns true if input could be interpreted as a byte string, false otherwise. + */ +function isValidBytes(input) { + for (let i=0; i < input.length; i ++) { + if (input.charCodeAt(i) > 255) { + return false; + } + } + return true; +} + +/** + * We validate a passed in input to see if it could be a valid private key. + * A valid private key is string of length 64 that is valid hex, or of length 32 that could be valid bytes. + * @param {*} input + */ +export function validatePrivateKey(input) { + const curInput = input.trim(); + if (!validateLengths(curInput, [32, 64])) { + return "Invalid length. We want either 32 or 64 but we got: " + curInput.length; + } + if (curInput.length === 64 && !isHex(curInput)) { + return "We have a string of length 64, but not valid hex. Cannot be interpreted as a private key."; + } + if (curInput.length === 32 && !isValidBytes(curInput)) { + return "We have a string of length 32 but cannot cannot be interpreted as valid bytes."; + } + return ""; +} + +/** + * We validate a passed in input to see if it could be a valid public key. + * A valid public key (in bytes) is either: + * 65 bytes beginning with 04 + * 33 bytes beginning with 02 or 03 + * @param {*} input + */ +export function validatePublicKey(input) { + const curInput = input.trim(); + if (!validateLengths(curInput, [33, 65, 66, 130])) { + return "Invalid length. We want either 33, 65 (if bytes) or 66, 130 (if hex) but we got: " + curInput.length; + } + if (isHex(curInput)) { + if (!validateLengths(curInput, [66, 130])) { + return "We have a hex string, but its length is wrong. We want 66, 130 but we got: " + curInput.length; + } + if (curInput.length === 66 && (curInput.slice(0, 2) !== "02" && curInput.slice(0, 2) !== "03")) { + return "We have a valid hex string, of reasonable length, (66) but doesn't start with the right value. Correct values are 02, or 03 but we have: " + curInput.slice(0, 2); + } + if (curInput.length === 130 && curInput.slice(0, 2) !== "04") { + return "We have a valid hex string of reasonable length, (130) but doesn't start with the right value. Correct values are 04 but we have: " + curInput.slice(0, 2); + } + return ""; + } + if (isValidBytes(curInput)) { + if (!validateLengths(curInput, [33, 65])) { + return "We have a byte string, but its length is wrong. We want 33 or 65 but we got: " + curInput.length; + } + if (curInput.length === 33 && toHex(curInput[0]) !== "02" && toHex(curInput[0]) !== "03") { + return "We have a valid byte string, of reasonable length, (33) but doesn't start with the right value. Correct values are 02, or 03 but we have: " + toHex(curInput[0]) ; + } + if (curInput.length === 65 && toHex(curInput[0]) !== "04") { + return "We have a valid byte string, of reasonable length, (65) but doesn't start with the right value. Correct value is 04 but we have: " + toHex(curInput[0]); + } + return ""; + } + +} + +/** + * We make sure the input is a valid hex string, regardless of if its hex or bytes. + * If not valid bytes or hex, we throw TypeError. + * @param {*} input + * @returns + */ +export function makeSureIsHex(input) { + if (!(isValidBytes(input)) && !(isHex(input))) { + throw TypeError("Input: " + input + " is not valid bytes or hex."); + } + if (isValidBytes(input) && !isHex(input)) { + return toHex(input); + } + return input; +} + +/** + * We make sure the input is valid bytes, regardless of if its hex or bytes. + * If not valid bytes or hex, we throw TypeError. + * @param {*} input + */ +export function makeSureIsBytes(input) { + if (!(isValidBytes(input)) && !(isHex(input))) { + throw TypeError("Input: " + input + " is not valid bytes or hex."); + } + if (isHex(input)) { + return fromArrayBuffer(Utils.convertToByteArray(input, "hex")); + } + return input; +} + // ################################################ BEGIN HELPER HASH FUNCTIONS ################################################# // SHA256(SHA256(input)) @@ -212,6 +332,35 @@ export function deserializeExtendedKeyFunc (input) { } } +// Reverse lookup for version bytes +const versionString = { + "043587cf": "tpub", + "04358394": "tprv", + "044a5262": "upub", + "044a4e28": "uprv", + "045f1cf6": "vpub", + "045f18bc": "vprv", + "024289ef": "Upub", + "024285b5": "Uprv", + "02575483": "Vpub", + "02575048": "Vprv", + "0488b21e": "xpub", + "0488ade4": "xprv", + "049d7cb2": "ypub", + "049d7878": "yprv", + "04b24746": "zpub", + "04b2430c": "zprv", + "02aa7ed3": "Zpub", + "02aa7a99": "Zprv", + "0295b43f": "Ypub", + "0295b005": "Yprv", + "019da462": "Ltub", + "019d9cfe": "Ltpv", + "01b26ef6": "Mtub", + "01b26792": "Mtpv", + "0436f6e1": "ttub", + "0436ef7d": "ttpv" +}; // Version byte dictionary. const versionBytes = { @@ -253,6 +402,16 @@ export function getExtendedKeyVersion(input) { } +/** + * Reverse lookup for version string. We take in bytes, output string. + * @param {*} input + * @returns + */ +export function getExtendedKeyString(input) { + return versionString[input]; +} + + /** * We serialize the extended key based off of the passed in data. * We assume that the i value should be interpreted as a Uint32 LE. @@ -296,6 +455,12 @@ const versionByteInfo = { "P2SH": "C4", "WIF": "EF", "hrp": "tb" + }, + "LTC": { + "hrp": "ltc", + "P2PKH": "30", + "P2SH": "32", + "WIF": "B0" } }; diff --git a/src/core/operations/BIP32Derive.mjs b/src/core/operations/BIP32Derive.mjs new file mode 100644 index 00000000..549cf4ef --- /dev/null +++ b/src/core/operations/BIP32Derive.mjs @@ -0,0 +1,93 @@ +/** + * @author dgoldenberg [virtualcurrency@mitre.org] + * @copyright Crown Copyright 2023 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +// import OperationError from "../errors/OperationError.mjs"; +import { b58DoubleSHAChecksum} from "../lib/Bitcoin.mjs"; +import { BIP32Factory} from "bip32"; +import ecc from "@bitcoinerlab/secp256k1"; + +/** + * Sanity checks a derivation path. + * @param {*} input + */ +function verifyDerivationPath(input) { + const splitResults = input.split("/"); + let startIndex = 0; + // We skip the first index if its m, as that's common. + if (splitResults[0] === "m") { + startIndex = 1; + } + for (let i =startIndex; i < splitResults.length; i++) { + const re = /^[0-9]{1,}[']{0,1}$/g; + if (!re.test(splitResults[i])) { + return false; + } + } + return true; +} + +/** + * BIP32Derive operation + */ +class BIP32Derive extends Operation { + + /** + * BIP32Derive constructor + */ + constructor() { + super(); + + this.name = "BIP32Derive"; + this.module = "Default"; + this.description = "Takes in an extended key, performs BIP32 key derivation on the extended key, and returns the result as an extended key."; + this.infoURL = "https://en.bitcoin.it/wiki/BIP_0032"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + "name": "Derivation Path", + "type": "string", + "value": "" + }, + ]; + this.checks = [ + { + "pattern": "^(X|x|Y|y|Z|z|L|l|T|t)[pub|prv|tbv|tub][A-HJ-NP-Za-km-z1-9]{2,}$", + "flags": "", + "args": [] + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + // We check if input is blank. + // If its blank or just whitespace, we don't need to bother dealing with it. + if (input.trim().length === 0) { + return ""; + } + input = input.trim(); + if (!verifyDerivationPath(args[0])) { + return "Invalid derivation path: " + args[0] + "\n"; + } + const xkeyRe = /^(X|x|Y|y|Z|z|L|l|T|t)[pub|prv|tbv|tub][A-HJ-NP-Za-km-z1-9]{2,}$/g; + if (!b58DoubleSHAChecksum(input) || !xkeyRe.test(input)) { + return "Possibly invalid Extended Key: " + input + "\n"; + } + const bip32 = BIP32Factory(ecc); + const node = bip32.fromBase58(input); + const child = node.derivePath(args[0]); + return child.toBase58(); + } + +} + +export default BIP32Derive; diff --git a/src/core/operations/PrivateECKeyToPublic.mjs b/src/core/operations/PrivateECKeyToPublic.mjs index 8a19d0d7..90e17138 100644 --- a/src/core/operations/PrivateECKeyToPublic.mjs +++ b/src/core/operations/PrivateECKeyToPublic.mjs @@ -7,9 +7,11 @@ */ import Operation from "../Operation.mjs"; -import {toHex} from "../lib/Hex.mjs"; import ec from "elliptic"; +import { validatePrivateKey, makeSureIsHex} from "../lib/Bitcoin.mjs"; +// import { toHex } from "crypto-api/src/encoder/hex.mjs"; +// const curves = ["secp256k1", "ed25519", "curve25519", "p521", "p384", "p256", "p224", "p192"]; /** * Class that takes in a private key, and returns the public key, either in compressed or uncompressed form(s). */ @@ -56,24 +58,15 @@ class PrivateECKeyToPublic extends Operation { return ""; } input = input.trim(); - const re = /^[0-9A-Fa-f]{2,}$/g; - if (!(input.length === 64 && re.test(input)) && !(input.length === 32)) { - return "Must pass a hex string of length 64, or a byte string of length 32. Got length " + input.length; - } - // If we have bytes, we need to turn the bytes to hex. - if (input.length !== undefined && input.length === 32) { - const buf = new Uint8Array(new ArrayBuffer(32)); - for (let i= 0; i < 32; i ++) { - if (input.charCodeAt(i) > 255) { - return "Cannot interpret this 32 character string as bytes."; - } - buf[i] = input.charCodeAt(i); - } - input = toHex(buf, "", 2, "", 0); + const privKeyCheck = validatePrivateKey(input); + + if (privKeyCheck.trim().length !== 0) { + return "Error with the input as private key. Error is:\n\t" + privKeyCheck; } + const processedInput = makeSureIsHex(input); const ecContext = ec.ec("secp256k1"); - const key = ecContext.keyFromPrivate(input); + const key = ecContext.keyFromPrivate(processedInput); const pubkey = key.getPublic(args[0], "hex"); return pubkey; diff --git a/src/core/operations/PrivateKeyToWIF.mjs b/src/core/operations/PrivateKeyToWIF.mjs index 18544b18..3dbeadd4 100644 --- a/src/core/operations/PrivateKeyToWIF.mjs +++ b/src/core/operations/PrivateKeyToWIF.mjs @@ -7,10 +7,10 @@ */ import Operation from "../Operation.mjs"; -import { base58Encode, getWIFVersionByte, doubleSHA} from "../lib/Bitcoin.mjs"; +import { base58Encode, getWIFVersionByte, doubleSHA, validatePrivateKey, makeSureIsHex} from "../lib/Bitcoin.mjs"; import { fromArrayBuffer } from "crypto-api/src/encoder/array-buffer.mjs"; import {toHex} from "crypto-api/src/encoder/hex.mjs"; -import {toHex as toHexOther} from "../lib/Hex.mjs"; +// import {toHex as toHexOther} from "../lib/Hex.mjs"; import Utils from "../Utils.mjs"; @@ -65,26 +65,13 @@ class PrivateKeyToWIF extends Operation { return ""; } input = input.trim(); - // We check to see if the input is hex or not. - // If it is not, we convert it back to hex - const re = /[0-9A-Fa-f]{2,}/g; - if (!(input.length === 64 && re.test(input)) && !(input.length === 32)) { - return "Must pass a hex string of length 64, or a byte string of length 32. Got length: " + input.length; + const privateKeyCheck = validatePrivateKey(input); + if (privateKeyCheck.trim().length !== 0) { + return "Error parsing private key. Error is:\n\t" + privateKeyCheck; } - if (input.length === 32) { - const buf = new Uint8Array(new ArrayBuffer(32)); - - for (let i= 0; i < 32; i ++) { - if (input.charCodeAt(i) > 255) { - return "Cannot interpret this 32 character string as bytes."; - } - buf[i] = input.charCodeAt(i); - } - input = toHexOther(buf, "", 2, "", 0); - } - + const processedKey = makeSureIsHex(input); const versionByte = getWIFVersionByte(args[0]); - let extendedPrivateKey = versionByte + input; + let extendedPrivateKey = versionByte + processedKey; if (args[1]) { extendedPrivateKey += "01"; } diff --git a/src/core/operations/PublicKeyToETHStyleAddress.mjs b/src/core/operations/PublicKeyToETHStyleAddress.mjs new file mode 100644 index 00000000..d5505c67 --- /dev/null +++ b/src/core/operations/PublicKeyToETHStyleAddress.mjs @@ -0,0 +1,87 @@ +/** + * @author dgoldenberg [virtualcurrency@mitre.org] + * @copyright Crown Copyright 2023 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import { makeSureIsBytes, validatePublicKey} from "../lib/Bitcoin.mjs"; +import JSSHA3 from "js-sha3"; +import Utils from "../Utils.mjs"; +import ec from "elliptic"; + +/** + * Turns a public key into an ETH address. + * @param {*} input Input, a public key in hex or bytes. + */ +function pubKeyToETHAddress(input) { + // Ethereum addresses require uncompressed public keys. + // We convert if the public key is compressed. + let curKey = makeSureIsBytes(input); + if (curKey[0] !== 0x04 || curKey.length !== 65) { + const ecContext = ec.ec("secp256k1"); + const thisKey = ecContext.keyFromPublic(curKey); + curKey = thisKey.getPublic(false, "hex"); + } + const algo = JSSHA3.keccak256; + // We need to redo the hex-> bytes transformation here because Javascript is silly. + // sometimes what is desired is an array of ints. + // Other times a string + // Here, the Keccak algorithm seems to want an array of ints. (sigh) + const result = algo(Utils.convertToByteArray(curKey, "hex").slice(1,)); + return "0x" + result.slice(-40); +} + +/** + * PublicKeyToETHStyleAddress operation + */ +class PublicKeyToETHStyleAddress extends Operation { + + /** + * PublicKeyToETHStyleAddress constructor + */ + constructor() { + super(); + + this.name = "Public Key To ETH Style Address"; + this.module = "Default"; + this.description = "Converts a public key (compressed or uncompressed) to an Ethereum style address."; + this.infoURL = "https://www.freecodecamp.org/news/how-to-create-an-ethereum-wallet-address-from-a-private-key-ae72b0eee27b/"; + this.inputType = "string"; + this.outputType = "string"; + this.args = []; + this.checks = [ + { + pattern: "^0[3|2][a-fA-F0-9]{64}$", + flags: "", + args: [] + }, + { + pattern: "^04[a-fA-F0-9]{128}$", + flags: "", + args: [] + } + + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + // We check if input is blank. + // If its blank or just whitespace, we don't need to bother dealing with it. + if (input.trim().length === 0) { + return ""; + } + if (validatePublicKey(input) !== "") { + return validatePublicKey(input); + } + return pubKeyToETHAddress(input); + } + +} + +export default PublicKeyToETHStyleAddress; diff --git a/src/core/operations/PublicKeyToP2PKHAddress.mjs b/src/core/operations/PublicKeyToP2PKHAddress.mjs index 87dc09af..19f904e7 100644 --- a/src/core/operations/PublicKeyToP2PKHAddress.mjs +++ b/src/core/operations/PublicKeyToP2PKHAddress.mjs @@ -9,11 +9,11 @@ import Operation from "../Operation.mjs"; import { fromArrayBuffer } from "crypto-api/src/encoder/array-buffer.mjs"; import {toHex} from "crypto-api/src/encoder/hex.mjs"; -import { base58Encode, getP2PKHVersionByte, getP2SHVersionByte, hash160Func, doubleSHA, getHumanReadablePart} from "../lib/Bitcoin.mjs"; +import { base58Encode, getP2PKHVersionByte, getP2SHVersionByte, hash160Func, doubleSHA, getHumanReadablePart, makeSureIsBytes, validatePublicKey} from "../lib/Bitcoin.mjs"; import {encodeProgramToSegwit} from "../lib/Bech32.mjs"; -import JSSHA3 from "js-sha3"; import Utils from "../Utils.mjs"; + /** * Converts a Public Key to a P2PKH Address of the given type. */ @@ -27,19 +27,19 @@ class PublicKeyToP2PKHAddress extends Operation { this.name = "Public Key To Cryptocurrency Address"; this.module = "Default"; - this.description = "Turns a public key into a cryptocurrency address."; + this.description = "Turns a public key into a cryptocurrency address. Can select P2PKH, P2SH-P2WPKH and P2WPKH addresses for Bitcoin and Testnet."; this.inputType = "string"; this.outputType = "string"; this.args = [ { "name": "Currency Type", "type": "option", - "value": ["BTC", "Testnet", "Ethereum"] + "value": ["BTC", "Testnet", "LTC"] }, { "name": "Address Type", "type": "option", - "value": ["P2PKH (V1 BTC Addresses)", "P2SH-P2PWPKH (Segwit Compatible)", "Segwit (P2WPKH)"] + "value": ["P2PKH (V1 BTC Addresses)", "P2SH-P2PWPKH (Segwit Compatible V3 Addresses)", "Segwit (P2WPKH bc1 Addresses)"] } ]; this.checks = [ @@ -48,11 +48,6 @@ class PublicKeyToP2PKHAddress extends Operation { flags: "", args: ["BTC", "P2PKH (V1 BTC Addresses)"] }, - { - pattern: "^04[a-fA-F0-9]{128}$", - flags: "", - args: ["Ethereum", "P2PKH (V1 BTC Addresses)"] - } ]; } @@ -68,75 +63,39 @@ class PublicKeyToP2PKHAddress extends Operation { if (input.trim().length === 0) { return ""; } - // We check to see if the input is hex or not. - // If it is, we convert back to bytes. - const re = /([0-9A-Fa-f]{2,})/g; - let inputIsHex = false; - let curInput = input; - if (re.test(input)) { - inputIsHex = true; - } - if (inputIsHex) { - curInput = fromArrayBuffer(Utils.convertToByteArray(input, "hex")); + if (validatePublicKey(input) !== "") { + return validatePublicKey(input); } - // We sanity check the input - const startByte = toHex(curInput[0]); - if (curInput.length !== 33 && curInput.length !== 65) { - return "Input is wrong length. Should be either 33 or 65 bytes, but is: " + curInput.length; - } - if (curInput.length === 33 && startByte !== "03" && startByte !== "02") { - return "Input is 33 bytes, but begins with invalid byte: " + startByte; - } - - if (curInput.length === 65 && startByte !== "04") { - return "Input is 65 bytes, but begins with invalid byte: " + startByte; - } - - if (args[0] === "Ethereum") { - // Ethereum addresses require uncompressed public keys. - if (startByte !== "04" || curInput.length !== 65) { - return "Ethereum addresses require uncompressed public keys."; - } - const algo = JSSHA3.keccak256; - // We need to redo the hex-> bytes transformation here because Javascript is silly. - // sometimes what is desired is an array of ints. - // Other times a string - // Here, the Keccak algorithm seems to want an array of ints. (sigh) - let result; - if (inputIsHex) { - result = algo(Utils.convertToByteArray(input, "hex").slice(1,)); - } else { - result = algo(Utils.convertToByteArray(toHex(input), "hex").slice(1,)); - } - return "0x" + result.slice(-40); - - } else { - // We hash the input - const hash160 = toHex(hash160Func(curInput)); - // We do segwit addresses first. - if (args[1] === "Segwit (P2WPKH)") { - const redeemScript = hash160; - const hrp = getHumanReadablePart(args[0]); + // We hash the input + const curInput = makeSureIsBytes(input); + const hash160 = toHex(hash160Func(curInput)); + // We do segwit addresses first. + if (args[1] === "Segwit (P2WPKH bc1 Addresses)") { + const redeemScript = hash160; + const hrp = getHumanReadablePart(args[0]); + if (hrp !== "") { return encodeProgramToSegwit(hrp, 0, Utils.convertToByteArray(redeemScript, "hex")); - } - // It its not segwit, we create the redeemScript either for P2PKH or P2SH-P2WPKH addresses. - const versionByte = "P2PKH (V1 BTC Addresses)" === args[1] ? getP2PKHVersionByte(args[0]) : getP2SHVersionByte(args[0]); - // If its a P2SH-P2WPKH address, we have to prepend some extra bytes and hash again. Either way we prepend the version byte. - let hashRedeemedScript; - if (args[1] === "P2SH-P2PWPKH (Segwit Compatible)") { - const redeemScript = "0014" + hash160; - hashRedeemedScript = versionByte + toHex(hash160Func(fromArrayBuffer(Utils.convertToByteArray(redeemScript, "hex")))); } else { - hashRedeemedScript = versionByte + hash160; + return args[0] + " does not support Segwit Addresses."; } - - // We calculate the checksum, convert to Base58 and then we're done! - const checksumHash = toHex(doubleSHA(fromArrayBuffer(Utils.convertToByteArray(hashRedeemedScript, "hex")))); - const finalString = hashRedeemedScript + checksumHash.slice(0, 8); - const address = base58Encode(Utils.convertToByteArray(finalString, "hex")); - return address; } + // It its not segwit, we create the redeemScript either for P2PKH or P2SH-P2WPKH addresses. + const versionByte = "P2PKH (V1 BTC Addresses)" === args[1] ? getP2PKHVersionByte(args[0]) : getP2SHVersionByte(args[0]); + // If its a P2SH-P2WPKH address, we have to prepend some extra bytes and hash again. Either way we prepend the version byte. + let hashRedeemedScript; + if (args[1] === "P2SH-P2PWPKH (Segwit Compatible V3 Addresses)") { + const redeemScript = "0014" + hash160; + hashRedeemedScript = versionByte + toHex(hash160Func(fromArrayBuffer(Utils.convertToByteArray(redeemScript, "hex")))); + } else { + hashRedeemedScript = versionByte + hash160; + } + + // We calculate the checksum, convert to Base58 and then we're done! + const checksumHash = toHex(doubleSHA(fromArrayBuffer(Utils.convertToByteArray(hashRedeemedScript, "hex")))); + const finalString = hashRedeemedScript + checksumHash.slice(0, 8); + const address = base58Encode(Utils.convertToByteArray(finalString, "hex")); + return address; } diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index a454e5e6..1bafde6b 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -129,6 +129,7 @@ import "./tests/DecryptKeyStoreFile.mjs"; import "./tests/WIFToPrivateKey.mjs"; import "./tests/SeedphraseToSeed.mjs"; import "./tests/DeserializeExtendedKey.mjs"; +import "./tests/PublicKeyToETHStyleAddress.mjs"; import "./tests/GetAllCasings.mjs"; import "./tests/SIGABA.mjs"; import "./tests/ELFInfo.mjs"; diff --git a/tests/operations/tests/DecryptKeyStoreFile.mjs b/tests/operations/tests/DecryptKeyStoreFile.mjs index 9c642b98..29c93fdb 100644 --- a/tests/operations/tests/DecryptKeyStoreFile.mjs +++ b/tests/operations/tests/DecryptKeyStoreFile.mjs @@ -34,8 +34,8 @@ TestRegister.addTests([ "args": [false] }, { - "op": "Public Key To Cryptocurrency Address", - "args": ["Ethereum", "P2PKH (V1 BTC Addresses)"] + "op": "Public Key To ETH Style Address", + "args": [] } ] diff --git a/tests/operations/tests/PrivateECKeyToPublic.mjs b/tests/operations/tests/PrivateECKeyToPublic.mjs index b60f89e5..5cddebfc 100644 --- a/tests/operations/tests/PrivateECKeyToPublic.mjs +++ b/tests/operations/tests/PrivateECKeyToPublic.mjs @@ -50,7 +50,7 @@ TestRegister.addTests([ { name: "Private EC Key to Public (Wrong Length)", input: "5E2A8FDE9F861056607208F512287CFBD634E124044EE23EBF7289E8E7B3822E08", - expectedOutput: "Must pass a hex string of length 64, or a byte string of length 32. Got length 66", + expectedOutput: "Error with the input as private key. Error is:\n\tInvalid length. We want either 32 or 64 but we got: 66", recipeConfig: [ { "op": "Private EC Key to Public Key", @@ -61,7 +61,7 @@ TestRegister.addTests([ { name: "Private EC Key to Public (From Bytes Uncompressed Wrong Length)", input: "5E2A8FDE9F861056607208F512287CFBD634E124044EE23EBF7289E8E7B3822E08", - expectedOutput: "Must pass a hex string of length 64, or a byte string of length 32. Got length 33", + expectedOutput: "Error with the input as private key. Error is:\n\tInvalid length. We want either 32 or 64 but we got: 33", recipeConfig: [ { "op": "From Hex", diff --git a/tests/operations/tests/PrivateKeyToWIF.mjs b/tests/operations/tests/PrivateKeyToWIF.mjs index 4d6832f2..3e78dbbd 100644 --- a/tests/operations/tests/PrivateKeyToWIF.mjs +++ b/tests/operations/tests/PrivateKeyToWIF.mjs @@ -61,7 +61,7 @@ TestRegister.addTests([ { name: "Private Key To WIF (BTC, Compressed, Wrong Number of Bytes)", input: "5E2A8FDE9F861056607208F512287CFBD634E124044EE23EBF7289E8E7B3822E08", - expectedOutput: "Must pass a hex string of length 64, or a byte string of length 32. Got length: 66", + expectedOutput: "Error parsing private key. Error is:\n\tInvalid length. We want either 32 or 64 but we got: 66", recipeConfig: [ { "op": "To WIF Format", @@ -72,7 +72,7 @@ TestRegister.addTests([ { name: "Private Key To WIF (BTC, Compressed, Wrong Number of Bytes)", input: "5E2A8FDE9F861056607208F512287CFBD634E124044EE23EBF7289E8E7B3822E08", - expectedOutput: "Must pass a hex string of length 64, or a byte string of length 32. Got length: 33", + expectedOutput: "Error parsing private key. Error is:\n\tInvalid length. We want either 32 or 64 but we got: 33", recipeConfig: [ { "op": "From Hex", diff --git a/tests/operations/tests/PublicKeyToETHStyleAddress.mjs b/tests/operations/tests/PublicKeyToETHStyleAddress.mjs new file mode 100644 index 00000000..81c509c2 --- /dev/null +++ b/tests/operations/tests/PublicKeyToETHStyleAddress.mjs @@ -0,0 +1,110 @@ +/** + * Public Key to ETH Style Address Cryptocurrency Address tests. + * + * @author dgoldenberg [virtualcurrency@mitre.org] + * @copyright MITRE 2023 + * @license Apache-2.0 + */ + +import TestRegister from "../../lib/TestRegister.mjs"; + + +TestRegister.addTests([ + { + name: "Public Key To ETH Style Address", + input: "04d26bcecd763bdf6bdb89ba929d2485429fbda73bae723d525ef55554ef45350582085bd24055079f6deebad5b6af612c14587c6862391d330484afe750fbf144", + expectedOutput: "0x63e8b85679d29235791a0f558d6485c7ed51c9e6", + recipeConfig: [ + { + "op": "Public Key To ETH Style Address", + "args": [] + }, + ], + + }, + { + name: "Public Key To ETH Style Address 2", + input: "047d3e5107b46421c3ddf7292fcbe1f5805952279926c325d119f6ddfed1e5e7ea9a9f5dceadc57b897cb286479450b66a6422f8f270bcd2a61ab4eea800911956", + expectedOutput: "0x099f75d5bc069026531394d5c6d6c37a41158d31", + recipeConfig: [ + { + "op": "Public Key To ETH Style Address", + "args": [] + }, + ], + + }, + { + name: "Public Key to ETH Style Address: Compressed Key", + input: "027d3e5107b46421c3ddf7292fcbe1f5805952279926c325d119f6ddfed1e5e7ea", + expectedOutput: "0x099f75d5bc069026531394d5c6d6c37a41158d31", + recipeConfig: [ + { + "op": "Public Key To ETH Style Address", + "args": [] + }, + ], + + }, + { + name: "Public Key to ETH Style Address: Compressed Key 2", + input: "039d491d3f5a2a79b422b76b8115b175b38e42c6103b1a84c88fb6221fe9072bbe", + expectedOutput: "0x345029d286159d516ca1169edf0a80ff6c855ac0", + recipeConfig: [ + { + "op": "Public Key To ETH Style Address", + "args": [], + }, + ], + + }, + { + name: "Public Key to ETH Style Address: Compressed Key (From Hex)", + input: "027d3e5107b46421c3ddf7292fcbe1f5805952279926c325d119f6ddfed1e5e7ea", + expectedOutput: "0x099f75d5bc069026531394d5c6d6c37a41158d31", + recipeConfig: [ + { + "op": "From Hex", + "args": ["Auto"] + }, + { + "op": "Public Key To ETH Style Address", + "args": [] + }, + ], + + }, + { + name: "Public Key to ETH Style Address: Compressed Key 2 (From Hex)", + input: "039d491d3f5a2a79b422b76b8115b175b38e42c6103b1a84c88fb6221fe9072bbe", + expectedOutput: "0x345029d286159d516ca1169edf0a80ff6c855ac0", + recipeConfig: [ + { + "op": "From Hex", + "args": ["Auto"] + }, + { + "op": "Public Key To ETH Style Address", + "args": [], + }, + ], + + }, + { + name: "Public Key To ETH Style Address (From Hex)", + input: "04d26bcecd763bdf6bdb89ba929d2485429fbda73bae723d525ef55554ef45350582085bd24055079f6deebad5b6af612c14587c6862391d330484afe750fbf144", + expectedOutput: "0x63e8b85679d29235791a0f558d6485c7ed51c9e6", + recipeConfig: [ + { + "op": "From Hex", + "args": ["Auto"] + }, + { + "op": "Public Key To ETH Style Address", + "args": [] + }, + ], + + }, + +]); diff --git a/tests/operations/tests/PublicKeyToP2PKHAddress.mjs b/tests/operations/tests/PublicKeyToP2PKHAddress.mjs index d06614fa..5a0cb7b5 100644 --- a/tests/operations/tests/PublicKeyToP2PKHAddress.mjs +++ b/tests/operations/tests/PublicKeyToP2PKHAddress.mjs @@ -32,6 +32,17 @@ TestRegister.addTests([ }, ], }, + { + name: "Public Key To Address: P2PKH LTC", + input: "037b39f764a10f31bfd47038738ca27bffeefce1fe4ccbfb9343fcb69d9363b27b", + expectedOutput: "LPTR2TBuF8vbwWaJdNeCAQemW4SC7q7zJP", + recipeConfig: [ + { + "op": "Public Key To Cryptocurrency Address", + "args": ["LTC", "P2PKH (V1 BTC Addresses)"] + }, + ], + }, { name: "Public Key To Address: P2PKH (Long)", input: "04219A19E157B5FEDDF7EBDD3C7A58D7AB4F6565E84226691B6A5F80BBCE8E0100B49D6AB503CA4B701626E941EB8D2460F154992D7AD4EC671CF1CFB8C1DE8164", @@ -50,7 +61,7 @@ TestRegister.addTests([ recipeConfig: [ { "op": "Public Key To Cryptocurrency Address", - "args": ["BTC", "P2SH-P2PWPKH (Segwit Compatible)"] + "args": ["BTC", "P2SH-P2PWPKH (Segwit Compatible V3 Addresses)"] }, ], }, @@ -61,7 +72,18 @@ TestRegister.addTests([ recipeConfig: [ { "op": "Public Key To Cryptocurrency Address", - "args": ["BTC", "P2SH-P2PWPKH (Segwit Compatible)"] + "args": ["BTC", "P2SH-P2PWPKH (Segwit Compatible V3 Addresses)"] + }, + ], + }, + { + name: "Public Key To Address: P2SH-P2WPKH LTC", + input: "02f442a169ca36702bbcbb268319295bece8fe1cbc6ca095b2669d13ef56c759de", + expectedOutput: "MMwYiJmkxDKqiP2WWAHMMgkeRt2nLxGqih", + recipeConfig: [ + { + "op": "Public Key To Cryptocurrency Address", + "args": ["LTC", "P2SH-P2PWPKH (Segwit Compatible V3 Addresses)"] }, ], }, @@ -72,7 +94,7 @@ TestRegister.addTests([ recipeConfig: [ { "op": "Public Key To Cryptocurrency Address", - "args": ["BTC", "Segwit (P2WPKH)"] + "args": ["BTC", "Segwit (P2WPKH bc1 Addresses)"] }, ], }, @@ -83,21 +105,20 @@ TestRegister.addTests([ recipeConfig: [ { "op": "Public Key To Cryptocurrency Address", - "args": ["BTC", "Segwit (P2WPKH)"] + "args": ["BTC", "Segwit (P2WPKH bc1 Addresses)"] }, ], }, { - name: "Public Key To Address: (ETH)", - input: "04d26bcecd763bdf6bdb89ba929d2485429fbda73bae723d525ef55554ef45350582085bd24055079f6deebad5b6af612c14587c6862391d330484afe750fbf144", - expectedOutput: "0x63e8b85679d29235791a0f558d6485c7ed51c9e6", + name: "Public Key To Address: P2WPKH LTC", + input: "026a532c31184b94edf540ad60c3cc208342be4f51cc764a373a420731dd198a59", + expectedOutput: "ltc1qj587punda8h0r4m83k794xseqlnl3az4ktu2zp", recipeConfig: [ { "op": "Public Key To Cryptocurrency Address", - "args": ["Ethereum", "Segwit (P2WPKH)"] + "args": ["LTC", "Segwit (P2WPKH bc1 Addresses)"] }, ], - }, { name: "Public Key To Address: (Testnet)", @@ -114,29 +135,29 @@ TestRegister.addTests([ { name: "Public Key To Address: P2WPKH (Wrong Length)", input: "03bc32bdc5dc96c9fb56e2481fefd321ebe9e17a807bbb337dea1df5e68b1f075642", - expectedOutput: "Input is wrong length. Should be either 33 or 65 bytes, but is: 34", + expectedOutput: "Invalid length. We want either 33, 65 (if bytes) or 66, 130 (if hex) but we got: 68", recipeConfig: [ { "op": "Public Key To Cryptocurrency Address", - "args": ["BTC", "Segwit (P2WPKH)"] + "args": ["BTC", "Segwit (P2WPKH bc1 Addresses)"] }, ], }, { name: "Public Key To Address: P2WPKH (Wrong Start)", input: "05bc32bdc5dc96c9fb56e2481fefd321ebe9e17a807bbb337dea1df5e68b1f0756", - expectedOutput: "Input is 33 bytes, but begins with invalid byte: 05", + expectedOutput: "We have a valid hex string, of reasonable length, (66) but doesn't start with the right value. Correct values are 02, or 03 but we have: 05", recipeConfig: [ { "op": "Public Key To Cryptocurrency Address", - "args": ["BTC", "Segwit (P2WPKH)"] + "args": ["BTC", "Segwit (P2WPKH bc1 Addresses)"] }, ], }, { name: "Public Key To Address: P2PKH (Long With Error)", input: "06219A19E157B5FEDDF7EBDD3C7A58D7AB4F6565E84226691B6A5F80BBCE8E0100B49D6AB503CA4B701626E941EB8D2460F154992D7AD4EC671CF1CFB8C1DE8164", - expectedOutput: "Input is 65 bytes, but begins with invalid byte: 06", + expectedOutput: "We have a valid hex string of reasonable length, (130) but doesn't start with the right value. Correct values are 04 but we have: 06", recipeConfig: [ { "op": "Public Key To Cryptocurrency Address", @@ -159,16 +180,4 @@ TestRegister.addTests([ } ], }, - { - name: "Public Key To Address: (ETH Compressed Key)", - input: "03ebf60a619da2fbc6239089ca0a93878ea53baa3d22188cacad4033b103237ae9", - expectedOutput: "Ethereum addresses require uncompressed public keys.", - recipeConfig: [ - { - "op": "Public Key To Cryptocurrency Address", - "args": ["Ethereum", "Segwit (P2WPKH)"] - }, - ], - - } ]); diff --git a/tests/operations/tests/SeedToMPK.mjs b/tests/operations/tests/SeedToMPK.mjs index e1dfc246..00e3e586 100644 --- a/tests/operations/tests/SeedToMPK.mjs +++ b/tests/operations/tests/SeedToMPK.mjs @@ -21,6 +21,21 @@ TestRegister.addTests([ } ], }, + { + name: "Seed To Master Private Key (xprv) From Hex", + input: "c766f48d3729a16249b5d0171c678d458d31454b2bb7791b61169b5541a130719714ebd41f22a2515246d013e9a4e978aee48dd5140b73a540108d58008c4aa9", + expectedOutput: "xprv9s21ZrQH143K2nwujwREGif1wyBwt5Jh9BdFVSgSeYdrUp1qPxKsHrmnpJ8xKpKPDvXJMmBRpsZ3X64MeafyURs8Xoj53kGu7hb48Yg7unj", + recipeConfig: [ + { + "op": "From Hex", + "args": ["Auto"] + }, + { + "op": "Seed To Master Key", + "args": ["xprv"] + } + ], + }, { name: "Seed To Master Private Key (tprv)", input: "c766f48d3729a16249b5d0171c678d458d31454b2bb7791b61169b5541a130719714ebd41f22a2515246d013e9a4e978aee48dd5140b73a540108d58008c4aa9",