ScannerStore.ts 16.4 KB
Newer Older
Thibaut Sardan's avatar
Thibaut Sardan committed
1
// Copyright 2015-2019 Parity Technologies (UK) Ltd.
Alexey's avatar
Alexey committed
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// This file is part of Parity.

// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with Parity.  If not, see <http://www.gnu.org/licenses/>.
16
import { GenericExtrinsicPayload } from '@polkadot/types';
YJ's avatar
YJ committed
17
18
19
20
21
22
23
import {
	hexStripPrefix,
	hexToU8a,
	isU8a,
	u8aToHex,
	u8aConcat
} from '@polkadot/util';
24
import { decodeAddress, encodeAddress } from '@polkadot/util-crypto';
Alexey's avatar
Alexey committed
25
import { Container } from 'unstated';
26
27
import { ExtrinsicPayload } from '@polkadot/types/interfaces';

28
import AccountsStore from 'stores/AccountsStore';
29
30
31
32
import {
	NETWORK_LIST,
	NetworkProtocols,
	SUBSTRATE_NETWORK_LIST
33
34
} from 'constants/networkSpecs';
import { isAscii } from 'utils/strings';
35
36
37
38
39
40
import {
	brainWalletSign,
	decryptData,
	keccak,
	ethSign,
	substrateSign
41
42
43
} from 'utils/native';
import { mod } from 'utils/numbers';
import transaction, { Transaction } from 'utils/transaction';
YJ's avatar
YJ committed
44
45
46
47
import {
	constructDataFromBytes,
	asciiToHex,
	encodeNumber
48
49
50
51
52
53
54
55
56
57
58
59
} from 'utils/decoders';
import { Account, FoundAccount } from 'types/identityTypes';
import { constructSURI } from 'utils/suri';
import { emptyAccount } from 'utils/account';
import {
	CompletedParsedData,
	EthereumParsedData,
	isEthereumCompletedParsedData,
	isMultipartData,
	SubstrateCompletedParsedData
} from 'types/scannerTypes';
import { NetworkProtocol } from 'types/networkSpecsTypes';
Alexey's avatar
Alexey committed
60

61
type TXRequest = Record<string, any>;
Alexey's avatar
Alexey committed
62
63

type SignedTX = {
64
65
66
	recipient: Account;
	sender: Account;
	txRequest: TXRequest;
Alexey's avatar
Alexey committed
67
68
};

69
type MultipartData = {
70
	[x: string]: Uint8Array;
71
72
};

Alexey's avatar
Alexey committed
73
type ScannerState = {
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
	busy: boolean;
	completedFramesCount: number;
	dataToSign: string | GenericExtrinsicPayload;
	isHash: boolean;
	isOversized: boolean;
	latestFrame: number | null;
	message: string | null;
	missedFrames: Array<number>;
	multipartData: MultipartData;
	multipartComplete: boolean;
	prehash: GenericExtrinsicPayload | null;
	recipient: FoundAccount | null;
	scanErrorMsg: string;
	sender: FoundAccount | null;
	signedData: string;
	signedTxList: SignedTX[];
	totalFrameCount: number;
	tx: Transaction | GenericExtrinsicPayload | string | Uint8Array | null;
	txRequest: TXRequest | null;
	type: 'transaction' | 'message' | null;
	unsignedData: CompletedParsedData | null;
Alexey's avatar
Alexey committed
95
96
};

97
const DEFAULT_STATE = Object.freeze({
98
	busy: false,
YJ's avatar
YJ committed
99
	completedFramesCount: 0,
100
101
102
	dataToSign: '',
	isHash: false,
	isOversized: false,
103
	latestFrame: null,
104
	message: null,
105
	missedFrames: [],
YJ's avatar
YJ committed
106
	multipartComplete: false,
107
	multipartData: {},
108
	prehash: null,
109
110
111
112
	recipient: null,
	scanErrorMsg: '',
	sender: null,
	signedData: '',
113
	signedTxList: [],
YJ's avatar
YJ committed
114
	totalFrameCount: 0,
115
	tx: null,
116
117
118
	txRequest: null,
	type: null,
	unsignedData: null
119
});
Alexey's avatar
Alexey committed
120

