From 1867afe2a458d2f74cc7842ada446416cbad3072 Mon Sep 17 00:00:00 2001 From: yknl Date: Mon, 10 Aug 2020 23:58:45 -0400 Subject: [PATCH] feat: create storage package --- packages/encryption/README.md | 11 + .../encryption/__tests__/encryption.test.js | 7 + packages/encryption/package-lock.json | 377 ++++++++++++++++ packages/encryption/package.json | 37 ++ packages/encryption/src/aesCipher.ts | 128 ++++++ packages/encryption/src/cryptoRandom.ts | 7 + packages/encryption/src/cryptoUtils.ts | 69 +++ packages/encryption/src/ec.ts | 414 ++++++++++++++++++ packages/encryption/src/encryption.ts | 130 ++++++ packages/encryption/src/hashRipemd160.ts | 63 +++ packages/encryption/src/hmacSha256.ts | 53 +++ packages/encryption/src/index.ts | 14 + packages/encryption/src/keys.ts | 94 ++++ packages/encryption/src/pbkdf2.ts | 159 +++++++ packages/encryption/src/sha2Hash.ts | 77 ++++ packages/encryption/src/utils.ts | 21 + packages/encryption/src/wallet.ts | 182 ++++++++ packages/encryption/tsconfig.build.json | 14 + 18 files changed, 1857 insertions(+) create mode 100644 packages/encryption/README.md create mode 100644 packages/encryption/__tests__/encryption.test.js create mode 100644 packages/encryption/package-lock.json create mode 100644 packages/encryption/package.json create mode 100644 packages/encryption/src/aesCipher.ts create mode 100644 packages/encryption/src/cryptoRandom.ts create mode 100644 packages/encryption/src/cryptoUtils.ts create mode 100644 packages/encryption/src/ec.ts create mode 100644 packages/encryption/src/encryption.ts create mode 100644 packages/encryption/src/hashRipemd160.ts create mode 100644 packages/encryption/src/hmacSha256.ts create mode 100644 packages/encryption/src/index.ts create mode 100644 packages/encryption/src/keys.ts create mode 100644 packages/encryption/src/pbkdf2.ts create mode 100644 packages/encryption/src/sha2Hash.ts create mode 100644 packages/encryption/src/utils.ts create mode 100644 packages/encryption/src/wallet.ts create mode 100644 packages/encryption/tsconfig.build.json diff --git a/packages/encryption/README.md b/packages/encryption/README.md new file mode 100644 index 000000000..b0c75f4c3 --- /dev/null +++ b/packages/encryption/README.md @@ -0,0 +1,11 @@ +# `encryption` + +> TODO: description + +## Usage + +``` +const encryption = require('encryption'); + +// TODO: DEMONSTRATE API +``` diff --git a/packages/encryption/__tests__/encryption.test.js b/packages/encryption/__tests__/encryption.test.js new file mode 100644 index 000000000..0f3bb61b8 --- /dev/null +++ b/packages/encryption/__tests__/encryption.test.js @@ -0,0 +1,7 @@ +'use strict'; + +const encryption = require('..'); + +describe('encryption', () => { + it('needs tests'); +}); diff --git a/packages/encryption/package-lock.json b/packages/encryption/package-lock.json new file mode 100644 index 000000000..7e774804d --- /dev/null +++ b/packages/encryption/package-lock.json @@ -0,0 +1,377 @@ +{ + "name": "@stacks/encryption", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/node": { + "version": "11.11.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", + "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==" + }, + "base-x": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.8.tgz", + "integrity": "sha512-Rl/1AWP4J/zRrk54hhlxH4drNxPJXYUaKffODVI53/dAsV4t9fBxyxYKAVPU1XBHxYwOWP9h9H0hM2MVw4YfJA==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==" + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bip174": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bip174/-/bip174-1.0.1.tgz", + "integrity": "sha512-Mq2aFs1TdMfxBpYPg7uzjhsiXbAtoVq44TNjEWtvuZBiBgc3m7+n55orYMtTAxdg7jWbL4DtH0MKocJER4xERQ==" + }, + "bip32": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/bip32/-/bip32-2.0.5.tgz", + "integrity": "sha512-zVY4VvJV+b2fS0/dcap/5XLlpqtgwyN8oRkuGgAS1uLOeEp0Yo6Tw2yUTozTtlrMJO3G8n4g/KX/XGFHW6Pq3g==", + "requires": { + "@types/node": "10.12.18", + "bs58check": "^2.1.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "tiny-secp256k1": "^1.1.3", + "typeforce": "^1.11.5", + "wif": "^2.0.6" + }, + "dependencies": { + "@types/node": { + "version": "10.12.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.18.tgz", + "integrity": "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==" + } + } + }, + "bip39": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.2.tgz", + "integrity": "sha512-J4E1r2N0tUylTKt07ibXvhpT2c5pyAFgvuA5q1H9uDy6dEGpjV8jmymh3MTYJDLCNbIVClSB9FbND49I6N24MQ==", + "requires": { + "@types/node": "11.11.6", + "create-hash": "^1.1.0", + "pbkdf2": "^3.0.9", + "randombytes": "^2.0.1" + } + }, + "bip66": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", + "integrity": "sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "bitcoin-ops": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/bitcoin-ops/-/bitcoin-ops-1.4.1.tgz", + "integrity": "sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow==" + }, + "bitcoinjs-lib": { + "version": "5.1.10", + "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-5.1.10.tgz", + "integrity": "sha512-CesUqtBtnYc+SOMsYN9jWQWhdohW1MpklUkF7Ukn4HiAyN6yxykG+cIJogfRt6x5xcgH87K1Q+Mnoe/B+du1Iw==", + "requires": { + "bech32": "^1.1.2", + "bip174": "^1.0.1", + "bip32": "^2.0.4", + "bip66": "^1.1.0", + "bitcoin-ops": "^1.4.0", + "bs58check": "^2.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.3", + "merkle-lib": "^2.0.10", + "pushdata-bitcoin": "^1.0.1", + "randombytes": "^2.0.1", + "tiny-secp256k1": "^1.1.1", + "typeforce": "^1.11.3", + "varuint-bitcoin": "^1.0.4", + "wif": "^2.0.1" + } + }, + "bn.js": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.2.tgz", + "integrity": "sha512-40rZaf3bUNKTVYu9sIeeEGOg7g14Yvnj9kH7b50EiwX0Q7A6umbvfI5tvHaOERH0XigqKkfLkFQxzb4e6CIXnA==" + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" + }, + "bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=", + "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" + } + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "elliptic": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", + "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + } + } + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "requires": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "merkle-lib": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/merkle-lib/-/merkle-lib-2.0.10.tgz", + "integrity": "sha1-grjbrnXieneFOItz+ddyXQ9vMyY=" + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" + }, + "nan": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", + "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==" + }, + "pbkdf2": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz", + "integrity": "sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==", + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "pushdata-bitcoin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pushdata-bitcoin/-/pushdata-bitcoin-1.0.1.tgz", + "integrity": "sha1-FZMdPNlnreUiBvUjqnMxrvfUOvc=", + "requires": { + "bitcoin-ops": "^1.3.0" + } + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "ripemd160-min": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/ripemd160-min/-/ripemd160-min-0.0.6.tgz", + "integrity": "sha512-+GcJgQivhs6S9qvLogusiTcS9kQUfgR75whKuy5jIhuiOfQuJ8fjqxV6EGD5duH1Y/FawFUMtMhyeq3Fbnib8A==" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "tiny-secp256k1": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-1.1.5.tgz", + "integrity": "sha512-duE2hSLSQIpHGzmK48OgRrGTi+4OTkXLC6aa86uOYQ6LLCYZSarVKIAvEtY7MoXjoL6bOXMSerEGMzrvW4SkDw==", + "requires": { + "bindings": "^1.3.0", + "bn.js": "^4.11.8", + "create-hmac": "^1.1.7", + "elliptic": "^6.4.0", + "nan": "^2.13.2" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + } + } + }, + "typeforce": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", + "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "varuint-bitcoin": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-1.1.2.tgz", + "integrity": "sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw==", + "requires": { + "safe-buffer": "^5.1.1" + } + }, + "wif": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz", + "integrity": "sha1-CNP1IFbGZnkplyb63g1DKudLRwQ=", + "requires": { + "bs58check": "<3.0.0" + } + } + } +} diff --git a/packages/encryption/package.json b/packages/encryption/package.json new file mode 100644 index 000000000..b30eab578 --- /dev/null +++ b/packages/encryption/package.json @@ -0,0 +1,37 @@ +{ + "name": "@stacks/encryption", + "version": "1.0.0", + "description": "Encryption utilities for Stacks", + "author": "yknl ", + "homepage": "https://blockstack.org", + "license": "GPL-3.0-or-later", + "main": "lib/index.js", + "directories": { + "lib": "lib", + "test": "__tests__" + }, + "files": [ + "lib" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/blockstack/blockstack.js.git" + }, + "scripts": { + "build": "rimraf lib && tsc -b tsconfig.build.json", + "test": "echo \"Error: run tests from root\" && exit 1" + }, + "bugs": { + "url": "https://github.com/blockstack/blockstack.js/issues" + }, + "dependencies": { + "@stacks/common": "^1.0.0", + "bip39": "^3.0.2", + "bitcoinjs-lib": "^5.1.10", + "bn.js": "^5.1.2", + "elliptic": "^6.5.3", + "randombytes": "^2.1.0", + "ripemd160-min": "^0.0.6", + "sha.js": "^2.4.11" + } +} diff --git a/packages/encryption/src/aesCipher.ts b/packages/encryption/src/aesCipher.ts new file mode 100644 index 000000000..f62af0f35 --- /dev/null +++ b/packages/encryption/src/aesCipher.ts @@ -0,0 +1,128 @@ +import { getCryptoLib } from './cryptoUtils' + +type NodeCryptoCreateCipher = typeof import('crypto').createCipheriv; +type NodeCryptoCreateDecipher = typeof import('crypto').createDecipheriv; + +export type CipherAlgorithm = 'aes-256-cbc' | 'aes-128-cbc'; + +export interface AesCipher { + encrypt( + algorithm: CipherAlgorithm, + key: Buffer, + iv: Buffer, + data: Buffer): Promise; + + decrypt( + algorithm: CipherAlgorithm, + key: Buffer, + iv: Buffer, + data: Buffer): Promise; +} + +export class NodeCryptoAesCipher implements AesCipher { + createCipher: NodeCryptoCreateCipher + + createDecipher: NodeCryptoCreateDecipher + + constructor(createCipher: NodeCryptoCreateCipher, createDecipher: NodeCryptoCreateDecipher) { + this.createCipher = createCipher + this.createDecipher = createDecipher + } + + async encrypt( + algorithm: CipherAlgorithm, + key: Buffer, + iv: Buffer, + data: Buffer): + Promise { + if (algorithm !== 'aes-128-cbc' && algorithm !== 'aes-256-cbc') { + throw new Error(`Unsupported cipher algorithm "${algorithm}"`) + } + const cipher = this.createCipher(algorithm, key, iv) + const result = Buffer.concat([cipher.update(data), cipher.final()]) + return Promise.resolve(result) + } + + async decrypt( + algorithm: CipherAlgorithm, + key: Buffer, + iv: Buffer, + data: Buffer): + Promise { + if (algorithm !== 'aes-128-cbc' && algorithm !== 'aes-256-cbc') { + throw new Error(`Unsupported cipher algorithm "${algorithm}"`) + } + const cipher = this.createDecipher(algorithm, key, iv) + const result = Buffer.concat([cipher.update(data), cipher.final()]) + return Promise.resolve(result) + } +} + +export class WebCryptoAesCipher implements AesCipher { + subtleCrypto: SubtleCrypto + + constructor(subtleCrypto: SubtleCrypto) { + this.subtleCrypto = subtleCrypto + } + + async encrypt( + algorithm: CipherAlgorithm, + key: Buffer, + iv: Buffer, + data: Buffer): + Promise { + let algo: string + let length: number + if (algorithm === 'aes-128-cbc') { + algo = 'AES-CBC' + length = 128 + } else if (algorithm === 'aes-256-cbc') { + algo = 'AES-CBC' + length = 256 + } else { + throw new Error(`Unsupported cipher algorithm "${algorithm}"`) + } + const cryptoKey = await this.subtleCrypto.importKey( + 'raw', key, + { name: algo, length }, + false, ['encrypt'] + ) + const result = await this.subtleCrypto.encrypt({ name: algo, iv }, cryptoKey, data) + return Buffer.from(result) + } + + async decrypt( + algorithm: CipherAlgorithm, + key: Buffer, + iv: Buffer, + data: Buffer): + Promise { + let algo: string + let length: number + if (algorithm === 'aes-128-cbc') { + algo = 'AES-CBC' + length = 128 + } else if (algorithm === 'aes-256-cbc') { + algo = 'AES-CBC' + length = 256 + } else { + throw new Error(`Unsupported cipher algorithm "${algorithm}"`) + } + const cryptoKey = await this.subtleCrypto.importKey( + 'raw', key, + { name: algo, length }, + false, ['decrypt'] + ) + const result = await this.subtleCrypto.decrypt({ name: algo, iv }, cryptoKey, data) + return Buffer.from(result) + } +} + +export async function createCipher(): Promise { + const cryptoLib = await getCryptoLib() + if (cryptoLib.name === 'subtleCrypto') { + return new WebCryptoAesCipher(cryptoLib.lib) + } else { + return new NodeCryptoAesCipher(cryptoLib.lib.createCipheriv, cryptoLib.lib.createDecipheriv) + } +} diff --git a/packages/encryption/src/cryptoRandom.ts b/packages/encryption/src/cryptoRandom.ts new file mode 100644 index 000000000..0ea6300db --- /dev/null +++ b/packages/encryption/src/cryptoRandom.ts @@ -0,0 +1,7 @@ +import * as randombytes from 'randombytes' + +export { randombytes as randomBytes } + +/** Optional function to generate cryptographically secure random bytes */ +export type GetRandomBytes = (count: number) => Buffer + diff --git a/packages/encryption/src/cryptoUtils.ts b/packages/encryption/src/cryptoUtils.ts new file mode 100644 index 000000000..1dfd7e031 --- /dev/null +++ b/packages/encryption/src/cryptoUtils.ts @@ -0,0 +1,69 @@ + +export function isSubtleCryptoAvailable(): boolean { + return typeof crypto !== 'undefined' && typeof crypto.subtle !== 'undefined' +} + +export function isNodeCryptoAvailable( + withFeature: (nodeCrypto: typeof import('crypto')) => boolean | T +): false | T +export function isNodeCryptoAvailable( + withFeature?: (nodeCrypto: typeof import('crypto')) => boolean | T +): boolean | T { + try { + const resolvedResult = require.resolve('crypto') + if (!resolvedResult) { + return false + } + // eslint-disable-next-line import/no-nodejs-modules,no-restricted-modules,global-require + const cryptoModule = require('crypto') as typeof import('crypto') + if (!cryptoModule) { + return false + } + if (withFeature) { + const features = withFeature(cryptoModule) + return features + } + return true + } catch (error) { + return false + } +} + +export const NO_CRYPTO_LIB = 'Crypto lib not found. Either the WebCrypto "crypto.subtle" or Node.js "crypto" module must be available.' + +export type TriplesecDecryptSignature = ( + arg: { data: Buffer; key: Buffer }, cb: (err: Error | null, buff: Buffer | null) => void +) => void + +export interface WebCryptoLib { + lib: SubtleCrypto; + name: 'subtleCrypto' +} + +export interface NodeCryptoLib { + lib: typeof import('crypto'); + name: 'nodeCrypto' +} + +// Make async for future version which may lazy load. +// eslint-disable-next-line @typescript-eslint/require-await +export async function getCryptoLib(): Promise { + if (isSubtleCryptoAvailable()) { + return { + lib: crypto.subtle, + name: 'subtleCrypto' + } + } else { + try { + // eslint-disable-next-line max-len + // eslint-disable-next-line import/no-nodejs-modules,no-restricted-modules,global-require,@typescript-eslint/no-var-requires + const nodeCrypto = require('crypto') as typeof import('crypto') + return { + lib: nodeCrypto, + name: 'nodeCrypto' + } + } catch (error) { + throw new Error(NO_CRYPTO_LIB) + } + } +} diff --git a/packages/encryption/src/ec.ts b/packages/encryption/src/ec.ts new file mode 100644 index 000000000..955318695 --- /dev/null +++ b/packages/encryption/src/ec.ts @@ -0,0 +1,414 @@ +import { ec as EllipticCurve } from 'elliptic' +import * as BN from 'bn.js' +import { randomBytes } from './cryptoRandom' +import { FailedDecryptionError } from '@stacks/common' +import { getPublicKeyFromPrivate } from './keys' +import { hashSha256Sync, hashSha512Sync } from './sha2Hash' +import { createHmacSha256 } from './hmacSha256' +import { createCipher } from './aesCipher' +import { getAesCbcOutputLength, getBase64OutputLength } from './utils' + +const ecurve = new EllipticCurve('secp256k1') + +/** + * Controls how the encrypted data buffer will be encoded as a string in the JSON payload. + * Options: + * `hex` -- the legacy default, file size increase 100% (2x). + * `base64` -- file size increased ~33%. + * @ignore + */ +export type CipherTextEncoding = 'hex' | 'base64' + +/** + * @ignore + */ +export type CipherObject = { + iv: string, + ephemeralPK: string, + cipherText: string, + /** If undefined then hex encoding is used for the `cipherText` string. */ + cipherTextEncoding?: CipherTextEncoding, + mac: string, + wasString: boolean +} + +/** + * @ignore + */ +export type SignedCipherObject = { + /** Hex encoded DER signature (up to 144 chars) */ + signature: string, + /** Hex encoded public key (66 char length) */ + publicKey: string, + /** The stringified json of a `CipherObject` */ + cipherText: string +} + +/** +* @ignore +*/ +export async function aes256CbcEncrypt(iv: Buffer, + key: Buffer, + plaintext: Buffer +): Promise { + const cipher = await createCipher() + const result = await cipher.encrypt('aes-256-cbc', key, iv, plaintext) + return result +} + +/** +* @ignore +*/ +async function aes256CbcDecrypt(iv: Buffer, key: Buffer, ciphertext: Buffer): Promise { + const cipher = await createCipher() + const result = await cipher.decrypt('aes-256-cbc', key, iv, ciphertext) + return result +} + +/** +* @ignore +*/ +async function hmacSha256(key: Buffer, content: Buffer) { + const hmacSha256 = await createHmacSha256() + return hmacSha256.digest(key, content) +} + +/** +* @ignore +*/ +function equalConstTime(b1: Buffer, b2: Buffer) { + if (b1.length !== b2.length) { + return false + } + let res = 0 + for (let i = 0; i < b1.length; i++) { + res |= b1[i] ^ b2[i] // jshint ignore:line + } + return res === 0 +} + +/** +* @ignore +*/ +function sharedSecretToKeys(sharedSecret: Buffer): { encryptionKey: Buffer; hmacKey: Buffer; } { + // generate mac and encryption key from shared secret + const hashedSecret = hashSha512Sync(sharedSecret) + return { + encryptionKey: hashedSecret.slice(0, 32), + hmacKey: hashedSecret.slice(32) + } +} + +/** + * Hex encodes a 32-byte BN.js instance. + * The result string is zero padded and always 64 characters in length. + * @ignore + */ +export function getHexFromBN(bnInput: BN): string { + const hexOut = bnInput.toString('hex', 64) + if (hexOut.length === 64) { + return hexOut + } else if (hexOut.length < 64) { + // pad with leading zeros + // the padStart function would require node 9 + const padding = '0'.repeat(64 - hexOut.length) + return `${padding}${hexOut}` + } else { + throw new Error('Generated a > 32-byte BN for encryption. Failing.') + } +} + +/** + * Returns a big-endian encoded 32-byte BN.js instance. + * The result Buffer is zero padded and always 32 bytes in length. + * @ignore + */ +export function getBufferFromBN(bnInput: BN): Buffer { + const result = bnInput.toArrayLike(Buffer, 'be', 32) + if (result.byteLength !== 32) { + throw new Error('Generated a 32-byte BN for encryption. Failing.') + } + return result +} + +/** + * Get details about the JSON envelope size overhead for ciphertext payloads. + * @ignore + */ +export function getCipherObjectWrapper(opts: { + wasString: boolean, + cipherTextEncoding: CipherTextEncoding, +}): { + /** The stringified JSON string of an empty `CipherObject`. */ + payloadShell: string, + /** Total string length of all the `CipherObject` values that always have constant lengths. */ + payloadValuesLength: number, + } { + // Placeholder structure of the ciphertext payload, used to determine the + // stringified JSON overhead length. + const shell: CipherObject = { + iv: '', + ephemeralPK: '', + mac: '', + cipherText: '', + wasString: !!opts.wasString, + } + if (opts.cipherTextEncoding === 'base64') { + shell.cipherTextEncoding = 'base64' + } + // Hex encoded 16 byte buffer. + const ivLength = 32 + // Hex encoded, compressed EC pubkey of 33 bytes. + const ephemeralPKLength = 66 + // Hex encoded 32 byte hmac-sha256. + const macLength = 64 + return { + payloadValuesLength: ivLength + ephemeralPKLength + macLength, + payloadShell: JSON.stringify(shell) + } +} + +/** + * Get details about the JSON envelope size overhead for signed ciphertext payloads. + * @param payloadShell - The JSON stringified empty `CipherObject` + * @ignore + */ +export function getSignedCipherObjectWrapper(payloadShell: string): { + /** The stringified JSON string of an empty `SignedCipherObject`. */ + signedPayloadValuesLength: number; + /** Total string length of all the `SignedCipherObject` values + * that always have constant lengths */ + signedPayloadShell: string; +} { + // Placeholder structure of the signed ciphertext payload, used to determine the + // stringified JSON overhead length. + const shell: SignedCipherObject = { + signature: '', + publicKey: '', + cipherText: payloadShell + } + // Hex encoded DER signature, up to 72 byte length. + const signatureLength = 144 + // Hex encoded 33 byte public key. + const publicKeyLength = 66 + return { + signedPayloadValuesLength: signatureLength + publicKeyLength, + signedPayloadShell: JSON.stringify(shell) + } +} + +/** + * Fast function that determines the final ASCII string byte length of the + * JSON stringified ECIES encrypted payload. + * @ignore + */ +export function eciesGetJsonStringLength(opts: { + contentLength: number, + wasString: boolean, + sign: boolean, + cipherTextEncoding: CipherTextEncoding +}): number { + const { payloadShell, payloadValuesLength } = getCipherObjectWrapper(opts) + + // Calculate the AES output length given the input length. + const cipherTextLength = getAesCbcOutputLength(opts.contentLength) + + // Get the encoded string length of the cipherText. + let encodedCipherTextLength: number + if (!opts.cipherTextEncoding || opts.cipherTextEncoding === 'hex') { + encodedCipherTextLength = (cipherTextLength * 2) + } else if (opts.cipherTextEncoding === 'base64') { + encodedCipherTextLength = getBase64OutputLength(cipherTextLength) + } else { + throw new Error(`Unexpected cipherTextEncoding "${opts.cipherTextEncoding}"`) + } + + if (!opts.sign) { + // Add the length of the JSON envelope, ciphertext length, and length of const values. + return payloadShell.length + + payloadValuesLength + + encodedCipherTextLength + } else { + // Get the signed version of the JSON envelope + const { + signedPayloadShell, + signedPayloadValuesLength + } = getSignedCipherObjectWrapper(payloadShell) + // Add length of the JSON envelope, ciphertext length, and length of the const values. + return signedPayloadShell.length + + signedPayloadValuesLength + + payloadValuesLength + + encodedCipherTextLength + } +} + +/** + * Encrypt content to elliptic curve publicKey using ECIES + * @param publicKey - secp256k1 public key hex string + * @param content - content to encrypt + * @return Object containing: + * iv (initialization vector, hex encoding), + * cipherText (cipher text either hex or base64 encoded), + * mac (message authentication code, hex encoded), + * ephemeral public key (hex encoded), + * wasString (boolean indicating with or not to return a buffer or string on decrypt) + * @private + * @ignore + */ +export async function encryptECIES(publicKey: string, + content: Buffer, + wasString: boolean, + cipherTextEncoding?: CipherTextEncoding): + Promise { + const ecPK = ecurve.keyFromPublic(publicKey, 'hex').getPublic() + const ephemeralSK = ecurve.genKeyPair() + const ephemeralPK = Buffer.from(ephemeralSK.getPublic().encodeCompressed()) + const sharedSecret = ephemeralSK.derive(ecPK) as BN + const sharedSecretBuffer = getBufferFromBN(sharedSecret) + const sharedKeys = sharedSecretToKeys(sharedSecretBuffer) + + const initializationVector = randomBytes(16) + + const cipherText = await aes256CbcEncrypt( + initializationVector, sharedKeys.encryptionKey, content + ) + + const macData = Buffer.concat([initializationVector, + ephemeralPK, + cipherText]) + const mac = await hmacSha256(sharedKeys.hmacKey, macData) + + let cipherTextString: string + if (!cipherTextEncoding || cipherTextEncoding === 'hex') { + cipherTextString = cipherText.toString('hex') + } else if (cipherTextEncoding === 'base64') { + cipherTextString = cipherText.toString('base64') + } else { + throw new Error(`Unexpected cipherTextEncoding "${cipherTextEncoding}"`) + } + + const result: CipherObject = { + iv: initializationVector.toString('hex'), + ephemeralPK: ephemeralPK.toString('hex'), + cipherText: cipherTextString, + mac: mac.toString('hex'), + wasString: !!wasString + } + if (cipherTextEncoding && cipherTextEncoding !== 'hex') { + result.cipherTextEncoding = cipherTextEncoding + } + return result +} + +/** + * Decrypt content encrypted using ECIES + * @param {String} privateKey - secp256k1 private key hex string + * @param {Object} cipherObject - object to decrypt, should contain: + * iv (initialization vector), cipherText (cipher text), + * mac (message authentication code), ephemeralPublicKey + * wasString (boolean indicating with or not to return a buffer or string on decrypt) + * @return {Buffer} plaintext + * @throws {FailedDecryptionError} if unable to decrypt + * @private + * @ignore + */ +export async function decryptECIES(privateKey: string, cipherObject: CipherObject): + Promise { + const ecSK = ecurve.keyFromPrivate(privateKey, 'hex') + let ephemeralPK = null + try { + ephemeralPK = ecurve.keyFromPublic(cipherObject.ephemeralPK, 'hex').getPublic() + } catch (error) { + throw new FailedDecryptionError('Unable to get public key from cipher object. ' + + 'You might be trying to decrypt an unencrypted object.') + } + + const sharedSecret = ecSK.derive(ephemeralPK) as BN + const sharedSecretBuffer = getBufferFromBN(sharedSecret) + + const sharedKeys = sharedSecretToKeys(sharedSecretBuffer) + + const ivBuffer = Buffer.from(cipherObject.iv, 'hex') + let cipherTextBuffer: Buffer + + if (!cipherObject.cipherTextEncoding || cipherObject.cipherTextEncoding === 'hex') { + cipherTextBuffer = Buffer.from(cipherObject.cipherText, 'hex') + } else if (cipherObject.cipherTextEncoding === 'base64') { + cipherTextBuffer = Buffer.from(cipherObject.cipherText, 'base64') + } else { + throw new Error(`Unexpected cipherTextEncoding "${cipherObject.cipherText}"`) + } + + const macData = Buffer.concat([ivBuffer, + Buffer.from(ephemeralPK.encodeCompressed()), + cipherTextBuffer]) + const actualMac = await hmacSha256(sharedKeys.hmacKey, macData) + const expectedMac = Buffer.from(cipherObject.mac, 'hex') + if (!equalConstTime(expectedMac, actualMac)) { + throw new FailedDecryptionError('Decryption failed: failure in MAC check') + } + const plainText = await aes256CbcDecrypt( + ivBuffer, sharedKeys.encryptionKey, cipherTextBuffer + ) + + if (cipherObject.wasString) { + return plainText.toString() + } else { + return plainText + } +} + +/** + * Sign content using ECDSA + * + * @param {String} privateKey - secp256k1 private key hex string + * @param {Object} content - content to sign + * @return {Object} contains: + * signature - Hex encoded DER signature + * public key - Hex encoded private string taken from privateKey + * @private + * @ignore + */ +export function signECDSA(privateKey: string, content: string | Buffer): { + publicKey: string, signature: string +} { + const contentBuffer = content instanceof Buffer ? content : Buffer.from(content) + const ecPrivate = ecurve.keyFromPrivate(privateKey, 'hex') + const publicKey = getPublicKeyFromPrivate(privateKey) + const contentHash = hashSha256Sync(contentBuffer) + const signature = ecPrivate.sign(contentHash) + const signatureString: string = signature.toDER('hex') + return { + signature: signatureString, + publicKey + } +} + +/** +* @ignore +*/ +function getBuffer(content: string | ArrayBuffer | Buffer) { + if (content instanceof Buffer) return content + else if (content instanceof ArrayBuffer) return Buffer.from(content) + else return Buffer.from(content) +} + +/** + * Verify content using ECDSA + * @param {String | Buffer} content - Content to verify was signed + * @param {String} publicKey - secp256k1 private key hex string + * @param {String} signature - Hex encoded DER signature + * @return {Boolean} returns true when signature matches publickey + content, false if not + * @private + * @ignore + */ +export function verifyECDSA( + content: string | ArrayBuffer | Buffer, + publicKey: string, + signature: string): boolean { + const contentBuffer = getBuffer(content) + const ecPublic = ecurve.keyFromPublic(publicKey, 'hex') + const contentHash = hashSha256Sync(contentBuffer) + + return ecPublic.verify(contentHash, signature) +} diff --git a/packages/encryption/src/encryption.ts b/packages/encryption/src/encryption.ts new file mode 100644 index 000000000..ccabf19c3 --- /dev/null +++ b/packages/encryption/src/encryption.ts @@ -0,0 +1,130 @@ +import { + CipherTextEncoding, + SignedCipherObject, + encryptECIES, + decryptECIES, + signECDSA +} from './ec'; + +import { getPublicKeyFromPrivate } from './keys'; + +export interface EncryptionOptions { + /** + * If set to `true` the data is signed using ECDSA on SHA256 hashes with the user's + * app private key. If a string is specified, it is used as the private key instead + * of the user's app private key. + * @default false + */ + sign?: boolean | string; + /** + * String encoding format for the cipherText buffer. + * Currently defaults to 'hex' for legacy backwards-compatibility. + * Only used if the `encrypt` option is also used. + * Note: in the future this should default to 'base64' for the significant + * file size reduction. + */ + cipherTextEncoding?: CipherTextEncoding; + /** + * Specifies if the original unencrypted content is a ASCII or UTF-8 string. + * For example stringified JSON. + * If true, then when the ciphertext is decrypted, it will be returned as + * a `string` type variable, otherwise will be returned as a Buffer. + */ + wasString?: boolean; +} + +/** + * Specify encryption options, and whether to sign the ciphertext. + */ +export interface EncryptContentOptions extends EncryptionOptions { + /** + * Encrypt the data with this key. + */ + publicKey?: string; + /** + * Encrypt the data with the public key corresponding to the supplied private key + */ + privateKey?: string; +} + +/** + * Encrypts the data provided with the app public key. + * @param {String|Buffer} content - data to encrypt + * @param {Object} [options=null] - options object + * @param {String} options.publicKey - the hex string of the ECDSA public + * key to use for encryption. If not provided, will use user's appPublicKey. + * @return {String} Stringified ciphertext object + */ +export async function encryptContent( + content: string | Buffer, + options?: EncryptContentOptions +): Promise { + const opts = Object.assign({}, options) + let privateKey: string + if (!opts.publicKey) { + if (!opts.privateKey) { + throw new Error('Either public key or private key must be supplied for encryption.') + } + opts.publicKey = getPublicKeyFromPrivate(opts.privateKey) + } + let wasString: boolean + if (typeof opts.wasString === 'boolean') { + wasString = opts.wasString + } else { + wasString = typeof content === 'string' + } + const contentBuffer = typeof content === 'string' ? Buffer.from(content) : content + const cipherObject = await encryptECIES(opts.publicKey, + contentBuffer, + wasString, + opts.cipherTextEncoding) + let cipherPayload = JSON.stringify(cipherObject) + if (opts.sign) { + if (typeof opts.sign === 'string') { + privateKey = opts.sign + } else if (!privateKey) { + throw new Error('Private key is required for signing.') + } + const signatureObject = signECDSA(privateKey, cipherPayload) + const signedCipherObject: SignedCipherObject = { + signature: signatureObject.signature, + publicKey: signatureObject.publicKey, + cipherText: cipherPayload + } + cipherPayload = JSON.stringify(signedCipherObject) + } + return cipherPayload +} + +/** + * Decrypts data encrypted with `encryptContent` with the + * transit private key. + * @param {String|Buffer} content - encrypted content. + * @param {Object} [options=null] - options object + * @param {String} options.privateKey - the hex string of the ECDSA private + * key to use for decryption. If not provided, will use user's appPrivateKey. + * @return {String|Buffer} decrypted content. + */ +export function decryptContent( + content: string, + options?: { + privateKey?: string + }, +): Promise { + const opts = Object.assign({}, options) + if (!opts.privateKey) { + throw new Error('Private key is required for decryption.') + } + + try { + const cipherObject = JSON.parse(content) + return decryptECIES(opts.privateKey, cipherObject) + } catch (err) { + if (err instanceof SyntaxError) { + throw new Error('Failed to parse encrypted content JSON. The content may not ' + + 'be encrypted. If using getFile, try passing { decrypt: false }.') + } else { + throw err + } + } +} \ No newline at end of file diff --git a/packages/encryption/src/hashRipemd160.ts b/packages/encryption/src/hashRipemd160.ts new file mode 100644 index 000000000..b6a5defd4 --- /dev/null +++ b/packages/encryption/src/hashRipemd160.ts @@ -0,0 +1,63 @@ +import Ripemd160Polyfill from 'ripemd160-min' +import { isNodeCryptoAvailable } from './cryptoUtils' + +type NodeCryptoCreateHash = typeof import('crypto').createHash + +export interface Ripemd160Digest { + digest(data: Buffer): Buffer +} + +export class Ripemd160PolyfillDigest implements Ripemd160Digest { + digest(data: Buffer): Buffer { + const instance = new Ripemd160Polyfill() + instance.update(data) + const hash = instance.digest() + if (Array.isArray(hash)) { + return Buffer.from(hash) + } else { + return Buffer.from(hash.buffer) + } + } +} + +export class NodeCryptoRipemd160Digest implements Ripemd160Digest { + nodeCryptoCreateHash: NodeCryptoCreateHash + + constructor(nodeCryptoCreateHash: NodeCryptoCreateHash) { + this.nodeCryptoCreateHash = nodeCryptoCreateHash + } + + digest(data: Buffer): Buffer { + try { + return this.nodeCryptoCreateHash('rmd160').update(data).digest() + } catch (error) { + try { + return this.nodeCryptoCreateHash('ripemd160').update(data).digest() + } catch (_err) { + console.log(error) + console.log('Node.js `crypto.createHash` exists but failing to digest for ripemd160, falling back to js implementation') + const polyfill = new Ripemd160PolyfillDigest() + return polyfill.digest(data) + } + } + } +} + +export function createHashRipemd160() { + const nodeCryptoCreateHash = isNodeCryptoAvailable(nodeCrypto => { + if (typeof nodeCrypto.createHash === 'function') { + return nodeCrypto.createHash + } + return false + }) + if (nodeCryptoCreateHash) { + return new NodeCryptoRipemd160Digest(nodeCryptoCreateHash) + } else { + return new Ripemd160PolyfillDigest() + } +} + +export function hashRipemd160(data: Buffer) { + const hash = createHashRipemd160() + return hash.digest(data) +} diff --git a/packages/encryption/src/hmacSha256.ts b/packages/encryption/src/hmacSha256.ts new file mode 100644 index 000000000..f7a4a1900 --- /dev/null +++ b/packages/encryption/src/hmacSha256.ts @@ -0,0 +1,53 @@ +import { getCryptoLib } from './cryptoUtils' + +export interface Hmac { + digest(key: Buffer, data: Buffer): Promise; +} + +type NodeCryptoCreateHmac = typeof import('crypto').createHmac + +export class NodeCryptoHmacSha256 implements Hmac { + createHmac: NodeCryptoCreateHmac + + constructor(createHmac: NodeCryptoCreateHmac) { + this.createHmac = createHmac + } + + async digest(key: Buffer, data: Buffer): Promise { + const result = this.createHmac('sha256', key) + .update(data) + .digest() + return Promise.resolve(result) + } +} + +export class WebCryptoHmacSha256 implements Hmac { + subtleCrypto: SubtleCrypto + + constructor(subtleCrypto: SubtleCrypto) { + this.subtleCrypto = subtleCrypto + } + + async digest(key: Buffer, data: Buffer): Promise { + const cryptoKey = await this.subtleCrypto.importKey( + 'raw', key, { name: 'HMAC', hash: 'SHA-256' }, + true, ['sign'] + ) + const sig = await this.subtleCrypto.sign( + // The `hash` is only specified for non-compliant browsers like Edge. + { name: 'HMAC', hash: 'SHA-256' }, + cryptoKey, data + ) + return Buffer.from(sig) + } +} + +export async function createHmacSha256(): Promise { + const cryptoLib = await getCryptoLib() + if (cryptoLib.name === 'subtleCrypto') { + return new WebCryptoHmacSha256(cryptoLib.lib) + } else { + return new NodeCryptoHmacSha256(cryptoLib.lib.createHmac) + } +} + diff --git a/packages/encryption/src/index.ts b/packages/encryption/src/index.ts new file mode 100644 index 000000000..59b490296 --- /dev/null +++ b/packages/encryption/src/index.ts @@ -0,0 +1,14 @@ +export * from './ec'; + +export { + encryptMnemonic, + decryptMnemonic +} from './wallet'; + +export * from './keys'; + +export * from './cryptoRandom'; + +export * from './sha2Hash'; + +export * from './encryption'; diff --git a/packages/encryption/src/keys.ts b/packages/encryption/src/keys.ts new file mode 100644 index 000000000..c60a96aef --- /dev/null +++ b/packages/encryption/src/keys.ts @@ -0,0 +1,94 @@ + +import { ECPair, address, networks, Network } from 'bitcoinjs-lib' +import { randomBytes } from './cryptoRandom' +import { hashSha256Sync } from './sha2Hash' +import { hashRipemd160 } from './hashRipemd160' + +/** + * + * @param numberOfBytes + * + * @ignore + */ +export function getEntropy(arg: number): Buffer { + if (!arg) { + arg = 32 + } + return randomBytes(arg) +} + +/** +* @ignore +*/ +export function makeECPrivateKey() { + const keyPair = ECPair.makeRandom({ rng: getEntropy }) + return keyPair.privateKey.toString('hex') +} + +/** +* @ignore +*/ +export function publicKeyToAddress(publicKey: string | Buffer) { + const publicKeyBuffer = Buffer.isBuffer(publicKey) ? publicKey : Buffer.from(publicKey, 'hex') + const publicKeyHash160 = hashRipemd160(hashSha256Sync(publicKeyBuffer)) + const result = address.toBase58Check(publicKeyHash160, networks.bitcoin.pubKeyHash) + return result +} + +/** +* @ignore +*/ +export function getPublicKeyFromPrivate(privateKey: string | Buffer) { + const privateKeyBuffer = Buffer.isBuffer(privateKey) ? privateKey : Buffer.from(privateKey, 'hex') + const keyPair = ECPair.fromPrivateKey(privateKeyBuffer) + return keyPair.publicKey.toString('hex') +} + +/** + * Time + * @private + * @ignore + */ +export function hexStringToECPair(skHex: string, network?: Network): ECPair.ECPairInterface { + const ecPairOptions = { + network: network || networks.bitcoin, + compressed: true + } + + if (skHex.length === 66) { + if (skHex.slice(64) !== '01') { + throw new Error('Improperly formatted private-key hex string. 66-length hex usually ' + + 'indicates compressed key, but last byte must be == 1') + } + return ECPair.fromPrivateKey(Buffer.from(skHex.slice(0, 64), 'hex'), ecPairOptions) + } else if (skHex.length === 64) { + ecPairOptions.compressed = false + return ECPair.fromPrivateKey(Buffer.from(skHex, 'hex'), ecPairOptions) + } else { + throw new Error('Improperly formatted private-key hex string: length should be 64 or 66.') + } +} + +/** + * + * @ignore + */ +export function ecPairToHexString(secretKey: ECPair.ECPairInterface) { + const ecPointHex = secretKey.privateKey.toString('hex') + if (secretKey.compressed) { + return `${ecPointHex}01` + } else { + return ecPointHex + } +} + +/** + * Creates a bitcoin address string from an ECPair + * @private + * @ignore + */ +export function ecPairToAddress(keyPair: ECPair.ECPairInterface) { + const sha256 = hashSha256Sync(keyPair.publicKey) + const hash160 = hashRipemd160(sha256) + return address.toBase58Check(hash160, keyPair.network.pubKeyHash) +} diff --git a/packages/encryption/src/pbkdf2.ts b/packages/encryption/src/pbkdf2.ts new file mode 100644 index 000000000..2d9fdd2e3 --- /dev/null +++ b/packages/encryption/src/pbkdf2.ts @@ -0,0 +1,159 @@ +import { getCryptoLib } from './cryptoUtils' + +export type Pbkdf2Digests = 'sha512' | 'sha256' + +export interface Pbkdf2 { + derive( + password: string, + salt: Buffer, + iterations: number, + keyLength: number, + digest: Pbkdf2Digests): Promise; +} + +type NodePbkdf2Fn = typeof import('crypto').pbkdf2 + +export class NodeCryptoPbkdf2 implements Pbkdf2 { + nodePbkdf2: NodePbkdf2Fn + + constructor(nodePbkdf2: NodePbkdf2Fn) { + this.nodePbkdf2 = nodePbkdf2 + } + + async derive( + password: string, + salt: Buffer, + iterations: number, + keyLength: number, + digest: Pbkdf2Digests): + Promise { + if (digest !== 'sha512' && digest !== 'sha256') { + throw new Error(`Unsupported digest "${digest}" for Pbkdf2`) + } + return new Promise((resolve, reject) => { + this.nodePbkdf2(password, salt, iterations, keyLength, digest, (error, result) => { + if (error) { + reject(error) + } + resolve(result) + }) + }) + } +} + +export class WebCryptoPbkdf2 implements Pbkdf2 { + subtleCrypto: SubtleCrypto + + constructor(subtleCrypto: SubtleCrypto) { + this.subtleCrypto = subtleCrypto + } + + async derive( + password: string, + salt: Buffer, + iterations: number, + keyLength: number, + digest: Pbkdf2Digests): + Promise { + let algo: string + if (digest === 'sha256') { + algo = 'SHA-256' + } else if (digest === 'sha512') { + algo = 'SHA-512' + } else { + throw new Error(`Unsupported Pbkdf2 digest algorithm "${digest}"`) + } + let result: ArrayBuffer + const passwordBytes = Buffer.from(password, 'utf8') + try { + const key = await this.subtleCrypto.importKey( + 'raw', passwordBytes, 'PBKDF2', false, ['deriveBits'] + ) + result = await this.subtleCrypto.deriveBits({ + name: 'PBKDF2', salt, iterations, hash: { name: algo } + }, key, keyLength * 8) + } catch (error) { + // Browser appears to support WebCrypto but missing pbkdf2 support. + const partialWebCrypto = new WebCryptoPartialPbkdf2(this.subtleCrypto) + return partialWebCrypto.derive(password, salt, iterations, keyLength, digest) + } + return Buffer.from(result) + } +} + +export class WebCryptoPartialPbkdf2 implements Pbkdf2 { + // An async implementation for browsers that support WebCrypto hmac + // but not pbkdf2. Extracted from crypto-browserify/pbkdf2 and modified to + // use WebCrypto for hmac operations. + // Original: https://github.com/crypto-browserify/pbkdf2/tree/v3.0.17/lib + + subtleCrypto: SubtleCrypto + + constructor(subtleCrypto: SubtleCrypto) { + this.subtleCrypto = subtleCrypto + } + + async derive( + password: string, + salt: Buffer, + iterations: number, + keyLength: number, + digest: Pbkdf2Digests): + Promise { + if (digest !== 'sha512' && digest !== 'sha256') { + throw new Error(`Unsupported digest "${digest}" for Pbkdf2`) + } + const key = Buffer.from(password, 'utf8') + const algo = digest === 'sha512' ? 'SHA-512' : 'SHA-256' + const algoOpts = { name: 'HMAC', hash: algo } + const hmacDigest = (key: ArrayBuffer, data: ArrayBuffer) => this.subtleCrypto + .importKey('raw', key, algoOpts, true, ['sign']) + .then(cryptoKey => this.subtleCrypto.sign(algoOpts, cryptoKey, data)) + .then(result => new Uint8Array(result)) + + const DK = new Uint8Array(keyLength) + const saltLength = salt.length + const block1 = new Uint8Array(saltLength + 4) + block1.set(salt) + let destPos = 0 + const hLen = digest === 'sha512' ? 64 : 32 + const l = Math.ceil(keyLength / hLen) + + function writeUInt32BE(data: Uint8Array, value: number, offset: number) { + value = +value + offset >>>= 0 + data[offset] = (value >>> 24) + data[offset + 1] = (value >>> 16) + data[offset + 2] = (value >>> 8) + data[offset + 3] = (value & 0xff) + return offset + 4 + } + + for (let i = 1; i <= l; i++) { + writeUInt32BE(block1, i, saltLength) + // eslint-disable-next-line no-await-in-loop + const T = await hmacDigest(key, block1) + let U = T + for (let j = 1; j < iterations; j++) { + // eslint-disable-next-line no-await-in-loop + U = await hmacDigest(key, U) + for (let k = 0; k < hLen; k++) { + T[k] ^= U[k] + } + } + DK.set(T.subarray(0, DK.byteLength - destPos), destPos) + destPos += hLen + } + return Buffer.from(DK.buffer) + } +} + +export async function createPbkdf2(): Promise { + const cryptoLib = await getCryptoLib() + if (cryptoLib.name === 'subtleCrypto') { + return new WebCryptoPbkdf2(cryptoLib.lib) + } else { + return new NodeCryptoPbkdf2(cryptoLib.lib.pbkdf2) + } +} + diff --git a/packages/encryption/src/sha2Hash.ts b/packages/encryption/src/sha2Hash.ts new file mode 100644 index 000000000..fe1eedb46 --- /dev/null +++ b/packages/encryption/src/sha2Hash.ts @@ -0,0 +1,77 @@ +import { sha256, sha512 } from 'sha.js' +import { getCryptoLib } from './cryptoUtils' + +type NodeCryptoCreateHash = typeof import('crypto').createHash + +export interface Sha2Hash { + digest(data: Buffer, algorithm?: 'sha256' | 'sha512'): Promise; +} + +export class NodeCryptoSha2Hash { + createHash: NodeCryptoCreateHash + + constructor(createHash: NodeCryptoCreateHash) { + this.createHash = createHash + } + + async digest(data: Buffer, algorithm = 'sha256'): Promise { + try { + const result = this.createHash(algorithm) + .update(data) + .digest() + return Promise.resolve(result) + } catch (error) { + console.log(error) + console.log(`Error performing ${algorithm} digest with Node.js 'crypto.createHash', falling back to JS implementation.`) + return Promise.resolve(algorithm === 'sha256' ? hashSha256Sync(data) : hashSha512Sync(data)) + } + } +} + +export class WebCryptoSha2Hash implements Sha2Hash { + subtleCrypto: SubtleCrypto + + constructor(subtleCrypto: SubtleCrypto) { + this.subtleCrypto = subtleCrypto + } + + async digest(data: Buffer, algorithm = 'sha256'): Promise { + let algo: string + if (algorithm === 'sha256') { + algo = 'SHA-256' + } else if (algorithm === 'sha512') { + algo = 'SHA-512' + } else { + throw new Error(`Unsupported hash algorithm ${algorithm}`) + } + try { + const hash = await this.subtleCrypto.digest(algo, data) + return Buffer.from(hash) + } catch (error) { + console.log(error) + console.log(`Error performing ${algorithm} digest with WebCrypto, falling back to JS implementation.`) + return Promise.resolve(algorithm === 'sha256' ? hashSha256Sync(data) : hashSha512Sync(data)) + } + } +} + +export async function createSha2Hash(): Promise { + const cryptoLib = await getCryptoLib() + if (cryptoLib.name === 'subtleCrypto') { + return new WebCryptoSha2Hash(cryptoLib.lib) + } else { + return new NodeCryptoSha2Hash(cryptoLib.lib.createHash) + } +} + +export function hashSha256Sync(data: Buffer) { + const hash = new sha256() + hash.update(data) + return hash.digest() +} + +export function hashSha512Sync(data: Buffer) { + const hash = new sha512() + hash.update(data) + return hash.digest() +} diff --git a/packages/encryption/src/utils.ts b/packages/encryption/src/utils.ts new file mode 100644 index 000000000..0b897fdc6 --- /dev/null +++ b/packages/encryption/src/utils.ts @@ -0,0 +1,21 @@ +/** + * Calculate the AES-CBC ciphertext output byte length a given input length. + * AES has a fixed block size of 16-bytes regardless key size. + * @ignore + */ +export function getAesCbcOutputLength(inputByteLength: number) { + // AES-CBC block mode rounds up to the next block size. + const cipherTextLength = (Math.floor(inputByteLength / 16) + 1) * 16 + return cipherTextLength + } + +/** + * Calculate the base64 encoded string length for a given input length. + * This is equivalent to the byte length when the string is ASCII or UTF8-8 + * encoded. + * @param number + */ +export function getBase64OutputLength(inputByteLength: number) { + const encodedLength = (Math.ceil(inputByteLength / 3) * 4) + return encodedLength + } \ No newline at end of file diff --git a/packages/encryption/src/wallet.ts b/packages/encryption/src/wallet.ts new file mode 100644 index 000000000..75de463c6 --- /dev/null +++ b/packages/encryption/src/wallet.ts @@ -0,0 +1,182 @@ +import { validateMnemonic, mnemonicToEntropy, entropyToMnemonic } from 'bip39'; +import { randomBytes, GetRandomBytes } from './cryptoRandom'; +import { createSha2Hash } from './sha2Hash'; +import { createHmacSha256 } from './hmacSha256'; +import { createCipher } from './aesCipher'; +import { createPbkdf2 } from './pbkdf2'; +import { TriplesecDecryptSignature } from './cryptoUtils'; + +/** + * Encrypt a raw mnemonic phrase to be password protected + * @param {string} phrase - Raw mnemonic phrase + * @param {string} password - Password to encrypt mnemonic with + * @return {Promise} The encrypted phrase + * @private + * @ignore + * */ +export async function encryptMnemonic( + phrase: string, + password: string, + opts?: { + getRandomBytes?: GetRandomBytes; + } +): Promise { + // hex encoded mnemonic string + let mnemonicEntropy: string; + try { + // must be bip39 mnemonic + mnemonicEntropy = mnemonicToEntropy(phrase); + } catch (error) { + console.error('Invalid mnemonic phrase provided'); + console.error(error); + throw new Error('Not a valid bip39 mnemonic'); + } + + // normalize plaintext to fixed length byte string + const plaintextNormalized = Buffer.from(mnemonicEntropy, 'hex'); + + // AES-128-CBC with SHA256 HMAC + const pbkdf2 = await createPbkdf2(); + let salt: Buffer; + if (opts && opts.getRandomBytes) { + salt = opts.getRandomBytes(16); + } else { + salt = randomBytes(16); + } + const keysAndIV = await pbkdf2.derive(password, salt, 100000, 48, 'sha512'); + const encKey = keysAndIV.slice(0, 16); + const macKey = keysAndIV.slice(16, 32); + const iv = keysAndIV.slice(32, 48); + + const cipher = await createCipher(); + const cipherText = await cipher.encrypt( + 'aes-128-cbc', + encKey, + iv, + plaintextNormalized + ); + + const hmacPayload = Buffer.concat([salt, cipherText]); + const hmacSha256 = await createHmacSha256(); + const hmacDigest = await hmacSha256.digest(macKey, hmacPayload); + + const payload = Buffer.concat([salt, hmacDigest, cipherText]); + return payload; +} + +// Used to distinguish bad password during decrypt vs invalid format +class PasswordError extends Error {} + +/** + * @ignore + */ +async function decryptMnemonicBuffer( + dataBuffer: Buffer, + password: string +): Promise { + const salt = dataBuffer.slice(0, 16); + const hmacSig = dataBuffer.slice(16, 48); // 32 bytes + const cipherText = dataBuffer.slice(48); + const hmacPayload = Buffer.concat([salt, cipherText]); + + const pbkdf2 = await createPbkdf2(); + const keysAndIV = await pbkdf2.derive(password, salt, 100000, 48, 'sha512'); + const encKey = keysAndIV.slice(0, 16); + const macKey = keysAndIV.slice(16, 32); + const iv = keysAndIV.slice(32, 48); + + const decipher = await createCipher(); + const decryptedResult = await decipher.decrypt( + 'aes-128-cbc', + encKey, + iv, + cipherText + ); + + const hmacSha256 = await createHmacSha256(); + const hmacDigest = await hmacSha256.digest(macKey, hmacPayload); + + // hash both hmacSig and hmacDigest so string comparison time + // is uncorrelated to the ciphertext + const sha2Hash = await createSha2Hash(); + const hmacSigHash = await sha2Hash.digest(hmacSig); + const hmacDigestHash = await sha2Hash.digest(hmacDigest); + + if (!hmacSigHash.equals(hmacDigestHash)) { + // not authentic + throw new PasswordError('Wrong password (HMAC mismatch)'); + } + + let mnemonic: string; + try { + mnemonic = entropyToMnemonic(decryptedResult); + } catch (error) { + console.error('Error thrown by `entropyToMnemonic`'); + console.error(error); + throw new PasswordError('Wrong password (invalid plaintext)'); + } + if (!validateMnemonic(mnemonic)) { + throw new PasswordError('Wrong password (invalid plaintext)'); + } + + return mnemonic; +} + +/** + * Decrypt legacy triplesec keys + * @param {Buffer} dataBuffer - The encrypted key + * @param {String} password - Password for data + * @return {Promise} Decrypted seed + * @private + * @ignore + */ +function decryptLegacy( + dataBuffer: Buffer, + password: string, + triplesecDecrypt: TriplesecDecryptSignature +): Promise { + return new Promise((resolve, reject) => { + if (!triplesecDecrypt) { + reject(new Error('The `triplesec.decrypt` function must be provided')); + } + triplesecDecrypt( + { + key: Buffer.from(password), + data: dataBuffer + }, + (err, plaintextBuffer) => { + if (!err) { + resolve(plaintextBuffer); + } else { + reject(err); + } + } + ); + }); +} + +/** + * Decrypt an encrypted mnemonic phrase with a password. + * Legacy triplesec encrypted payloads are also supported. + * @param data - Buffer or hex-encoded string of the encrypted mnemonic + * @param password - Password for data + * @return the raw mnemonic phrase + * @private + * @ignore + */ +export async function decryptMnemonic( + data: string | Buffer, + password: string, + triplesecDecrypt?: TriplesecDecryptSignature +) { + const dataBuffer = Buffer.isBuffer(data) ? data : Buffer.from(data, 'hex'); + try { + return await decryptMnemonicBuffer(dataBuffer, password); + } catch (err) { + if (err instanceof PasswordError) { + throw err; + } + const data = await decryptLegacy(dataBuffer, password, triplesecDecrypt); + return data.toString(); + } +} diff --git a/packages/encryption/tsconfig.build.json b/packages/encryption/tsconfig.build.json new file mode 100644 index 000000000..61ab30ed3 --- /dev/null +++ b/packages/encryption/tsconfig.build.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "moduleResolution": "node", + "noEmit": false, + "rootDir": "./src", + "outDir": "./lib" + }, + "include": [ + "src/**/*" + ] +}