ghost-keychain/packages/extension-base/src/background/handlers/Extension.ts
Jaco Greeff db0b7e0eb3
Display child when parent has been removed (#349)
* Display child when parent has been removed

* Update packages/extension-ui/src/Popup/index.tsx

* Small styling updates
2020-05-31 10:51:29 +02:00

446 lines
13 KiB
TypeScript

// Copyright 2019-2020 @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 { MetadataDef } from '@polkadot/extension-inject/types';
import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
import { AccountJson, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountCreateExternal, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAccountValidate, RequestAuthorizeApprove, RequestAuthorizeReject, RequestDeriveCreate, ResponseDeriveValidate, RequestMetadataApprove, RequestMetadataReject, RequestSigningApprovePassword, RequestSigningApproveSignature, RequestSigningCancel, RequestSeedCreate, RequestTypes, ResponseAccountExport, RequestAccountForget, ResponseSeedCreate, RequestSeedValidate, RequestDeriveValidate, ResponseSeedValidate, ResponseType, SigningRequest, RequestJsonRestore, ResponseJsonRestore } from '../types';
import chrome from '@polkadot/extension-inject/chrome';
import keyring from '@polkadot/ui-keyring';
import accountsObservable from '@polkadot/ui-keyring/observable/accounts';
import { TypeRegistry } from '@polkadot/types';
import { KeyringPair, KeyringPair$Meta } from '@polkadot/keyring/types';
import { assert, isHex, isObject } from '@polkadot/util';
import { keyExtractSuri, mnemonicGenerate, mnemonicValidate } from '@polkadot/util-crypto';
import State from './State';
import { createSubscription, unsubscribe } from './subscriptions';
const SEED_DEFAULT_LENGTH = 12;
const SEED_LENGTHS = [12, 15, 18, 21, 24];
// a global registry to use internally
const registry = new TypeRegistry();
function transformAccounts (accounts: SubjectInfo): AccountJson[] {
return Object.values(accounts).map(({ json: { address, meta } }): AccountJson => ({
address,
...meta
}));
}
export default class Extension {
readonly #state: State;
constructor (state: State) {
this.#state = state;
}
private accountsCreateExternal ({ address, genesisHash, name }: RequestAccountCreateExternal): boolean {
keyring.addExternal(address, { genesisHash, name });
return true;
}
private accountsCreateSuri ({ genesisHash, name, password, suri, type }: RequestAccountCreateSuri): boolean {
keyring.addUri(suri, password, { genesisHash, name }, type);
return true;
}
private accountsEdit ({ address, name }: RequestAccountEdit): boolean {
const pair = keyring.getPair(address);
assert(pair, 'Unable to find pair');
keyring.saveAccountMeta(pair, { ...pair.meta, name });
return true;
}
private accountsExport ({ address, password }: RequestAccountExport): ResponseAccountExport {
return { exportedJson: JSON.stringify(keyring.backupAccount(keyring.getPair(address), password)) };
}
private accountsValidate ({ address, password }: RequestAccountValidate): boolean {
try {
keyring.backupAccount(keyring.getPair(address), password);
return true;
} catch (e) {
return false;
}
}
private accountsForget ({ address }: RequestAccountForget): boolean {
keyring.forgetAccount(address);
return true;
}
// FIXME This looks very much like what we have in Tabs
private accountsSubscribe (id: string, port: chrome.runtime.Port): boolean {
const cb = createSubscription<'pri(accounts.subscribe)'>(id, port);
const subscription = accountsObservable.subject.subscribe((accounts: SubjectInfo): void =>
cb(transformAccounts(accounts))
);
port.onDisconnect.addListener((): void => {
unsubscribe(id);
subscription.unsubscribe();
});
return true;
}
private authorizeApprove ({ id }: RequestAuthorizeApprove): boolean {
const queued = this.#state.getAuthRequest(id);
assert(queued, 'Unable to find request');
const { resolve } = queued;
resolve(true);
return true;
}
private authorizeReject ({ id }: RequestAuthorizeReject): boolean {
const queued = this.#state.getAuthRequest(id);
assert(queued, 'Unable to find request');
const { reject } = queued;
reject(new Error('Rejected'));
return true;
}
// FIXME This looks very much like what we have in accounts
private authorizeSubscribe (id: string, port: chrome.runtime.Port): boolean {
const cb = createSubscription<'pri(authorize.requests)'>(id, port);
const subscription = this.#state.authSubject.subscribe((requests: AuthorizeRequest[]): void =>
cb(requests)
);
port.onDisconnect.addListener((): void => {
unsubscribe(id);
subscription.unsubscribe();
});
return true;
}
private metadataApprove ({ id }: RequestMetadataApprove): boolean {
const queued = this.#state.getMetaRequest(id);
assert(queued, 'Unable to find request');
const { request, resolve } = queued;
this.#state.saveMetadata(request);
resolve(true);
return true;
}
private metadataList (): MetadataDef[] {
return this.#state.knownMetadata;
}
private metadataReject ({ id }: RequestMetadataReject): boolean {
const queued = this.#state.getMetaRequest(id);
assert(queued, 'Unable to find request');
const { reject } = queued;
reject(new Error('Rejected'));
return true;
}
private metadataSubscribe (id: string, port: chrome.runtime.Port): boolean {
const cb = createSubscription<'pri(metadata.requests)'>(id, port);
const subscription = this.#state.metaSubject.subscribe((requests: MetadataRequest[]): void =>
cb(requests)
);
port.onDisconnect.addListener((): void => {
unsubscribe(id);
subscription.unsubscribe();
});
return true;
}
private jsonRestore ({ json, password }: RequestJsonRestore): ResponseJsonRestore {
try {
const pair = keyring.restoreAccount(json, password);
if (pair) {
return { error: null };
}
} catch (error) {
return { error: (error as Error).message };
}
return { error: 'Could not restore account.' };
}
private jsonVerifyFile ({ json }: RequestJsonRestore): boolean {
try {
const publicKey = keyring.decodeAddress(json.address, true);
const isFileValid = publicKey.length === 32 && isHex(json.encoded) && isObject(json.meta) && (
Array.isArray(json.encoding.content)
? json.encoding.content[0] === 'pkcs8'
: json.encoding.content === 'pkcs8'
);
return isFileValid;
} catch (error) {
console.error(error);
}
return false;
}
private jsonVerifyPassword (password: string): boolean {
return keyring.isPassValid(password);
}
private seedCreate ({ length = SEED_DEFAULT_LENGTH, type }: RequestSeedCreate): ResponseSeedCreate {
const seed = mnemonicGenerate(length);
return {
address: keyring.createFromUri(seed, {}, type).address,
seed
};
}
private seedValidate ({ suri, type }: RequestSeedValidate): ResponseSeedValidate {
const { phrase } = keyExtractSuri(suri);
if (isHex(phrase)) {
assert(isHex(phrase, 256), 'Hex seed needs to be 256-bits');
} else {
// sadly isHex detects as string, so we need a cast here
assert(SEED_LENGTHS.includes((phrase as string).split(' ').length), `Mnemonic needs to contain ${SEED_LENGTHS.join(', ')} words`);
assert(mnemonicValidate(phrase), 'Not a valid mnemonic seed');
}
return {
address: keyring.createFromUri(suri, {}, type).address,
suri
};
}
private signingApprovePassword ({ id, password }: RequestSigningApprovePassword): boolean {
const queued = this.#state.getSignRequest(id);
assert(queued, 'Unable to find request');
const { reject, request, resolve } = queued;
const pair = keyring.getPair(request.payload.address);
if (!pair) {
reject(new Error('Unable to find pair'));
return false;
}
pair.decodePkcs8(password);
const result = request.sign(registry, pair);
pair.lock();
resolve({
id,
...result
});
return true;
}
private signingApproveSignature ({ id, signature }: RequestSigningApproveSignature): boolean {
const queued = this.#state.getSignRequest(id);
assert(queued, 'Unable to find request');
const { resolve } = queued;
resolve({ id, signature });
return true;
}
private signingCancel ({ id }: RequestSigningCancel): boolean {
const queued = this.#state.getSignRequest(id);
assert(queued, 'Unable to find request');
const { reject } = queued;
reject(new Error('Cancelled'));
return true;
}
// FIXME This looks very much like what we have in authorization
private signingSubscribe (id: string, port: chrome.runtime.Port): boolean {
const cb = createSubscription<'pri(signing.requests)'>(id, port);
const subscription = this.#state.signSubject.subscribe((requests: SigningRequest[]): void =>
cb(requests)
);
port.onDisconnect.addListener((): void => {
unsubscribe(id);
subscription.unsubscribe();
});
return true;
}
private windowOpen (path = '/'): boolean {
console.error('open', `${chrome.extension.getURL('index.html')}#${path}`);
chrome.tabs.create({
url: `${chrome.extension.getURL('index.html')}#${path}`
});
return true;
}
private derive (parentAddress: string, suri: string, password: string, metadata: KeyringPair$Meta): KeyringPair {
const parentPair = keyring.getPair(parentAddress);
try {
parentPair.decodePkcs8(password);
} catch (e) {
throw new Error('invalid password');
}
try {
return parentPair.derive(suri, metadata);
} catch (err) {
throw new Error(`"${suri}" is not a valid derivation path`);
}
}
private derivationValidate ({ parentAddress, parentPassword, suri }: RequestDeriveValidate): ResponseDeriveValidate {
const childPair = this.derive(parentAddress, suri, parentPassword, {});
return {
address: childPair.address,
suri
};
}
private derivationCreate ({ genesisHash, name, parentAddress, parentPassword, password, suri }: RequestDeriveCreate): boolean {
const childPair = this.derive(parentAddress, suri, parentPassword, {
genesisHash,
name,
parentAddress,
suri
});
keyring.addPair(childPair, password);
return true;
}
// Weird thought, the eslint override is not needed in Tabs
// eslint-disable-next-line @typescript-eslint/require-await
public async handle<TMessageType extends MessageTypes> (id: string, type: TMessageType, request: RequestTypes[TMessageType], port: chrome.runtime.Port): Promise<ResponseType<TMessageType>> {
switch (type) {
case 'pri(authorize.approve)':
return this.authorizeApprove(request as RequestAuthorizeApprove);
case 'pri(authorize.reject)':
return this.authorizeReject(request as RequestAuthorizeReject);
case 'pri(authorize.requests)':
return this.authorizeSubscribe(id, port);
case 'pri(accounts.create.external)':
return this.accountsCreateExternal(request as RequestAccountCreateExternal);
case 'pri(accounts.create.suri)':
return this.accountsCreateSuri(request as RequestAccountCreateSuri);
case 'pri(accounts.edit)':
return this.accountsEdit(request as RequestAccountEdit);
case 'pri(accounts.export)':
return this.accountsExport(request as RequestAccountExport);
case 'pri(accounts.forget)':
return this.accountsForget(request as RequestAccountForget);
case 'pri(accounts.validate)':
return this.accountsValidate(request as RequestAccountValidate);
case 'pri(accounts.subscribe)':
return this.accountsSubscribe(id, port);
case 'pri(metadata.approve)':
return this.metadataApprove(request as RequestMetadataApprove);
case 'pri(metadata.list)':
return this.metadataList();
case 'pri(metadata.reject)':
return this.metadataReject(request as RequestMetadataReject);
case 'pri(metadata.requests)':
return this.metadataSubscribe(id, port);
case 'pri(derivation.create)':
return this.derivationCreate(request as RequestDeriveCreate);
case 'pri(derivation.validate)':
return this.derivationValidate(request as RequestDeriveValidate);
case 'pri(json.restore)':
return this.jsonRestore(request as RequestJsonRestore);
case 'pri(json.verify.file)':
return this.jsonVerifyFile(request as RequestJsonRestore);
case 'pri(json.verify.password)':
return this.jsonVerifyPassword(request as string);
case 'pri(seed.create)':
return this.seedCreate(request as RequestSeedCreate);
case 'pri(seed.validate)':
return this.seedValidate(request as RequestSeedValidate);
case 'pri(signing.approve.password)':
return this.signingApprovePassword(request as RequestSigningApprovePassword);
case 'pri(signing.approve.signature)':
return this.signingApproveSignature(request as RequestSigningApproveSignature);
case 'pri(signing.cancel)':
return this.signingCancel(request as RequestSigningCancel);
case 'pri(signing.requests)':
return this.signingSubscribe(id, port);
case 'pri(window.open)':
return this.windowOpen();
case 'pri(window.open.json)':
return this.windowOpen('/account/restore-json');
default:
throw new Error(`Unable to handle message of type ${type}`);
}
}
}