YJ's avatar
YJ committed
121
122
const MULTIPART = new Uint8Array([0]); // always mark as multipart for simplicity's sake. Consistent with @polkadot/react-qr

YJ's avatar
YJ committed
123
124
125
126
127
// const SIG_TYPE_NONE = new Uint8Array();
// const SIG_TYPE_ED25519 = new Uint8Array([0]);
const SIG_TYPE_SR25519 = new Uint8Array([1]);
// const SIG_TYPE_ECDSA = new Uint8Array([2]);

Alexey's avatar
Alexey committed
128
export default class ScannerStore extends Container<ScannerState> {
129
	state: ScannerState = DEFAULT_STATE;
130

131
	async setUnsigned(data: string): Promise<void> {
132
133
134
135
136
		this.setState({
			unsignedData: JSON.parse(data)
		});
	}

137
138
139
140
141
	/*
	 * @param strippedData: the rawBytes from react-native-camera, stripped of the ec11 padding to fill the frame size. See: decoders.js
	 * N.B. Substrate oversized/multipart payloads will already be hashed at this point.
	 */

142
143
144
145
146
	async setParsedData(
		strippedData: Uint8Array,
		accountsStore: AccountsStore,
		multipartComplete = false
	): Promise<void> {
YJ's avatar
YJ committed
147
148
149
150
		const parsedData = await constructDataFromBytes(
			strippedData,
			multipartComplete
		);
151

152
		if (isMultipartData(parsedData)) {
YJ's avatar
YJ committed
153
154
155
156
157
158
159
160
161
			this.setPartData(
				parsedData.currentFrame,
				parsedData.frameCount,
				parsedData.partData,
				accountsStore
			);
			return;
		}

162
		if (accountsStore.getAccountByAddress(parsedData.data.account)) {
YJ's avatar
YJ committed
163
164
165
166
167
168
			this.setState({
				unsignedData: parsedData
			});
		} else {
			// If the address is not found on device in its current encoding,
			// try decoding the public key and encoding it to all the other known network prefixes.
169
			const networks = Object.keys(SUBSTRATE_NETWORK_LIST);
170
171

			for (let i = 0; i < networks.length; i++) {
172
173
				const key = networks[i];
				const account = accountsStore.getAccountByAddress(
174
175
176
177
178
179
180
181
					encodeAddress(
						decodeAddress(parsedData.data.account),
						SUBSTRATE_NETWORK_LIST[key].prefix
					)
				);

				if (account) {
					parsedData.data.account = account.address;
182

YJ's avatar
YJ committed
183
184
185
186
					this.setState({
						unsignedData: parsedData
					});
					return;
187
188
189
				}
			}

YJ's avatar
YJ committed
190
191
192
			// if the account was not found, unsignedData was never set, alert the user appropriately.
			this.setErrorMsg(
				`No private key found for ${parsedData.data.account} in your signer key storage.`
193
194
			);
		}
195
196
197

		// set payload before it got hashed.
		// signature will be generated from the hash, but we still want to display it.
198
199
200
201
202
		if (parsedData.hasOwnProperty('preHash')) {
			this.setPrehashPayload(
				(parsedData as SubstrateCompletedParsedData).preHash
			);
		}
203
204
	}

205
206
207
208
209
210
	async setPartData(
		frame: number,
		frameCount: number,
		partData: string,
		accountsStore: AccountsStore
	): Promise<boolean | void | Uint8Array> {
211
212
213
214
215
216
217
		const {
			latestFrame,
			missedFrames,
			multipartComplete,
			multipartData,
			totalFrameCount
		} = this.state;
YJ's avatar
YJ committed
218
219
220
221
222
223
224
225
226
227
228
229
230
231

		// set it once only
		if (!totalFrameCount) {
			this.setState({
				totalFrameCount: frameCount
			});
		}

		const partDataAsBytes = new Uint8Array(partData.length / 2);

		for (let i = 0; i < partDataAsBytes.length; i++) {
			partDataAsBytes[i] = parseInt(partData.substr(i * 2, 2), 16);
		}

232
		if (
233
234
			partDataAsBytes[0] === new Uint8Array([0x00])[0] ||
			partDataAsBytes[0] === new Uint8Array([0x7b])[0]
235
236
237
238
239
		) {
			// part_data for frame 0 MUST NOT begin with byte 00 or byte 7B.
			throw new Error('Error decoding invalid part data.');
		}

YJ's avatar
YJ committed
240
241
242
243
244
245
246
247
248
		const completedFramesCount = Object.keys(multipartData).length;

		if (
			completedFramesCount > 0 &&
			totalFrameCount > 0 &&
			completedFramesCount === totalFrameCount &&
			!multipartComplete
		) {
			// all the frames are filled
249
250

			this.setState({
YJ's avatar
YJ committed
251
				multipartComplete: true
252
253
			});

YJ's avatar
YJ committed
254
255
			// concatenate all the parts into one binary blob
			let concatMultipartData = Object.values(multipartData).reduce(
256
257
258
259
260
261
262
				(acc: Uint8Array, part: Uint8Array): Uint8Array => {
					const c = new Uint8Array(acc.length + part.length);
					c.set(acc);
					c.set(part, acc.length);
					return c;
				},
				new Uint8Array(0)
263
			);
YJ's avatar
YJ committed
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278

			// unshift the frame info
			const frameInfo = u8aConcat(
				MULTIPART,
				encodeNumber(totalFrameCount),
				encodeNumber(frame)
			);
			concatMultipartData = u8aConcat(frameInfo, concatMultipartData);

			// handle the binary blob as a single UOS payload
			this.setParsedData(concatMultipartData, accountsStore, true);
		} else if (completedFramesCount < totalFrameCount) {
			// we haven't filled all the frames yet
			const nextDataState = multipartData;
			nextDataState[frame] = partDataAsBytes;
279

280
281
282
			const missedFramesRange: number = latestFrame
				? mod(frame - latestFrame, totalFrameCount) - 1
				: 0;
283
284
285
286
287
288
289
290

			// we skipped at least one frame that we haven't already scanned before
			if (
				latestFrame &&
				missedFramesRange >= 1 &&
				!missedFrames.includes(frame)
			) {
				// enumerate all the frames between (current)frame and latestFrame
Hanwen Cheng's avatar
Hanwen Cheng committed
291
292
293
				const updatedMissedFrames = Array.from(
					new Array(missedFramesRange),
					(_, i) => mod(i + latestFrame, totalFrameCount)
294
295
296
297
				);

				const dedupMissedFrames = new Set([
					...this.state.missedFrames,
Hanwen Cheng's avatar
Hanwen Cheng committed
298
					...updatedMissedFrames
299
300
301
302
303
304
305
306
307
308
309
310
				]);

				this.setState({
					missedFrames: Array.from(dedupMissedFrames)
				});
			}

			// if we just filled a frame that was previously missed, remove it from the missedFrames list
			if (missedFrames && missedFrames.includes(frame - 1)) {
				missedFrames.splice(missedFrames.indexOf(frame - 1), 1);
			}

YJ's avatar
YJ committed
311
			this.setState({
312
				latestFrame: frame,
YJ's avatar
YJ committed
313
314
				multipartData: nextDataState
			});
315
		}
316
317
318
319

		this.setState({
			completedFramesCount
		});
320
321
	}

322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
	async setData(accountsStore: AccountsStore): Promise<boolean | void> {
		const { unsignedData } = this.state;
		if (!isMultipartData(unsignedData) && unsignedData !== null) {
			switch (unsignedData.action) {
				case 'signTransaction':
					return await this.setTXRequest(unsignedData, accountsStore);
				case 'signData':
					return await this.setDataToSign(unsignedData, accountsStore);
				default:
					return;
			}
		} else {
			throw new Error(
				'Scanned QR should contain either transaction or a message to sign'
			);
337
338
339
		}
	}

340
341
342
343
	async setDataToSign(
		signRequest: CompletedParsedData,
		accountsStore: AccountsStore
	): Promise<boolean> {
YJ's avatar
YJ committed
344
345
		this.setBusy();

346
347
		const address = signRequest.data.account;
		const message = signRequest.data.data;
348
349
350
351
352
		const crypto = (signRequest as SubstrateCompletedParsedData).data?.crypto;
		const isHash =
			(signRequest as SubstrateCompletedParsedData)?.isHash || false;
		const isOversized =
			(signRequest as SubstrateCompletedParsedData)?.oversized || false;
353
354

		let dataToSign = '';
355
356
357
		const messageString = message?.toString();
		if (messageString === undefined)
			throw new Error('No message data to sign.');
358
359
360

		if (crypto === 'sr25519' || crypto === 'ed25519') {
			// only Substrate payload has crypto field
361
			dataToSign = message!.toString();
362
		} else {
363
			dataToSign = await ethSign(message!.toString());
364
365
		}

366
		const sender = accountsStore.getAccountByAddress(address);
Hanwen Cheng's avatar
Hanwen Cheng committed
367
		if (!sender) {
368
			throw new Error(
Hanwen Cheng's avatar
Hanwen Cheng committed
369
				`No private key found for ${address} in your signer key storage.`
370
371
372
373
374
			);
		}

		this.setState({
			dataToSign,
375
376
377
378
			isHash: isHash,
			isOversized: isOversized,
			message: message!.toString(),
			sender: sender!,
379
380
381
382
383
384
			type: 'message'
		});

		return true;
	}

385
386
387
388
	async setTXRequest(
		txRequest: CompletedParsedData,
		accountsStore: AccountsStore
	): Promise<boolean> {
389
390
		this.setBusy();

391
392
		const isOversized =
			(txRequest as SubstrateCompletedParsedData)?.oversized || false;
393

394
		const isEthereum = isEthereumCompletedParsedData(txRequest);
395
396
397

		if (
			isEthereum &&
398
399
400
401
402
			!(
				txRequest.data &&
				(txRequest as EthereumParsedData).data!.rlp &&
				txRequest.data.account
			)
403
404
405
		) {
			throw new Error('Scanned QR contains no valid transaction');
		}
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
		let tx, networkKey, recipientAddress, dataToSign;
		if (isEthereumCompletedParsedData(txRequest)) {
			tx = await transaction(txRequest.data.rlp);
			networkKey = tx.ethereumChainId;
			recipientAddress = tx.action;
			// For Eth, always sign the keccak hash.
			// For Substrate, only sign the blake2 hash if payload bytes length > 256 bytes (handled in decoder.js).
			dataToSign = await keccak(txRequest.data.rlp);
		} else {
			tx = txRequest.data.data;
			networkKey = (txRequest.data
				.data as ExtrinsicPayload)?.genesisHash.toHex();
			recipientAddress = txRequest.data.account;
			dataToSign = txRequest.data.data;
		}
421

Hanwen Cheng's avatar
Hanwen Cheng committed
422
		const sender = await accountsStore.getById({
423
424
425
426
427
428
			address: txRequest.data.account,
			networkKey
		});

		const networkTitle = NETWORK_LIST[networkKey].title;

Hanwen Cheng's avatar
Hanwen Cheng committed
429
		if (!sender) {
430
431
432
433
434
			throw new Error(
				`No private key found for account ${txRequest.data.account} found in your signer key storage for the ${networkTitle} chain.`
			);
		}

435
		const recipient =
436
437
438
			(await accountsStore.getById({
				address: recipientAddress,
				networkKey
439
			})) || emptyAccount(recipientAddress, networkKey);
440
441

		this.setState({
442
			dataToSign: dataToSign as string,
443
			isOversized,
444
			recipient: recipient as FoundAccount,
445
446
447
448
449
450
451
452
453
			sender,
			tx,
			txRequest,
			type: 'transaction'
		});

		return true;
	}

454
	//seed is SURI on substrate and is seedPhrase on Ethereum
455
	async signData(seed: string): Promise<void> {
456
		const { dataToSign, isHash, sender } = this.state;
457

458
459
460
		if (!sender || !NETWORK_LIST.hasOwnProperty(sender.networkKey))
			throw new Error('Signing Error: sender could not be found.');

461
462
463
464
465
		const isEthereum =
			NETWORK_LIST[sender.networkKey].protocol === NetworkProtocols.ETHEREUM;

		let signedData;
		if (isEthereum) {
466
			signedData = await brainWalletSign(seed, dataToSign as string);
467
468
469
470
471
		} else {
			let signable;

			if (dataToSign instanceof GenericExtrinsicPayload) {
				signable = u8aToHex(dataToSign.toU8a(true), -1, false);
472
473
			} else if (isHash) {
				signable = hexStripPrefix(dataToSign);
474
475
476
477
			} else if (isU8a(dataToSign)) {
				signable = hexStripPrefix(u8aToHex(dataToSign));
			} else if (isAscii(dataToSign)) {
				signable = hexStripPrefix(asciiToHex(dataToSign));
478
479
			} else {
				throw new Error('Signing Error: cannot signing message');
480
			}
481
			let signed = await substrateSign(seed, signable);
482
			signed = '0x' + signed;
YJ's avatar
YJ committed
483
			// TODO: tweak the first byte if and when sig type is not sr25519
484
			const sig = u8aConcat(SIG_TYPE_SR25519, hexToU8a(signed));
YJ's avatar
YJ committed
485
			signedData = u8aToHex(sig, -1, false); // the false doesn't add 0x
486
487
488
489
		}
		this.setState({ signedData });
	}

490
491
492
493
	async signDataWithSeedPhrase(
		seedPhrase: string,
		protocol: NetworkProtocol
	): Promise<void> {
494
495
496
497
		if (
			protocol === NetworkProtocols.SUBSTRATE ||
			protocol === NetworkProtocols.UNKNOWN
		) {
Hanwen Cheng's avatar
Hanwen Cheng committed
498
			const suri = constructSURI({
499
				derivePath: this.state.sender?.path,
Hanwen Cheng's avatar
Hanwen Cheng committed
500
				password: '',
501
				phrase: seedPhrase
Hanwen Cheng's avatar
Hanwen Cheng committed
502
			});
503
			await this.signData(suri);
Hanwen Cheng's avatar
Hanwen Cheng committed
504
		} else {
505
			await this.signData(seedPhrase);
Hanwen Cheng's avatar
Hanwen Cheng committed
506
507
508
		}
	}

509
	async signDataLegacy(pin = '1'): Promise<void> {
Hanwen Cheng's avatar
Hanwen Cheng committed
510
		const { sender } = this.state;
511
512
		if (!sender || !sender.encryptedSeed)
			throw new Error('Signing Error: sender could not be found.');
513
514
		const seed = await decryptData(sender.encryptedSeed, pin);
		await this.signData(seed);
Hanwen Cheng's avatar
Hanwen Cheng committed
515
516
	}

517
518
519
	async cleanup(): Promise<void> {
		await this.setState({
			...DEFAULT_STATE
520
		});
521
		this.clearMultipartProgress();
YJ's avatar
YJ committed
522
523
	}

524
	clearMultipartProgress(): void {
YJ's avatar
YJ committed
525
		this.setState({
526
527
528
529
			completedFramesCount: DEFAULT_STATE.completedFramesCount,
			latestFrame: DEFAULT_STATE.latestFrame,
			missedFrames: DEFAULT_STATE.missedFrames,
			multipartComplete: DEFAULT_STATE.multipartComplete,
YJ's avatar
YJ committed
530
			multipartData: {},
531
532
			totalFrameCount: DEFAULT_STATE.totalFrameCount,
			unsignedData: DEFAULT_STATE.unsignedData
YJ's avatar
YJ committed
533
534
535
536
537
538
		});
	}

