Skip to content

Commit

Permalink
Sign and Verify exposed to Bob3 (#15)
Browse files Browse the repository at this point in the history
* Sign and Verify exposed to Bob3

* verifymessagewithname will now check if namestate is CLOSED as per discussion seen on discord

* add back lock wallet

* add confirm ux for sign message

Co-authored-by: realrasengan <root@freenode.net>
Co-authored-by: Chi Kei Chan <chikeichan@gmail.com>
  • Loading branch information
3 people committed Jan 10, 2022
1 parent fb0c365 commit 73194e1
Show file tree
Hide file tree
Showing 11 changed files with 28,815 additions and 44 deletions.
28,413 changes: 28,402 additions & 11 deletions package-lock.json

Large diffs are not rendered by default.

58 changes: 58 additions & 0 deletions src/background/controllers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,52 @@ const controllers: {
})
},

[MessageTypes.SIGN_MESSAGE]: async (app, message) => {
const {payload} = message;
const {address, msg} = payload;
return new Promise(async (resolve, reject) => {
const queue = await app.exec('wallet', 'getTxQueue');

if (queue.length) {
return reject(new Error('user has unconfirmed tx.'));
}

app.exec('analytics', 'track', {
name: 'Bob3 Sign',
});

const requestJson = await app.exec('wallet', 'createSignMessageRequest', msg, address);

await app.exec('wallet', 'addTxToQueue', requestJson);

const popup = await openPopup();
closePopupOnAcceptOrReject(app, resolve, reject, popup);
});
},

[MessageTypes.SIGN_MESSAGE_WITH_NAME]: async (app, message) => {
const {payload} = message;
const {name, msg} = payload;
return new Promise(async (resolve, reject) => {
const queue = await app.exec('wallet', 'getTxQueue');

if (queue.length) {
return reject(new Error('user has unconfirmed tx.'));
}

app.exec('analytics', 'track', {
name: 'Bob3 Sign with Name',
});

const requestJson = await app.exec('wallet', 'createSignMessageRequest', msg, undefined, name);

await app.exec('wallet', 'addTxToQueue', requestJson);

const popup = await openPopup();
closePopupOnAcceptOrReject(app, resolve, reject, popup);
});
},

