├── LICENSE ├── README.md ├── index.js ├── package.json └── test └── index.test.js /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 knaccc 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | 13 | Base58 library Copyright 2014-2018, MyMonero.com (also BSD-3-Clause licensed). 14 | 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Monero Subaddress javascript utility 2 | 3 | This library generates subaddresses. 4 | 5 | To use, first find your public spend key and secret view key from your Monero wallet. In the GUI, these are available in the Settings->Seed & Keys area. In the CLI, open your wallet and use the commands `spendkey` and `viewkey` to find the keys. 6 | 7 | Monero wallets can have multiple receiving addresses called subaddresses. These are organized into accounts. Each Monero account can have multiple subaddresses. 8 | 9 | Subaddresses are unlinkable. This means that it is impossible for someone to look at two of your subaddresses and know that they both are addresses for the same wallet. 10 | 11 | The first address of the first account is the same as the main wallet address. 12 | 13 | ## Example code 14 | 15 | The following example will display the second subaddress (subaddress index 1) of the first account (account index 0) in the wallet. 16 | 17 | ```javascript 18 | const subaddress = require('subaddress'); 19 | 20 | let publicSpendKeyHex = "3a4a80eada742b2e21025df7ee00dd95227441c64f4f7ed63886c19fd619a6ab"; 21 | let privateViewKeyHex = "3d09263424487cbdc78d56e1f411ff1c171f84d756b736a2ced698011278d709"; 22 | 23 | let addr = subaddress.getSubaddress(privateViewKeyHex, publicSpendKeyHex, 0, 1); 24 | console.log(addr); 25 | ``` 26 | 27 | ## NPM package 28 | 29 | To run the example code, you must have the node and npm packages installed. 30 | 31 | If you are using Ubuntu, type: 32 | 33 | ``` 34 | sudo apt install nodejs npm 35 | npm install subaddress 36 | ``` 37 | 38 | To run the example code, put it into a file called example.js and type: 39 | 40 | ``` 41 | node example.js 42 | ``` 43 | 44 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const BN = require('bn.js'); 2 | const elliptic = require('elliptic'); 3 | const keccak = require('keccak'); 4 | const Buffer = require('safe-buffer').Buffer; 5 | 6 | const ed25519 = require('elliptic').eddsa('ed25519'); 7 | 8 | const cnBase58 = (function () { 9 | var b58 = {}; 10 | var alphabet_str = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; 11 | var alphabet = []; 12 | for (var i = 0; i < alphabet_str.length; i++) { 13 | alphabet.push(alphabet_str.charCodeAt(i)); 14 | } 15 | var encoded_block_sizes = [0, 2, 3, 5, 6, 7, 9, 10, 11]; 16 | var alphabet_size = alphabet.length; 17 | var full_block_size = 8; 18 | var full_encoded_block_size = 11; 19 | 20 | function hextobin(hex) { 21 | if (hex.length % 2 !== 0) throw "Hex string has invalid length!"; 22 | var res = new Uint8Array(hex.length / 2); 23 | for (var i = 0; i < hex.length / 2; ++i) { 24 | res[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); 25 | } 26 | return res; 27 | } 28 | 29 | function bintostr(bin) { 30 | var out = []; 31 | for (var i = 0; i < bin.length; i++) { 32 | out.push(String.fromCharCode(bin[i])); 33 | } 34 | return out.join(""); 35 | } 36 | 37 | function uint8_be_to_64(data) { 38 | if (data.length < 1 || data.length > 8) { 39 | throw "Invalid input length"; 40 | } 41 | var res = new BN(0); 42 | var twopow8 = new BN(2).pow(new BN(8)); 43 | var i = 0; 44 | switch (9 - data.length) { 45 | case 1: 46 | res = res.add(new BN(data[i++])); 47 | case 2: 48 | res = res.mul(twopow8).add(new BN(data[i++])); 49 | case 3: 50 | res = res.mul(twopow8).add(new BN(data[i++])); 51 | case 4: 52 | res = res.mul(twopow8).add(new BN(data[i++])); 53 | case 5: 54 | res = res.mul(twopow8).add(new BN(data[i++])); 55 | case 6: 56 | res = res.mul(twopow8).add(new BN(data[i++])); 57 | case 7: 58 | res = res.mul(twopow8).add(new BN(data[i++])); 59 | case 8: 60 | res = res.mul(twopow8).add(new BN(data[i++])); 61 | break; 62 | default: 63 | throw "Impossible condition"; 64 | } 65 | return res; 66 | } 67 | 68 | b58.encode_block = function (data, buf, index) { 69 | if (data.length < 1 || data.length > full_encoded_block_size) { 70 | throw "Invalid block length: " + data.length; 71 | } 72 | var num = uint8_be_to_64(data); 73 | var i = encoded_block_sizes[data.length] - 1; 74 | while (num.cmp(new BN(0)) === 1) { 75 | var remainder = num.mod(new BN(alphabet_size)); 76 | num = num.div(new BN(alphabet_size)); 77 | buf[index + i] = alphabet[remainder.toNumber()]; 78 | i--; 79 | } 80 | return buf; 81 | }; 82 | 83 | b58.encode = function (hex) { 84 | var data = hextobin(hex); 85 | if (data.length === 0) { 86 | return ""; 87 | } 88 | var full_block_count = Math.floor(data.length / full_block_size); 89 | var last_block_size = data.length % full_block_size; 90 | var res_size = full_block_count * full_encoded_block_size + encoded_block_sizes[last_block_size]; 91 | 92 | var res = new Uint8Array(res_size); 93 | var i; 94 | for (i = 0; i < res_size; ++i) { 95 | res[i] = alphabet[0]; 96 | } 97 | for (i = 0; i < full_block_count; i++) { 98 | res = b58.encode_block(data.subarray(i * full_block_size, i * full_block_size + full_block_size), res, i * full_encoded_block_size); 99 | } 100 | if (last_block_size > 0) { 101 | res = b58.encode_block(data.subarray(full_block_count * full_block_size, full_block_count * full_block_size + last_block_size), res, full_block_count * full_encoded_block_size) 102 | } 103 | return bintostr(res); 104 | }; 105 | 106 | return b58; 107 | })(); 108 | 109 | function fastHash(hex) { 110 | return keccak('keccak256').update(Buffer.from(hex, 'hex')).digest('hex'); 111 | } 112 | 113 | const l = new BN(2).pow(new BN(252)).add(new BN("27742317777372353535851937790883648493", 10)); 114 | function hashToScalar(hex) { 115 | let h = fastHash(hex); 116 | let s = elliptic.utils.intFromLE(h); 117 | s = s.umod(l); 118 | return s; 119 | } 120 | 121 | function pointToHex(p) { 122 | return elliptic.utils.toHex(ed25519.encodePoint(p, 'hex')); 123 | } 124 | 125 | function intToLittleEndianUint32Hex(value) { 126 | let h = value.toString(16); 127 | if(h.length>8) throw 'value must not equal or exceed 2^32'; 128 | while(h.length<8) h = '0' + h; 129 | return h.match(/../g).reverse().join(''); 130 | } 131 | 132 | function asciiToHex(str) { 133 | var a = []; 134 | for (var n = 0, l = str.length; n < l; n ++) { 135 | var hex = Number(str.charCodeAt(n)).toString(16); 136 | if(hex.length==1) hex = '0' + hex; 137 | a.push(hex); 138 | } 139 | return a.join(''); 140 | } 141 | 142 | const PUBLIC_ADDRESS_PREFIX_HEX = '12'; 143 | const PUBLIC_SUBADDRESS_PREFIX_HEX = '2a'; 144 | const SUBADDR_HEX = asciiToHex('SubAddr') + '00'; 145 | 146 | function getSubaddressPublicSpendKeyPoint(privateViewKeyBytes, publicSpendKeyBytes, accountIndex, subaddressIndex) { 147 | 148 | let data = SUBADDR_HEX + privateViewKeyBytes + intToLittleEndianUint32Hex(accountIndex) + intToLittleEndianUint32Hex(subaddressIndex); 149 | let m = hashToScalar(data); 150 | let M = ed25519.curve.g.mul(m); 151 | let B = ed25519.decodePoint(publicSpendKeyBytes); 152 | let D = B.add(M); 153 | 154 | return D; 155 | } 156 | 157 | function getSubaddress(privateViewKeyHex, publicSpendKeyHex, accountIndex, subaddressIndex) { 158 | 159 | let addressPrefix; 160 | let C, D; 161 | 162 | if(accountIndex==0 && subaddressIndex==0) { 163 | addressPrefix = PUBLIC_ADDRESS_PREFIX_HEX; 164 | D = ed25519.decodePoint(publicSpendKeyHex); 165 | C = ed25519.curve.g.mul(elliptic.utils.intFromLE(privateViewKeyHex)); 166 | } 167 | else { 168 | addressPrefix = PUBLIC_SUBADDRESS_PREFIX_HEX; 169 | D = getSubaddressPublicSpendKeyPoint(privateViewKeyHex, publicSpendKeyHex, accountIndex, subaddressIndex); 170 | C = D.mul(elliptic.utils.intFromLE(privateViewKeyHex)); 171 | } 172 | 173 | let hex = addressPrefix + pointToHex(D) + pointToHex(C); 174 | let checksumHex = fastHash(hex).substring(0, 8); 175 | hex += checksumHex; 176 | return cnBase58.encode(hex); 177 | 178 | } 179 | 180 | module.exports = {getSubaddress: getSubaddress}; 181 | 182 | 183 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "subaddress", 3 | "version": "1.0.3", 4 | "description": "Monero subaddress utilities", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "./node_modules/.bin/jest --detectOpenHandles" 8 | }, 9 | "jest": { 10 | "collectCoverage": true, 11 | "coverageReporters": [ 12 | "json", 13 | "html" 14 | ], 15 | "notify": true 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/knaccc/subaddress-js.git" 20 | }, 21 | "keywords": [ 22 | "Monero", 23 | "subaddress" 24 | ], 25 | "author": "knaccc", 26 | "license": "BSD-3-Clause", 27 | "bugs": { 28 | "url": "https://github.com/knaccc/subaddress-js/issues" 29 | }, 30 | "homepage": "https://github.com/knaccc/subaddress-js#readme", 31 | "dependencies": { 32 | "bn.js": ">=4.11.8", 33 | "elliptic": ">=6.4.1", 34 | "keccak": ">=2.0.0", 35 | "safe-buffer": ">=5.1.2" 36 | }, 37 | "devDependencies": { 38 | "jest": "^24.0.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | const subaddress = require('../index'); 2 | 3 | const mnemonic = 'vary ambush western rafts session laboratory jerseys napkin muffin exult tolerant ' + 4 | 'efficient ensign fewest sulking wonders pledge alerts acquire tubes rogue outbreak lifestyle lopped pledge'; 5 | 6 | const privSpendKey = '8d8c8eeca38ac3b46aa293fd519b3860e96b5f873c12a95e3e1cdeda0bac4903'; 7 | const pubSpendKeyHex = 'f8631661f6ab4e6fda310c797330d86e23a682f20d5bc8cc27b18051191f16d7'; 8 | 9 | const privViewKeyHex = '99c57d1f0f997bc8ca98559a0ccc3fada3899756e63d1516dba58b7e468cfc05'; 10 | const pubViewKeyHex = '4a1535063ad1fee2dabbf909d4fd9a873e29541b401f0944754e17c9a41820ce'; 11 | 12 | const address = '4B33mFPMq6mKi7Eiyd5XuyKRVMGVZz1Rqb9ZTyGApXW5d1aT7UBDZ89ewmnWFkzJ5wPd2SFbn313vCT8a4E2Qf4KQH4pNey'; 13 | 14 | 15 | const subs = { 16 | account: { 17 | '0': { 18 | 'index': { 19 | '1': '8C5zHM5ud8nGC4hC2ULiBLSWx9infi8JUUmWEat4fcTf8J4H38iWYVdFmPCA9UmfLTZxD43RsyKnGEdZkoGij6csDeUnbEB', 20 | '256': '883z7xonbVBGXpsatJZ53vcDiXQkrkTHUHPxrdrHXiPnZY8DMaYJ7a88C5ovncy5zHWkLc2cQ2hUoaKYCjFtjwFV4vtcpiF' 21 | } 22 | }, 23 | '256': { 24 | 'index': { 25 | '1': '87X4ksVMRv2UGhHcgVjY6KJDjqP9S4zrCNkmomL1ziQVeZXF3RXbAx7i2rRt3UU5eXDzG9TWZ6Rk1Fyg6pZrAKQCNfLrSne', 26 | '256': '86gYdT7yqDJUXegizt1vbF3YKz5qSYVaMB61DFBDzrpVEpYgDbmuXJbXE77LQfAygrVGwYpw8hxxx9DRTiyHAemA8B5yBAq' 27 | } 28 | } 29 | } 30 | }; 31 | 32 | 33 | describe('monero subaddress tests', () => { 34 | 35 | test('should generate the right address for account 0 with index 1', async () => { 36 | const genAddr = subaddress.getSubaddress(privViewKeyHex, pubSpendKeyHex, 0, 1); 37 | expect(genAddr).toEqual(subs.account['0'].index['1']); 38 | }); 39 | 40 | test('should generate the right address for account 0 with index 256', async () => { 41 | const genAddr = subaddress.getSubaddress(privViewKeyHex, pubSpendKeyHex, 0, 256); 42 | expect(genAddr).toEqual(subs.account['0'].index['256']); 43 | }); 44 | 45 | test('should generate the right address for account 256 with index 1', async () => { 46 | const genAddr = subaddress.getSubaddress(privViewKeyHex, pubSpendKeyHex, 256, 1); 47 | expect(genAddr).toEqual(subs.account['256'].index['1']); 48 | }); 49 | 50 | test('should generate the right address for account 256 with index 256', async () => { 51 | const genAddr = subaddress.getSubaddress(privViewKeyHex, pubSpendKeyHex, 256, 256); 52 | expect(genAddr).toEqual(subs.account['256'].index['256']); 53 | }); 54 | }); --------------------------------------------------------------------------------