Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement signRaw #196

Merged
merged 1 commit into from
Dec 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions packages/extension-ui/src/Popup/Signing/Bytes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

import styled from 'styled-components';
import React from 'react';

interface Props {
className?: string;
bytes: string;
url: string;
}

function Bytes ({ bytes, className, url }: Props): React.ReactElement<Props> {
return (
<table className={className}>
<tbody>
<tr>
<td className='label'>from</td>
<td className='data'>{url}</td>
</tr>
<tr>
<td className='label'>bytes</td>
<td className='data'>{bytes}</td>
</tr>
</tbody>
</table>
);
}

export default styled(Bytes)`
border: 0;
display: block;
font-size: 0.75rem;
margin-top: 0.75rem;

td.data {
max-width: 0;
overflow: hidden;
text-align: left;
text-overflow: ellipsis;
vertical-align: middle;
width: 100%;

pre {
font-family: inherit;
font-size: 0.75rem;
margin: 0;
}
}

td.label {
opacity: 0.5;
padding: 0 0.5rem;
text-align: right;
vertical-align: middle;
white-space: nowrap;
}
`;
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ function mortalityAsString (era: ExtrinsicEra, hexBlockNumber: string): string {
return `mortal, valid from #${formatNumber(mortal.birth(blockNumber))} to #${formatNumber(mortal.death(blockNumber))}`;
}

