├── .github ├── FUNDING.yml └── workflows │ ├── nodejs.yml │ └── codeql-analysis.yml ├── .gitignore ├── src ├── utils.ts ├── index.ts └── index.spec.ts ├── tsconfig.json ├── LICENSE ├── package.json └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: alepop 2 | buy_me_a_coffee: alepop 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | npm-debug.log 3 | /dist 4 | .tern-port 5 | /coverage 6 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export const pathRegex = new RegExp("^m(\\/[0-9]+')+$"); 2 | 3 | export const replaceDerive = (val: string): string => val.replace("'", ''); 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "rootDir": "./src", 6 | "outDir": "./dist", 7 | "noImplicitAny": false, 8 | "sourceMap": false, 9 | "removeComments": true, 10 | "moduleResolution": "node", 11 | "declaration": true 12 | }, 13 | "exclude": [ 14 | "node_modules", 15 | "src/**/*.spec.*" 16 | ], 17 | "include": [ 18 | "src/**/*" 19 | ] 20 | } -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [8.x, 10.x, 12.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: npm install and test 21 | run: | 22 | npm ci 23 | npm test -- --coverage 24 | env: 25 | CI: true 26 | - name: Upload coverage to Codecov 27 | uses: codecov/codecov-action@v1.0.2 28 | with: 29 | token: ${{secrets.CODECOV_TOKEN}} 30 | file: ./coverage/lcov.info 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 alepop 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ed25519-hd-key", 3 | "version": "1.3.0", 4 | "description": "BIP-0032 like derivation for ed25519 curve", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "files": [ 8 | "dist/" 9 | ], 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/alepop/ed25519-hd-key" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/alepop/ed25519-hd-key/issues" 16 | }, 17 | "homepage": "https://github.com/alepop/ed25519-hd-key", 18 | "scripts": { 19 | "prepublish": "npm run build", 20 | "build": "tsc -p ./", 21 | "test": "jest" 22 | }, 23 | "jest": { 24 | "verbose": true, 25 | "transform": { 26 | ".(ts|tsx)": "ts-jest" 27 | }, 28 | "testRegex": "(/spec/.*|\\.(test|spec))\\.(ts|tsx|js)$", 29 | "moduleFileExtensions": [ 30 | "ts", 31 | "tsx", 32 | "js" 33 | ], 34 | "globals": { 35 | "window": {}, 36 | "ts-jest": { 37 | "tsConfig": "./tsconfig.json" 38 | } 39 | } 40 | }, 41 | "keywords": [ 42 | "ed25519", 43 | "bip32", 44 | "slip-0010", 45 | "crypto", 46 | "trezor" 47 | ], 48 | "author": "Aleksey Popov", 49 | "license": "MIT", 50 | "dependencies": { 51 | "create-hmac": "1.1.7", 52 | "tweetnacl": "1.0.3" 53 | }, 54 | "devDependencies": { 55 | "@types/jest": "25.2.3", 56 | "@types/node": "14.0.4", 57 | "jest": "25.5.4", 58 | "jest-cli": "26.0.1", 59 | "ts-jest": "25.5.1", 60 | "typescript": "3.9.3" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "Code scanning - action" 2 | 3 | on: 4 | push: 5 | branches: [master, ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [master] 9 | schedule: 10 | - cron: '0 12 * * 2' 11 | 12 | jobs: 13 | CodeQL-Build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v2 20 | with: 21 | # We must fetch at least the immediate parents so that if this is 22 | # a pull request then we can checkout the head. 23 | fetch-depth: 2 24 | 25 | # If this run was triggered by a pull request event, then checkout 26 | # the head of the pull request instead of the merge commit. 27 | - run: git checkout HEAD^2 28 | if: ${{ github.event_name == 'pull_request' }} 29 | 30 | # Initializes the CodeQL tools for scanning. 31 | - name: Initialize CodeQL 32 | uses: github/codeql-action/init@v1 33 | # Override language selection by uncommenting this and choosing your languages 34 | # with: 35 | # languages: go, javascript, csharp, python, cpp, java 36 | 37 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 38 | # If this step fails, then you should remove it and run the build manually (see below) 39 | - name: Autobuild 40 | uses: github/codeql-action/autobuild@v1 41 | 42 | # ℹ️ Command-line programs to run using the OS shell. 43 | # 📚 https://git.io/JvXDl 44 | 45 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 46 | # and modify them (or add more) to build your code if your project 47 | # uses a compiled language 48 | 49 | #- run: | 50 | # make bootstrap 51 | # make release 52 | 53 | - name: Perform CodeQL Analysis 54 | uses: github/codeql-action/analyze@v1 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![BuyMeACoffee](https://raw.githubusercontent.com/pachadotdev/buymeacoffee-badges/main/bmc-yellow.svg)](https://www.buymeacoffee.com/alepop) 2 | [![codecov](https://codecov.io/gh/alepop/ed25519-hd-key/branch/master/graph/badge.svg)](https://codecov.io/gh/alepop/ed25519-hd-key) 3 | 4 | ed25519 HD Key 5 | ===== 6 | 7 | Key Derivation for `ed25519` 8 | ------------ 9 | 10 | [SLIP-0010](https://github.com/satoshilabs/slips/blob/master/slip-0010.md) - Specification 11 | 12 | Installation 13 | ------------ 14 | 15 | npm i --save ed25519-hd-key 16 | 17 | 18 | Usage 19 | ----- 20 | 21 | **example:** 22 | 23 | ```js 24 | const { derivePath, getMasterKeyFromSeed, getPublicKey } = require('ed25519-hd-key') 25 | const hexSeed = 'fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542'; 26 | 27 | const { key, chainCode } = getMasterKeyFromSeed(hexSeed); 28 | console.log(key.toString('hex')) 29 | // => 2b4be7f19ee27bbf30c667b642d5f4aa69fd169872f8fc3059c08ebae2eb19e7 30 | console.log(chainCode.toString('hex')); 31 | // => 90046a93de5380a72b5e45010748567d5ea02bbf6522f979e05c0d8d8ca9fffb 32 | 33 | const { key, chainCode} = derivePath("m/0'/2147483647'", hexSeed); 34 | 35 | console.log(key.toString('hex')) 36 | // => ea4f5bfe8694d8bb74b7b59404632fd5968b774ed545e810de9c32a4fb4192f4 37 | console.log(chainCode.toString('hex')); 38 | // => 138f0b2551bcafeca6ff2aa88ba8ed0ed8de070841f0c4ef0165df8181eaad7f 39 | 40 | console.log(getPublicKey(key).toString('hex')) 41 | // => 005ba3b9ac6e90e83effcd25ac4e58a1365a9e35a3d3ae5eb07b9e4d90bcf7506d 42 | ``` 43 | Tests 44 | ----- 45 | ``` 46 | npm test 47 | ``` 48 | 49 | References 50 | ---------- 51 | [SLIP-0010](https://github.com/satoshilabs/slips/blob/master/slip-0010.md) 52 | 53 | [BIP-0032](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) 54 | 55 | [BIP-0044](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) 56 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as createHmac from 'create-hmac' 2 | import * as nacl from 'tweetnacl' 3 | 4 | import { replaceDerive, pathRegex } from './utils'; 5 | 6 | interface Nacl { 7 | crypto_sign_seed_keypair: ( 8 | val: Buffer, 9 | ) => { signPk: Buffer; signSk: Buffer }; 10 | } 11 | type Hex = string; 12 | type Path = string; 13 | 14 | type Keys = { 15 | key: Buffer; 16 | chainCode: Buffer; 17 | }; 18 | 19 | const ED25519_CURVE = 'ed25519 seed'; 20 | const HARDENED_OFFSET = 0x80000000; 21 | 22 | export const getMasterKeyFromSeed = (seed: Hex): Keys => { 23 | const hmac = createHmac('sha512', ED25519_CURVE); 24 | const I = hmac.update(Buffer.from(seed, 'hex')).digest(); 25 | const IL = I.slice(0, 32); 26 | const IR = I.slice(32); 27 | return { 28 | key: IL, 29 | chainCode: IR, 30 | }; 31 | }; 32 | 33 | export const CKDPriv = ({ key, chainCode }: Keys, index: number): Keys => { 34 | const indexBuffer = Buffer.allocUnsafe(4); 35 | indexBuffer.writeUInt32BE(index, 0); 36 | 37 | const data = Buffer.concat([Buffer.alloc(1, 0), key, indexBuffer]); 38 | 39 | const I = createHmac('sha512', chainCode) 40 | .update(data) 41 | .digest(); 42 | const IL = I.slice(0, 32); 43 | const IR = I.slice(32); 44 | return { 45 | key: IL, 46 | chainCode: IR, 47 | }; 48 | }; 49 | 50 | export const getPublicKey = (privateKey: Buffer, withZeroByte = true): Buffer => { 51 | const keyPair = nacl.sign.keyPair.fromSeed(privateKey); 52 | const signPk = keyPair.secretKey.subarray(32); 53 | const zero = Buffer.alloc(1, 0); 54 | return withZeroByte ? 55 | Buffer.concat([zero, Buffer.from(signPk)]) : 56 | Buffer.from(signPk); 57 | }; 58 | 59 | export const isValidPath = (path: string): boolean => { 60 | if (!pathRegex.test(path)) { 61 | return false; 62 | } 63 | return !path 64 | .split('/') 65 | .slice(1) 66 | .map(replaceDerive) 67 | .some(isNaN as any /* ts T_T*/); 68 | }; 69 | 70 | export const derivePath = (path: Path, seed: Hex, offset = HARDENED_OFFSET): Keys => { 71 | if (!isValidPath(path)) { 72 | throw new Error('Invalid derivation path'); 73 | } 74 | 75 | const { key, chainCode } = getMasterKeyFromSeed(seed); 76 | const segments = path 77 | .split('/') 78 | .slice(1) 79 | .map(replaceDerive) 80 | .map(el => parseInt(el, 10)); 81 | 82 | return segments.reduce( 83 | (parentKeys, segment) => CKDPriv(parentKeys, segment + offset), 84 | { key, chainCode }, 85 | ); 86 | }; 87 | -------------------------------------------------------------------------------- /src/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { derivePath, getPublicKey, getMasterKeyFromSeed } from './'; 2 | 3 | const vector_1_seed = '000102030405060708090a0b0c0d0e0f'; 4 | const vector_1 = [ 5 | { 6 | path: "m/0'", 7 | chainCode: 8 | '8b59aa11380b624e81507a27fedda59fea6d0b779a778918a2fd3590e16e9c69', 9 | key: '68e0fe46dfb67e368c75379acec591dad19df3cde26e63b93a8e704f1dade7a3', 10 | publicKey: 11 | '008c8a13df77a28f3445213a0f432fde644acaa215fc72dcdf300d5efaa85d350c', 12 | }, 13 | { 14 | path: "m/0'/1'", 15 | chainCode: 16 | 'a320425f77d1b5c2505a6b1b27382b37368ee640e3557c315416801243552f14', 17 | key: 'b1d0bad404bf35da785a64ca1ac54b2617211d2777696fbffaf208f746ae84f2', 18 | publicKey: 19 | '001932a5270f335bed617d5b935c80aedb1a35bd9fc1e31acafd5372c30f5c1187', 20 | }, 21 | { 22 | path: "m/0'/1'/2'", 23 | chainCode: 24 | '2e69929e00b5ab250f49c3fb1c12f252de4fed2c1db88387094a0f8c4c9ccd6c', 25 | key: '92a5b23c0b8a99e37d07df3fb9966917f5d06e02ddbd909c7e184371463e9fc9', 26 | publicKey: 27 | '00ae98736566d30ed0e9d2f4486a64bc95740d89c7db33f52121f8ea8f76ff0fc1', 28 | }, 29 | { 30 | path: "m/0'/1'/2'/2'", 31 | chainCode: 32 | '8f6d87f93d750e0efccda017d662a1b31a266e4a6f5993b15f5c1f07f74dd5cc', 33 | key: '30d1dc7e5fc04c31219ab25a27ae00b50f6fd66622f6e9c913253d6511d1e662', 34 | publicKey: 35 | '008abae2d66361c879b900d204ad2cc4984fa2aa344dd7ddc46007329ac76c429c', 36 | }, 37 | { 38 | path: "m/0'/1'/2'/2'/1000000000'", 39 | chainCode: 40 | '68789923a0cac2cd5a29172a475fe9e0fb14cd6adb5ad98a3fa70333e7afa230', 41 | key: '8f94d394a8e8fd6b1bc2f3f49f5c47e385281d5c17e65324b0f62483e37e8793', 42 | publicKey: 43 | '003c24da049451555d51a7014a37337aa4e12d41e485abccfa46b47dfb2af54b7a', 44 | }, 45 | ]; 46 | 47 | const vector_2_seed = 48 | 'fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542'; 49 | const vector_2 = [ 50 | { 51 | path: "m/0'", 52 | chainCode: 53 | '0b78a3226f915c082bf118f83618a618ab6dec793752624cbeb622acb562862d', 54 | key: '1559eb2bbec5790b0c65d8693e4d0875b1747f4970ae8b650486ed7470845635', 55 | publicKey: 56 | '0086fab68dcb57aa196c77c5f264f215a112c22a912c10d123b0d03c3c28ef1037', 57 | }, 58 | { 59 | path: "m/0'/2147483647'", 60 | chainCode: 61 | '138f0b2551bcafeca6ff2aa88ba8ed0ed8de070841f0c4ef0165df8181eaad7f', 62 | key: 'ea4f5bfe8694d8bb74b7b59404632fd5968b774ed545e810de9c32a4fb4192f4', 63 | publicKey: 64 | '005ba3b9ac6e90e83effcd25ac4e58a1365a9e35a3d3ae5eb07b9e4d90bcf7506d', 65 | }, 66 | { 67 | path: "m/0'/2147483647'/1'", 68 | chainCode: 69 | '73bd9fff1cfbde33a1b846c27085f711c0fe2d66fd32e139d3ebc28e5a4a6b90', 70 | key: '3757c7577170179c7868353ada796c839135b3d30554bbb74a4b1e4a5a58505c', 71 | publicKey: 72 | '002e66aa57069c86cc18249aecf5cb5a9cebbfd6fadeab056254763874a9352b45', 73 | }, 74 | { 75 | path: "m/0'/2147483647'/1'/2147483646'", 76 | chainCode: 77 | '0902fe8a29f9140480a00ef244bd183e8a13288e4412d8389d140aac1794825a', 78 | key: '5837736c89570de861ebc173b1086da4f505d4adb387c6a1b1342d5e4ac9ec72', 79 | publicKey: 80 | '00e33c0f7d81d843c572275f287498e8d408654fdf0d1e065b84e2e6f157aab09b', 81 | }, 82 | { 83 | path: "m/0'/2147483647'/1'/2147483646'/2'", 84 | chainCode: 85 | '5d70af781f3a37b829f0d060924d5e960bdc02e85423494afc0b1a41bbe196d4', 86 | key: '551d333177df541ad876a60ea71f00447931c0a9da16f227c11ea080d7391b8d', 87 | publicKey: 88 | '0047150c75db263559a70d5778bf36abbab30fb061ad69f69ece61a72b0cfa4fc0', 89 | }, 90 | ]; 91 | 92 | describe('Test vectors', () => { 93 | // https://github.com/satoshilabs/slips/blob/master/slip-0010.md#test-vector-1-for-ed25519 94 | describe('Vector 1', () => { 95 | it('should have valid keys for vector 1 seed hex', () => { 96 | const { key, chainCode } = getMasterKeyFromSeed(vector_1_seed); 97 | expect(key.toString('hex')).toEqual( 98 | '2b4be7f19ee27bbf30c667b642d5f4aa69fd169872f8fc3059c08ebae2eb19e7', 99 | ); 100 | expect(chainCode.toString('hex')).toEqual( 101 | '90046a93de5380a72b5e45010748567d5ea02bbf6522f979e05c0d8d8ca9fffb', 102 | ); 103 | }); 104 | vector_1.forEach(vector => { 105 | it(`should valid for ${vector.path}`, () => { 106 | const { key, chainCode } = derivePath( 107 | vector.path, 108 | vector_1_seed, 109 | ); 110 | expect({ 111 | path: vector.path, 112 | key: key.toString('hex'), 113 | chainCode: chainCode.toString('hex'), 114 | publicKey: getPublicKey(key).toString('hex'), 115 | }).toEqual(vector); 116 | }); 117 | }); 118 | }); 119 | // https://github.com/satoshilabs/slips/blob/master/slip-0010.md#test-vector-2-for-ed25519 120 | describe('Vector 2', () => { 121 | it('should have valid keys for vector 2 seed hex', () => { 122 | const { key, chainCode } = getMasterKeyFromSeed(vector_2_seed); 123 | expect(key.toString('hex')).toEqual( 124 | '171cb88b1b3c1db25add599712e36245d75bc65a1a5c9e18d76f9f2b1eab4012', 125 | ); 126 | expect(chainCode.toString('hex')).toEqual( 127 | 'ef70a74db9c3a5af931b5fe73ed8e1a53464133654fd55e7a66f8570b8e33c3b', 128 | ); 129 | }); 130 | vector_2.forEach(vector => { 131 | it(`should valid for ${vector.path}`, () => { 132 | const { key, chainCode } = derivePath( 133 | vector.path, 134 | vector_2_seed, 135 | ); 136 | expect({ 137 | path: vector.path, 138 | key: key.toString('hex'), 139 | chainCode: chainCode.toString('hex'), 140 | publicKey: getPublicKey(key).toString('hex'), 141 | }).toEqual(vector); 142 | }); 143 | }); 144 | }); 145 | }); 146 | --------------------------------------------------------------------------------