[MessageTypes.SEND_TX]: async (app, message) => {
const {payload} = message;
const {amount, address} = payload;
Expand Down Expand Up @@ -394,6 +440,18 @@ const controllers: {
return app.exec('node', 'getLatestBlock');
},

[MessageTypes.VERIFY_MESSAGE]: async (app, message) => {
const {payload} = message;
const {msg,signature,address} = payload;
return app.exec('node', 'verifyMessage', msg, signature, address);
},

[MessageTypes.VERIFY_MESSAGE_WITH_NAME]: async (app, message) => {
const {payload} = message;
const {msg,signature,name} = payload;
return app.exec('node', 'verifyMessageWithName', msg, signature, name);
},

[MessageTypes.GET_API]: async (app, message) => {
return app.exec('setting', 'getAPI');
},
Expand Down
52 changes: 52 additions & 0 deletions src/background/services/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ const bdb = require('bdb');
const DB = require('bdb/lib/db');
const rules = require("hsd/lib/covenants/rules");
import {get, put} from '@src/util/db';
const {states,statesByVal} = require('hsd/lib/covenants/namestate');
const Network = require("hsd/lib/protocol/network");
const networkType = process.env.NETWORK_TYPE || 'main';

const NAME_CACHE: string[] = [];
const NAME_MAP: { [hash: string]: string } = {};
export default class NodeService extends GenericService {
store: typeof DB;
network: typeof Network;

async getHeaders(): Promise<any> {
const { apiHost, apiKey } = await this.exec('setting', 'getAPI');
Expand Down Expand Up @@ -129,6 +133,53 @@ export default class NodeService extends GenericService {
return name;
}

async verifyMessage(msg: string, signature: string, address: string) {
if(!msg || !signature || !address) {
throw new Error('Required paremeters include msg as a string, signature as a string, and address as a string.');
}

const headers = await this.getHeaders();
const result = await this.fetch(null, {
method: 'POST',
headers: headers,
body: JSON.stringify({
method: 'verifymessage',
params: [address, signature, msg]
}),
});
if(result.error) {
throw new Error('Error when verifymessage');
}
else {
return result.result;
}
}

async verifyMessageWithName(msg: string, signature: string, name: string) {
if(!msg || !signature || !name) {
throw new Error('Required paremeters include msg as a string, signature as a string, and name as a string.');
}
if(!rules.verifyName(name))
throw new Error('Invalid name.');

const ni = await this.getNameInfo(name);
const ownerHash = ni.result.info.owner.hash;
const ownerIndex = ni.result.info.owner.index;
const state = ni.result.info.state;

if(!ownerHash)
throw new Error('Could not find owner');
else if(state!==statesByVal[states.CLOSED])
throw new Error('Invalid name state.');

const address = await this.getCoin(ownerHash, ownerIndex);

if(!address)
throw new Error('Could not find owner');

return await this.verifyMessage(msg, signature, address.address);
}

async getNameInfo(tld: string) {
const headers = await this.getHeaders();
return this.fetch(null, {
Expand Down Expand Up @@ -225,6 +276,7 @@ export default class NodeService extends GenericService {
async start() {
this.store = bdb.create('/node-store');
await this.store.open();
this.network = Network.get(networkType);
}

async stop() {
Expand Down
155 changes: 141 additions & 14 deletions src/background/services/wallet/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {GenericService} from "@src/util/svc";
import crypto from 'crypto';
const Mnemonic = require('hsd/lib/hd/mnemonic');
const WalletDB = require("hsd/lib/wallet/walletdb");
const Network = require("hsd/lib/protocol/network");
Expand All @@ -19,11 +20,18 @@ const bdb = require('bdb');
const DB = require('bdb/lib/db');
const layout = require('hsd/lib/wallet/layout').txdb;
const {Resource} = require('hsd/lib/dns/resource');
const blake2b = require('bcrypto/lib/blake2b');
import {get, put} from '@src/util/db';
import pushMessage from "@src/util/pushMessage";
import {ActionType as WalletActionType, setWalletBalance} from "@src/ui/ducks/wallet";
import {ActionType as AppActionType} from "@src/ui/ducks/app";
import {ActionType, setTransactions, Transaction} from "@src/ui/ducks/transactions";
import {
ActionType,
setTransactions,
SIGN_MESSAGE_METHOD,
SIGN_MESSAGE_WITH_NAME_METHOD, SignMessageRequest,
Transaction
} from "@src/ui/ducks/transactions";
import {ActionTypes, setDomainNames} from "@src/ui/ducks/domains";
import {ActionType as QueueActionType, setTXQueue } from "@src/ui/ducks/queue";
import {toDollaryDoos} from "@src/util/number";
Expand All @@ -36,6 +44,7 @@ const {types, typesByVal} = rules;

const networkType = process.env.NETWORK_TYPE || 'main';
const LOOKAHEAD = 100;
const MAGIC_STRING = `handshake signed message:\n`;

export default class WalletService extends GenericService {
network: typeof Network;
Expand Down Expand Up @@ -1058,7 +1067,7 @@ export default class WalletService extends GenericService {
});
};

submitTx = async (opts: {txJSON: Transaction; password: string}) => {
submitTx = async (opts: {txJSON: Transaction|SignMessageRequest; password: string}) => {
const walletId = this.selectedID;
const wallet = await this.wdb.get(walletId);

Expand All @@ -1071,23 +1080,43 @@ export default class WalletService extends GenericService {
}
});

const latestBlockNow = await this.exec('node', 'getLatestBlock');
this.wdb.height = latestBlockNow.height;
const mtx = MTX.fromJSON(opts.txJSON);
const tx = await wallet.sendMTX(mtx, this.passphrase);
let returnValue;

if (opts.txJSON.method === SIGN_MESSAGE_WITH_NAME_METHOD) {
returnValue = await this.signMessageWithName(opts.txJSON.data.name!, opts.txJSON.data.message);
}

if (opts.txJSON.method === SIGN_MESSAGE_METHOD) {
returnValue = await this.signMessage(opts.txJSON.data.address!, opts.txJSON.data.message);
}

if (!opts.txJSON.method) {
const latestBlockNow = await this.exec('node', 'getLatestBlock');
this.wdb.height = latestBlockNow.height;
const mtx = MTX.fromJSON(opts.txJSON);
const tx = await wallet.sendMTX(mtx, this.passphrase);
await this.exec('node', 'sendRawTransaction', tx.toHex());
returnValue = tx.getJSON(this.network);
}

await this.removeTxFromQueue(opts.txJSON);
await this.exec('node', 'sendRawTransaction', tx.toHex());
const json = tx.getJSON(this.network);
this.emit('txAccepted', json);
return json;
this.emit('txAccepted', returnValue);
return returnValue;
};

async _addOutputPathToTxQueue(queue: Transaction[]): Promise<Transaction[]> {
async _addOutputPathToTxQueue(queue: Transaction[]|SignMessageRequest[]): Promise<Transaction[]|SignMessageRequest[]> {
for (let i = 0; i < queue.length; i++) {
const tx = queue[i];
for (let outputIndex = 0; outputIndex < tx.outputs.length; outputIndex++) {
const output = tx.outputs[outputIndex];
output.owned = await this.hasAddress(output.address);

if (tx.method) {
continue;
}

if (!tx.method) {
for (let outputIndex = 0; outputIndex < tx.outputs.length; outputIndex++) {
const output = tx.outputs[outputIndex];
output.owned = await this.hasAddress(output.address);
}
}
}

Expand Down Expand Up @@ -1220,6 +1249,104 @@ export default class WalletService extends GenericService {
}
};

createSignMessageRequest = async (message: string, address?: string, name?: string): Promise<SignMessageRequest> => {
const walletId = this.selectedID;

if (typeof address === 'string') {
return {
hash: crypto.createHash('sha256').update(Buffer.from(address + message + Date.now()).toString('hex')).digest('hex'),
method: SIGN_MESSAGE_METHOD,
walletId: walletId,
data: {
address,
message,
},
bid: undefined,
height: 0,
};
}

if (typeof name === 'string') {
return {
hash: crypto.createHash('sha256').update(Buffer.from(name + message + Date.now()).toString('hex')).digest('hex'),
method: SIGN_MESSAGE_WITH_NAME_METHOD,
walletId: walletId,
data: {
name,
message,
},
bid: undefined,
height: 0,
}
}

throw new Error('name or address must be present');
};

signMessage = async(address: string, msg: string): Promise<string> => {
if(!address || !msg) {
throw new Error('Requires parameters address of type string and msg of type string.');
}

const walletId = this.selectedID;
const wallet = await this.wdb.get(walletId);

try {
await wallet.unlock(this.passphrase, 60000);
const key = await wallet.getKey(Address.from(address));

if(!key) {
throw new Error('Address not found.');
}

if(!wallet.master.key) {
throw new Error('Wallet is locked');
}

const _msg = Buffer.from(MAGIC_STRING + msg, 'utf8');
const hash = blake2b.digest(_msg);

const sig = key.sign(hash);

return sig.toString('base64');
} finally {
await wallet.lock();
}
};

signMessageWithName = async (name: string, msg: string): Promise<string> => {
if (!name || !msg) {
throw new Error('Requires parameters name of type string and msg of type string.');
} else if (!rules.verifyName(name)) {
throw new Error('Requires valid name per Handshake protocol rules.');
}

const walletId = this.selectedID;
const wallet = await this.wdb.get(walletId);

try {
await wallet.unlock(this.passphrase, 60000);

const ns = await wallet.getNameStateByName(name);

if(!ns || !ns.owner) {
throw new Error('Cannot find the name owner.');
}

const coin = await wallet.getCoin(ns.owner.hash, ns.owner.index);

if(!coin) {
throw new Error('Cannot find the address of the name owner.');
}

const address = coin.address.toString(this.network);

return this.signMessage(address, msg);
} finally {
await wallet.lock();
}
};

async shouldContinue() {
if (this.forceStopRescan) {
this.forceStopRescan = false;
Expand Down

0 comments on commit 73194e1

Please sign in to comment.