function Details ({ className, isDecoded, payload: { era, nonce, tip }, request: { blockNumber, genesisHash, method, specVersion: hexSpec }, url }: Props): React.ReactElement<Props> {
function Extrinsic ({ className, isDecoded, payload: { era, nonce, tip }, request: { blockNumber, genesisHash, method, specVersion: hexSpec }, url }: Props): React.ReactElement<Props> {
const chain = useRef(findChain(genesisHash)).current;
const specVersion = useRef(bnToBn(hexSpec)).current;
const [decoded, setDecoded] = useState<Decoded>({ json: null, method: null });
Expand Down Expand Up @@ -136,7 +136,7 @@ function Details ({ className, isDecoded, payload: { era, nonce, tip }, request:
);
}

export default styled(Details)`
export default styled(Extrinsic)`
border: 0;
display: block;
font-size: 0.75rem;
Expand Down
90 changes: 55 additions & 35 deletions packages/extension-ui/src/Popup/Signing/Request.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,23 @@
// of the Apache-2.0 license. See the LICENSE file for details.

import { ExtrinsicPayload } from '@polkadot/types/interfaces';
import { AccountJson, RequestExtrinsicSign } from '@polkadot/extension/background/types';
import { AccountJson, RequestSign } from '@polkadot/extension/background/types';
import { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types';

import React, { useContext, useState, useEffect } from 'react';
import { TypeRegistry, createType } from '@polkadot/types';

import { ActionBar, ActionContext, Address, Link } from '../../components';
import { approveSignPassword, approveSignSignature, cancelSignRequest } from '../../messaging';
import Details from './Details';
import Bytes from './Bytes';
import Extrinsic from './Extrinsic';
import Qr from './Qr';
import Unlock from './Unlock';

interface Props {
account: AccountJson;
isFirst: boolean;
request: RequestExtrinsicSign;
request: RequestSign;
signId: string;
url: string;
}
Expand All @@ -27,16 +29,18 @@ const registry = new TypeRegistry();

export default function Request ({ account: { isExternal }, isFirst, request, signId, url }: Props): React.ReactElement<Props> | null {
const onAction = useContext(ActionContext);
const [payload, setPayload] = useState<ExtrinsicPayload | null>(null);
const [hexBytes, setHexBytes] = useState<string | null>(null);
const [extrinsic, setExtrinsic] = useState<ExtrinsicPayload | null>(null);

useEffect((): void => {
setPayload(createType(registry, 'ExtrinsicPayload', request, { version: request.version }));
const inner = request.inner;
if ((inner as SignerPayloadRaw).data) {
setHexBytes((inner as SignerPayloadRaw).data);
} else {
setExtrinsic(createType(registry, 'ExtrinsicPayload', inner, { version: (inner as SignerPayloadJSON).version }));
}
}, [request]);

if (!payload) {
return null;
}

const _onCancel = (): Promise<void> =>
cancelSignRequest(signId)
.then((): void => onAction())
Expand All @@ -49,30 +53,46 @@ export default function Request ({ account: { isExternal }, isFirst, request, si
approveSignSignature(signId, signature)
.then((): void => onAction())
.catch((error: Error) => console.error(error));

return (
<Address
address={request.address}
genesisHash={request.genesisHash}
>
{isExternal && isFirst
? <Qr
payload={payload}
request={request}
onSignature={_onSignature}
/>
: <Details
isDecoded={isFirst}
payload={payload}
request={request}
url={url}
/>
}
{isFirst && !isExternal && <Unlock onSign={_onSign} />}
<ActionBar>
<div />
<Link isDanger onClick={_onCancel}>Cancel</Link>
</ActionBar>
</Address>
);
if (extrinsic !== null) {
const payload = request.inner as SignerPayloadJSON;
return (
<Address
address={payload.address}
genesisHash={payload.genesisHash}
>
{isExternal && isFirst
? <Qr
payload={extrinsic}
request={payload}
onSignature={_onSignature}
/>
: <Extrinsic
isDecoded={isFirst}
payload={extrinsic}
request={payload}
url={url}
/>
}
{isFirst && !isExternal && <Unlock onSign={_onSign} />}
<ActionBar>
<div />
<Link isDanger onClick={_onCancel}>Cancel</Link>
</ActionBar>
</Address>
);
} else if (hexBytes !== null) {
const payload = request.inner as SignerPayloadRaw;
return (
<Address address={payload.address}>
<Bytes bytes={payload.data} url={url} />
{isFirst && !isExternal && <Unlock onSign={_onSign} />}
<ActionBar>
<div />
<Link isDanger onClick={_onCancel}>Cancel</Link>
</ActionBar>
</Address>
);
} else {
return null;
}
}
23 changes: 23 additions & 0 deletions packages/extension/src/background/RequestBytesSign.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright 2019 @polkadot/extension authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

import { KeyringPair } from '@polkadot/keyring/types';
import { RequestSign } from './types';
import { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types';
import { u8aToHex, hexToU8a } from '@polkadot/util';
import { TypeRegistry } from '@polkadot/types';

export default class RequestBytesSign implements RequestSign {
inner: SignerPayloadJSON | SignerPayloadRaw;

constructor (inner: SignerPayloadRaw) {
this.inner = inner;
}

sign (_registry: TypeRegistry, pair: KeyringPair): { signature: string } {
const inner = this.inner as SignerPayloadRaw;
const signedBytes = pair.sign(hexToU8a(inner.data));
Copy link
Contributor Author

@c410-f3r c410-f3r Nov 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the SignerPayloadRawBase contract,

export interface SignerPayloadRawBase {
    /**
     * @description The hex-encoded data for this request
     */
    data: string;
    /**
     * @description The type of the contained data
     */
    type?: 'bytes' | 'payload';
}

data will always be hex, so it is up to the caller to pass valid data.

Also, type?: 'bytes' | 'payload'; doesn't make sense. One can simply const bytes = extrinsic.toU8a(true) and signRaw(bytes). Can we remove it?

Copy link
Member

@jacogr jacogr Nov 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The payload was added specifically for other extensions, such as the centrality one - where they don't care about de-serliazing the payload, but rather just want to sign it as a raw blob. From an implementation perspective, both can just be treated as bytes here, it does not need to be treated separately for the case where signRaw is called.

The reason behind it - in case of transactions, the API will first try to use the normal payload sign, if that is not available will fallback to signRaw. Since the normal sign path is catered for, we should not receive a payload in normal circumstances.

Either way, it it ever does receive a type: payload it actually cannot quite decode it, since it is in a signable form, not a decodable form, so it should be treated it as a "piece of data". (There is a small trick here which came in with v4 where the signature needs to have type information attached in the case of a payload, but we don't need to cater for it here in signRaw)

TL;DR Ignore the type specifier on signRaw, we can just treat all data as bytes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very interesting, thank you for writing this in an explanatory way

return { signature: u8aToHex(signedBytes) };
}
}
22 changes: 22 additions & 0 deletions packages/extension/src/background/RequestExtrinsicSign.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2019 @polkadot/extension authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

import { createType, TypeRegistry } from '@polkadot/types';
import { KeyringPair } from '@polkadot/keyring/types';
import { RequestSign } from './types';
import { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types';

export default class RequestExtrinsicSign implements RequestSign {
inner: SignerPayloadJSON | SignerPayloadRaw;

constructor (inner: SignerPayloadJSON) {
this.inner = inner;
}

sign (registry: TypeRegistry, pair: KeyringPair): { signature: string } {
const inner = this.inner as SignerPayloadJSON;
const extrinsic = createType(registry, 'ExtrinsicPayload', this.inner, { version: inner.version });
return extrinsic.sign(pair);
}
}
9 changes: 3 additions & 6 deletions packages/extension/src/background/handlers/Extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { AccountJson, AuthorizeRequest, MessageTypes, RequestAccountCreateExtern
import extension from 'extensionizer';
import keyring from '@polkadot/ui-keyring';
import accountsObservable from '@polkadot/ui-keyring/observable/accounts';
import { TypeRegistry, createType } from '@polkadot/types';
import { TypeRegistry } from '@polkadot/types';
import { assert, isHex } from '@polkadot/util';
import { keyExtractSuri, mnemonicGenerate, mnemonicValidate } from '@polkadot/util-crypto';

Expand Down Expand Up @@ -153,7 +153,7 @@ export default class Extension {
assert(queued, 'Unable to find request');

const { request, resolve, reject } = queued;
const pair = keyring.getPair(request.address);
const pair = keyring.getPair(request.inner.address);

if (!pair) {
reject(new Error('Unable to find pair'));
Expand All @@ -162,10 +162,7 @@ export default class Extension {
}

pair.decodePkcs8(password);

const payload = createType(registry, 'ExtrinsicPayload', request, { version: request.version });
const result = payload.sign(pair);

const result = request.sign(registry, pair);
pair.lock();

resolve({
Expand Down
12 changes: 6 additions & 6 deletions packages/extension/src/background/handlers/State.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

import { AccountJson, AuthorizeRequest, RequestAuthorizeTab, RequestExtrinsicSign, ResponseExtrinsicSign, SigningRequest } from '../types';
import { AccountJson, AuthorizeRequest, RequestAuthorizeTab, RequestSign, ResponseSigning, SigningRequest } from '../types';

import extension from 'extensionizer';
import { BehaviorSubject } from 'rxjs';
Expand All @@ -28,8 +28,8 @@ type AuthUrls = Record<string, {
interface SignRequest {
account: AccountJson;
id: string;
request: RequestExtrinsicSign;
resolve: (result: ResponseExtrinsicSign) => void;
request: RequestSign;
resolve: (result: ResponseSigning) => void;
reject: (error: Error) => void;
url: string;
}
Expand Down Expand Up @@ -125,8 +125,8 @@ export default class State {
};
}

private signComplete = (id: string, fn: Function): (result: ResponseExtrinsicSign | Error) => void => {
return (result: ResponseExtrinsicSign | Error): void => {
private signComplete = (id: string, fn: Function): (result: ResponseSigning | Error) => void => {
return (result: ResponseSigning | Error): void => {
delete this._signRequests[id];
this.updateIconSign(true);

Expand Down Expand Up @@ -211,7 +211,7 @@ export default class State {
return this._signRequests[id];
}

public signQueue (url: string, request: RequestExtrinsicSign, account: AccountJson): Promise<ResponseExtrinsicSign> {
public sign (url: string, request: RequestSign, account: AccountJson): Promise<ResponseSigning> {
const id = getId();

return new Promise((resolve, reject): void => {
Expand Down
28 changes: 22 additions & 6 deletions packages/extension/src/background/handlers/Tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@
// of the Apache-2.0 license. See the LICENSE file for details.

import { InjectedAccount } from '@polkadot/extension-inject/types';
import { KeyringPair } from '@polkadot/keyring/types';
import { RequestAuthorizeTab, ResponseSigning, RequestTypes, ResponseTypes, MessageTypes } from '../types';
import { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types';
import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
import { RequestAuthorizeTab, RequestExtrinsicSign, ResponseExtrinsicSign, RequestTypes, ResponseTypes, MessageTypes } from '../types';

import keyring from '@polkadot/ui-keyring';
import accountsObservable from '@polkadot/ui-keyring/observable/accounts';
import { assert } from '@polkadot/util';
import RequestBytesSign from '../RequestBytesSign';
import RequestExtrinsicSign from '../RequestExtrinsicSign';

import State from './State';
import { createSubscription, unsubscribe } from './subscriptions';
Expand Down Expand Up @@ -50,13 +54,22 @@ export default class Tabs {
return true;
}

private extrinsicSign (url: string, request: RequestExtrinsicSign): Promise<ResponseExtrinsicSign> {
const { address } = request;
private getSigningPair (address: string): KeyringPair {
const pair = keyring.getPair(address);

assert(pair, 'Unable to find keypair');
return pair;
}

return this.state.signQueue(url, request, { address, ...pair.meta });
private bytesSign (url: string, request: SignerPayloadRaw): Promise<ResponseSigning> {
const address = request.address;
const pair = this.getSigningPair(address);
return this.state.sign(url, new RequestBytesSign(request), { address, ...pair.meta });
}

private extrinsicSign (url: string, request: SignerPayloadJSON): Promise<ResponseSigning> {
const address = request.address;
const pair = this.getSigningPair(address);
return this.state.sign(url, new RequestExtrinsicSign(request), { address, ...pair.meta });
}

public async handle<TMessageType extends MessageTypes> (id: string, type: TMessageType, request: RequestTypes[TMessageType], url: string, port: chrome.runtime.Port): Promise<ResponseTypes[keyof ResponseTypes]> {
Expand All @@ -74,8 +87,11 @@ export default class Tabs {
case 'pub(accounts.subscribe)':
return this.accountsSubscribe(url, id, port);

case 'pub(bytes.sign)':
return this.bytesSign(url, request as SignerPayloadRaw);

case 'pub(extrinsic.sign)':
return this.extrinsicSign(url, request as RequestExtrinsicSign);
return this.extrinsicSign(url, request as SignerPayloadJSON);

default:
throw new Error(`Unable to handle message of type ${type}`);
Expand Down
Loading