ADD: wallets message sign/verify

This commit is contained in:
Ivan Vershigora 2021-03-11 13:45:49 +03:00 committed by Overtorment
parent 7367b58716
commit fd61fa6bb2
15 changed files with 69 additions and 64 deletions

View File

@ -1,7 +1,6 @@
import bip39 from 'bip39'; import bip39 from 'bip39';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import b58 from 'bs58check'; import b58 from 'bs58check';
import bitcoinMessage from 'bitcoinjs-message';
import { randomBytes } from '../rng'; import { randomBytes } from '../rng';
import { AbstractHDWallet } from './abstract-hd-wallet'; import { AbstractHDWallet } from './abstract-hd-wallet';
@ -820,12 +819,18 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
return false; return false;
} }
_getIndexByAddress(address) { /**
* Finds WIF corresponding to address and returns it
*
* @param address {string} Address that belongs to this wallet
* @returns {string|false} WIF or false
*/
_getWIFbyAddress(address) {
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
if (this._getExternalAddressByIndex(c) === address) return { internal: false, index: c }; if (this._getExternalAddressByIndex(c) === address) return this._getWIFByIndex(false, c);
} }
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
if (this._getInternalAddressByIndex(c) === address) return { internal: true, index: c }; if (this._getInternalAddressByIndex(c) === address) return this._getWIFByIndex(true, c);
} }
return null; return null;
} }
@ -1107,26 +1112,4 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
return { tx }; return { tx };
} }
signMessage(message, address) {
const search = this._getIndexByAddress(address);
if (search === null) throw new Error('Invalid address');
let options;
switch (this.type) {
case 'HDsegwitBech32':
case 'HDAezeedWallet':
options = { segwitType: 'p2wpkh' };
break;
case 'HDsegwitP2SH':
options = { segwitType: 'p2sh(p2wpkh)' };
break;
}
const wif = this._getWIFByIndex(search.internal, search.index);
const keyPair = bitcoin.ECPair.fromWIF(wif);
const privateKey = keyPair.privateKey;
const signature = bitcoinMessage.sign(message, privateKey, keyPair.compressed, options);
return signature.toString('base64');
}
} }

View File