	/**
	 * @dev signing payload type can be either transaction or message
	 */
539
	getType(): 'transaction' | 'message' | null {
540
541
542
		return this.state.type;
	}

YJ's avatar
YJ committed
543
544
545
	/**
	 * @dev sets a lock on writes
	 */
546
	setBusy(): void {
547
548
549
550
551
		this.setState({
			busy: true
		});
	}

YJ's avatar
YJ committed
552
553
554
	/**
	 * @dev allow write operations
	 */
555
	setReady(): void {
556
557
558
559
560
		this.setState({
			busy: false
		});
	}

561
	isBusy(): boolean {
562
563
564
		return this.state.busy;
	}

565
	isMultipartComplete(): boolean {
YJ's avatar
YJ committed
566
		return this.state.multipartComplete;
567
568
	}

YJ's avatar
YJ committed
569
570
571
	/**
	 * @dev is the payload a hash
	 */
572
	getIsHash(): boolean {
573
574
575
		return this.state.isHash;
	}

YJ's avatar
YJ committed
576
577
578
	/**
	 * @dev is the payload size greater than 256 (in Substrate chains)
	 */
579
	getIsOversized(): boolean {
580
581
582
		return this.state.isOversized;
	}

YJ's avatar
YJ committed
583
584
585
	/**
	 * @dev returns the number of completed frames so far
	 */
586
	getCompletedFramesCount(): number {
YJ's avatar
YJ committed
587
588
589
590
591
592
		return this.state.completedFramesCount;
	}

	/**
	 * @dev returns the number of frames to fill in total
	 */
593
	getTotalFramesCount(): number {
YJ's avatar
YJ committed
594
595
596
		return this.state.totalFrameCount;
	}

597
	getSender(): FoundAccount | null {
598
599
600
		return this.state.sender;
	}

601
	getRecipient(): FoundAccount | null {
602
603
604
		return this.state.recipient;
	}

605
	getTXRequest(): TXRequest | null {
606
607
608
		return this.state.txRequest;
	}

609
	getMessage(): string | null {
610
611
612
		return this.state.message;
	}

YJ's avatar
YJ committed
613
614
615
	/**
	 * @dev unsigned data, not yet formatted as signable payload
	 */
616
	getUnsigned(): CompletedParsedData | null {
617
618
619
		return this.state.unsignedData;
	}

620
	getTx(): GenericExtrinsicPayload | Transaction | string | Uint8Array | null {
621
622
623
		return this.state.tx;
	}

YJ's avatar
YJ committed
624
625
626
	/**
	 * @dev unsigned date, formatted as signable payload
	 */
627
	getDataToSign(): string | GenericExtrinsicPayload {
628
629
630
		return this.state.dataToSign;
	}

631
	getSignedTxData(): string {
632
633
634
		return this.state.signedData;
	}

635
	setErrorMsg(scanErrorMsg: string): void {
636
637
638
		this.setState({ scanErrorMsg });
	}

639
	getErrorMsg(): string {
640
641
		return this.state.scanErrorMsg;
	}
642

643
	getMissedFrames(): number[] {
644
645
646
		return this.state.missedFrames;
	}

647
	getPrehashPayload(): GenericExtrinsicPayload | null {
648
649
650
		return this.state.prehash;
	}

651
	setPrehashPayload(prehash: GenericExtrinsicPayload): void {
652
653
654
655
		this.setState({
			prehash
		});
	}
Hanwen Cheng's avatar
Hanwen Cheng committed
656
}