@ -19,6 +19,7 @@ export class AbstractWallet {
constructor() { constructor() {
this.type = this.constructor.type; this.type = this.constructor.type;
this.typeReadable = this.constructor.typeReadable; this.typeReadable = this.constructor.typeReadable;
this.segwitType = this.constructor.segwitType;
this.label = ''; this.label = '';
this.secret = ''; // private key or recovery phrase this.secret = ''; // private key or recovery phrase
this.balance = 0; this.balance = 0;

View File

@ -15,6 +15,7 @@ const { CipherSeed } = require('aezeed');
export class HDAezeedWallet extends AbstractHDElectrumWallet { export class HDAezeedWallet extends AbstractHDElectrumWallet {
static type = 'HDAezeedWallet'; static type = 'HDAezeedWallet';
static typeReadable = 'HD Aezeed'; static typeReadable = 'HD Aezeed';
static segwitType = 'p2wpkh';
setSecret(newSecret) { setSecret(newSecret) {
this.secret = newSecret.trim(); this.secret = newSecret.trim();

View File

@ -8,6 +8,7 @@ import { AbstractHDElectrumWallet } from './abstract-hd-electrum-wallet';
export class HDSegwitBech32Wallet extends AbstractHDElectrumWallet { export class HDSegwitBech32Wallet extends AbstractHDElectrumWallet {
static type = 'HDsegwitBech32'; static type = 'HDsegwitBech32';
static typeReadable = 'HD SegWit (BIP84 Bech32 Native)'; static typeReadable = 'HD SegWit (BIP84 Bech32 Native)';
static segwitType = 'p2wpkh';
allowSend() { allowSend() {
return true; return true;

View File

@ -12,6 +12,7 @@ const HDNode = require('bip32');
export class HDSegwitP2SHWallet extends AbstractHDElectrumWallet { export class HDSegwitP2SHWallet extends AbstractHDElectrumWallet {
static type = 'HDsegwitP2SH'; static type = 'HDsegwitP2SH';
static typeReadable = 'HD SegWit (BIP49 P2SH)'; static typeReadable = 'HD SegWit (BIP49 P2SH)';
static segwitType = 'p2sh(p2wpkh)';
allowSend() { allowSend() {
return true; return true;

View File

@ -495,23 +495,41 @@ export class LegacyWallet extends AbstractWallet {
return false; return false;
} }
signMessage(message) { /**
let options; * Finds WIF corresponding to address and returns it
switch (this.type) { *
case 'segwitBech32': * @param address {string} Address that belongs to this wallet
options = { segwitType: 'p2wpkh' }; * @returns {string|false} WIF or false
break; */
case 'segwitP2SH': _getWIFbyAddress(address) {
options = { segwitType: 'p2sh(p2wpkh)' }; return this.getAddress() === address ? this.secret : null;
break; }
}
const keyPair = bitcoin.ECPair.fromWIF(this.secret); /**
* Signes text message using address private key and returs signature
*
* @param message {string}
* @param address {string}
* @returns {string} base64 encoded signature
*/
signMessage(message, address) {
const wif = this._getWIFbyAddress(address);
if (wif === null) throw new Error('Invalid address');
const keyPair = bitcoin.ECPair.fromWIF(wif);
const privateKey = keyPair.privateKey; const privateKey = keyPair.privateKey;
const options = this.segwitType ? { segwitType: this.segwitType } : undefined;
const signature = bitcoinMessage.sign(message, privateKey, keyPair.compressed, options); const signature = bitcoinMessage.sign(message, privateKey, keyPair.compressed, options);
return signature.toString('base64'); return signature.toString('base64');
} }
/**
* Verifies text message signature by address
*
* @param message {string}
* @param address {string}
* @param signature {string}
* @returns {boolean} base64 encoded signature
*/
verifyMessage(message, address, signature) { verifyMessage(message, address, signature) {
// null, true so it can verify Electrum signatores without errors // null, true so it can verify Electrum signatores without errors
return bitcoinMessage.verify(message, address, signature, null, true); return bitcoinMessage.verify(message, address, signature, null, true);

View File

@ -4,6 +4,7 @@ const bitcoin = require('bitcoinjs-lib');
export class SegwitBech32Wallet extends LegacyWallet { export class SegwitBech32Wallet extends LegacyWallet {
static type = 'segwitBech32'; static type = 'segwitBech32';
static typeReadable = 'P2 WPKH'; static typeReadable = 'P2 WPKH';
static segwitType = 'p2wpkh';
getAddress() { getAddress() {
if (this._address) return this._address; if (this._address) return this._address;

View File

@ -19,6 +19,7 @@ function pubkeyToP2shSegwitAddress(pubkey, network) {
export class SegwitP2SHWallet extends LegacyWallet { export class SegwitP2SHWallet extends LegacyWallet {
static type = 'segwitP2SH'; static type = 'segwitP2SH';
static typeReadable = 'SegWit (P2SH)'; static typeReadable = 'SegWit (P2SH)';
static segwitType = 'p2sh(p2wpkh)';
static witnessToAddress(witness) { static witnessToAddress(witness) {
try { try {

View File

@ -95,13 +95,13 @@ describe('HDAezeedWallet', () => {
let signature; let signature;
// external address // external address
signature = aezeed.signMessage('vires is numeris', 'bc1qdjj7lhj9lnjye7xq3dzv3r4z0cta294xy78txn'); signature = aezeed.signMessage('vires is numeris', aezeed._getExternalAddressByIndex(0));
assert.strictEqual(signature, 'J9zF7mdGGdc/9HMlvor6Zl7ap1qseQpiBDJ4oaSpkzbQGGhdfkM6LHo6m9BV8o/BlqiQI1vuODaNlBFyeyIWgfE='); assert.strictEqual(signature, 'J9zF7mdGGdc/9HMlvor6Zl7ap1qseQpiBDJ4oaSpkzbQGGhdfkM6LHo6m9BV8o/BlqiQI1vuODaNlBFyeyIWgfE=');
assert.strictEqual(aezeed.verifyMessage('vires is numeris', 'bc1qdjj7lhj9lnjye7xq3dzv3r4z0cta294xy78txn', signature), true); assert.strictEqual(aezeed.verifyMessage('vires is numeris', aezeed._getExternalAddressByIndex(0), signature), true);
// internal address // internal address
signature = aezeed.signMessage('vires is numeris', 'bc1qzyjq8sjj56n8v9fgw5klsc8sq8yuy0jx03hzzp'); signature = aezeed.signMessage('vires is numeris', aezeed._getInternalAddressByIndex(0));
assert.strictEqual(signature, 'KIda06aSswmo9NiAYNUBRADA9q1v39raSmHHVg56+thtah5xL7hVw/x+cZgydFNyel2bXfyGluJRaP1uRQfJtzo='); assert.strictEqual(signature, 'KIda06aSswmo9NiAYNUBRADA9q1v39raSmHHVg56+thtah5xL7hVw/x+cZgydFNyel2bXfyGluJRaP1uRQfJtzo=');
assert.strictEqual(aezeed.verifyMessage('vires is numeris', 'bc1qzyjq8sjj56n8v9fgw5klsc8sq8yuy0jx03hzzp', signature), true); assert.strictEqual(aezeed.verifyMessage('vires is numeris', aezeed._getInternalAddressByIndex(0), signature), true);
}); });
}); });

View File

@ -140,13 +140,13 @@ describe('Legacy HD (BIP44)', () => {
let signature; let signature;
// external address // external address
signature = hd.signMessage('vires is numeris', '1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA'); signature = hd.signMessage('vires is numeris', hd._getExternalAddressByIndex(0));
assert.strictEqual(signature, 'H5J8DbqvuBy8lqRW7+LTVrrtrsaqLSwRDyj+5XtCrZpdCgPlxKM4EKRD6qvdKeyEh1fiSfIVB/edPAum3gKcJZo='); assert.strictEqual(signature, 'H5J8DbqvuBy8lqRW7+LTVrrtrsaqLSwRDyj+5XtCrZpdCgPlxKM4EKRD6qvdKeyEh1fiSfIVB/edPAum3gKcJZo=');
assert.strictEqual(hd.verifyMessage('vires is numeris', '1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA', signature), true); assert.strictEqual(hd.verifyMessage('vires is numeris', hd._getExternalAddressByIndex(0), signature), true);
// internal address // internal address
signature = hd.signMessage('vires is numeris', '1J3J6EvPrv8q6AC3VCjWV45Uf3nssNMRtH'); signature = hd.signMessage('vires is numeris', hd._getInternalAddressByIndex(0));
assert.strictEqual(signature, 'H98hmvtyPFUbR6E5Tcsqmc+eSjlYhP2vy41Y6IyHS9DVKEI5n8VEMpIEDtvlMARVce96nOqbRHXo9nD05WXH/Eo='); assert.strictEqual(signature, 'H98hmvtyPFUbR6E5Tcsqmc+eSjlYhP2vy41Y6IyHS9DVKEI5n8VEMpIEDtvlMARVce96nOqbRHXo9nD05WXH/Eo=');
assert.strictEqual(hd.verifyMessage('vires is numeris', '1J3J6EvPrv8q6AC3VCjWV45Uf3nssNMRtH', signature), true); assert.strictEqual(hd.verifyMessage('vires is numeris', hd._getInternalAddressByIndex(0), signature), true);
}); });
}); });

View File

@ -112,25 +112,25 @@ describe('Bech32 Segwit HD (BIP84)', () => {
let signature; let signature;
// external address // external address
signature = hd.signMessage('vires is numeris', 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu'); signature = hd.signMessage('vires is numeris', hd._getExternalAddressByIndex(0));
assert.strictEqual(signature, 'KGW4FfrptS9zV3UptUWxbEf65GhC2mCUz86G0GpN/H4MUC29Y5TsRhWGIqG2lettEpZXZETuc2yL+O7/UvDhxhM='); assert.strictEqual(signature, 'KGW4FfrptS9zV3UptUWxbEf65GhC2mCUz86G0GpN/H4MUC29Y5TsRhWGIqG2lettEpZXZETuc2yL+O7/UvDhxhM=');
assert.strictEqual(hd.verifyMessage('vires is numeris', 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu', signature), true); assert.strictEqual(hd.verifyMessage('vires is numeris', hd._getExternalAddressByIndex(0), signature), true);
// internal address // internal address
signature = hd.signMessage('vires is numeris', 'bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el'); signature = hd.signMessage('vires is numeris', hd._getInternalAddressByIndex(0));
assert.strictEqual(signature, 'KJ5B9JkZ042FhtGeObU/MxLCzQWHbrpXNQxhfJj9wMboa/icLIIaAlsKaSkS27fZLvX3WH0qyj3aAaXscnWsfSw='); assert.strictEqual(signature, 'KJ5B9JkZ042FhtGeObU/MxLCzQWHbrpXNQxhfJj9wMboa/icLIIaAlsKaSkS27fZLvX3WH0qyj3aAaXscnWsfSw=');
assert.strictEqual(hd.verifyMessage('vires is numeris', 'bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el', signature), true); assert.strictEqual(hd.verifyMessage('vires is numeris', hd._getInternalAddressByIndex(0), signature), true);
// multiline message // multiline message
signature = hd.signMessage('vires\nis\nnumeris', 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu'); signature = hd.signMessage('vires\nis\nnumeris', hd._getExternalAddressByIndex(0));
assert.strictEqual(signature, 'KFI22tlJVGq2HGQM5rcBtYu+Jq8oc7QyjSBP1ZQup3a/GEw1Khu2qFbL/iLzqw95wN22a/Tll1oMLdWxg9cWMYM='); assert.strictEqual(signature, 'KFI22tlJVGq2HGQM5rcBtYu+Jq8oc7QyjSBP1ZQup3a/GEw1Khu2qFbL/iLzqw95wN22a/Tll1oMLdWxg9cWMYM=');
assert.strictEqual(hd.verifyMessage('vires\nis\nnumeris', 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu', signature), true); assert.strictEqual(hd.verifyMessage('vires\nis\nnumeris', hd._getExternalAddressByIndex(0), signature), true);
// can't sign if address doesn't belong to wallet // can't sign if address doesn't belong to wallet
assert.throws(() => hd.signMessage('vires is numeris', '186FBQmCV5W1xY7ywaWtTZPAQNciVN8Por')); assert.throws(() => hd.signMessage('vires is numeris', '186FBQmCV5W1xY7ywaWtTZPAQNciVN8Por'));
// can't verify wrong signature // can't verify wrong signature
assert.throws(() => hd.verifyMessage('vires is numeris', 'bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el', 'wrong signature')); assert.throws(() => hd.verifyMessage('vires is numeris', hd._getInternalAddressByIndex(0), 'wrong signature'));
// can verify electrum message signature // can verify electrum message signature
// bech32 segwit (p2wpkh) // bech32 segwit (p2wpkh)

View File

@ -173,13 +173,13 @@ describe('P2SH Segwit HD (BIP49)', () => {
let signature; let signature;
// external address // external address
signature = hd.signMessage('vires is numeris', '37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf'); signature = hd.signMessage('vires is numeris', hd._getExternalAddressByIndex(0));
assert.strictEqual(signature, 'JMgoRSlLLLw6mw/Gbbg8Uj3fACkIJ85CZ52T5ZQfBnpUBkz0myRju6Rmgvmq7ugytc4WyYbzdGEc3wufNbjP09g='); assert.strictEqual(signature, 'JMgoRSlLLLw6mw/Gbbg8Uj3fACkIJ85CZ52T5ZQfBnpUBkz0myRju6Rmgvmq7ugytc4WyYbzdGEc3wufNbjP09g=');
assert.strictEqual(hd.verifyMessage('vires is numeris', '37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf', signature), true); assert.strictEqual(hd.verifyMessage('vires is numeris', hd._getExternalAddressByIndex(0), signature), true);
// internal address // internal address
signature = hd.signMessage('vires is numeris', '34K56kSjgUCUSD8GTtuF7c9Zzwokbs6uZ7'); signature = hd.signMessage('vires is numeris', hd._getInternalAddressByIndex(0));
assert.strictEqual(signature, 'I5WkniWTnJhTW74t3kTAkHq3HdiupTNgOZLpMp0hvUfAJw2HMuyRiNLl2pbNWobNCCrmvffSWM7IgkOBz/J9fYA='); assert.strictEqual(signature, 'I5WkniWTnJhTW74t3kTAkHq3HdiupTNgOZLpMp0hvUfAJw2HMuyRiNLl2pbNWobNCCrmvffSWM7IgkOBz/J9fYA=');
assert.strictEqual(hd.verifyMessage('vires is numeris', '34K56kSjgUCUSD8GTtuF7c9Zzwokbs6uZ7', signature), true); assert.strictEqual(hd.verifyMessage('vires is numeris', hd._getInternalAddressByIndex(0), signature), true);
}); });
}); });

View File

@ -73,8 +73,8 @@ describe('Legacy wallet', () => {
const l = new LegacyWallet(); const l = new LegacyWallet();
l.setSecret('L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1'); // from bitcoinjs-message examples l.setSecret('L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1'); // from bitcoinjs-message examples
const signature = l.signMessage('This is an example of a signed message.'); const signature = l.signMessage('This is an example of a signed message.', l.getAddress());
assert.strictEqual(signature, 'H9L5yLFjti0QTHhPyFrZCT1V/MMnBtXKmoiKDZ78NDBjERki6ZTQZdSMCtkgoNmp17By9ItJr8o7ChX0XxY91nk='); assert.strictEqual(signature, 'H9L5yLFjti0QTHhPyFrZCT1V/MMnBtXKmoiKDZ78NDBjERki6ZTQZdSMCtkgoNmp17By9ItJr8o7ChX0XxY91nk=');
assert.strictEqual(l.verifyMessage('This is an example of a signed message.', '1F3sAm6ZtwLAUnj7d38pGFxtP3RVEvtsbV', signature), true); assert.strictEqual(l.verifyMessage('This is an example of a signed message.', l.getAddress(), signature), true);
}); });
}); });

View File

@ -41,10 +41,8 @@ describe('Segwit P2SH wallet', () => {
const l = new SegwitBech32Wallet(); const l = new SegwitBech32Wallet();
l.setSecret('L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1'); // from bitcoinjs-message examples l.setSecret('L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1'); // from bitcoinjs-message examples
console.info('add', l.getAddress()) const signature = l.signMessage('This is an example of a signed message.', l.getAddress());
const signature = l.signMessage('This is an example of a signed message.');
assert.strictEqual(signature, 'J9L5yLFjti0QTHhPyFrZCT1V/MMnBtXKmoiKDZ78NDBjERki6ZTQZdSMCtkgoNmp17By9ItJr8o7ChX0XxY91nk='); assert.strictEqual(signature, 'J9L5yLFjti0QTHhPyFrZCT1V/MMnBtXKmoiKDZ78NDBjERki6ZTQZdSMCtkgoNmp17By9ItJr8o7ChX0XxY91nk=');
assert.strictEqual(l.verifyMessage('This is an example of a signed message.', 'bc1qngw83fg8dz0k749cg7k3emc7v98wy0c74dlrkd', signature), true); assert.strictEqual(l.verifyMessage('This is an example of a signed message.', l.getAddress(), signature), true);
}); });
}); });

View File

@ -42,8 +42,8 @@ describe('Segwit P2SH wallet', () => {
const l = new SegwitP2SHWallet(); const l = new SegwitP2SHWallet();
l.setSecret('L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1'); // from bitcoinjs-message examples l.setSecret('L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1'); // from bitcoinjs-message examples
const signature = l.signMessage('This is an example of a signed message.'); const signature = l.signMessage('This is an example of a signed message.', l.getAddress());
assert.strictEqual(signature, 'I9L5yLFjti0QTHhPyFrZCT1V/MMnBtXKmoiKDZ78NDBjERki6ZTQZdSMCtkgoNmp17By9ItJr8o7ChX0XxY91nk='); assert.strictEqual(signature, 'I9L5yLFjti0QTHhPyFrZCT1V/MMnBtXKmoiKDZ78NDBjERki6ZTQZdSMCtkgoNmp17By9ItJr8o7ChX0XxY91nk=');
assert.strictEqual(l.verifyMessage('This is an example of a signed message.', '3DnW8JGpPViEZdpqat8qky1zc26EKbXnmM', signature), true); assert.strictEqual(l.verifyMessage('This is an example of a signed message.', l.getAddress(), signature), true);
}); });
}); });