Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe('Extension', () => {
let tabs: Tabs;
const suri = 'seed sock milk update focus rotate barely fade car face mechanic mercy';
const password = 'passw0rd';
const address = '5FbSap4BsWfjyRhCchoVdZHkDnmDm3NEgLZ25mesq4aw2WvX';

async function createExtension (): Promise<Extension> {
await cryptoWaitReady();
Expand All @@ -33,9 +34,9 @@ describe('Extension', () => {
const authUrls: AuthUrls = {};

authUrls['localhost:3000'] = {
authorizedAccounts: [address],
count: 0,
id: '11',
isAllowed: true,
origin: 'example.com',
url: 'http://localhost:3000'
};
Expand Down
57 changes: 30 additions & 27 deletions packages/extension-base/src/background/handlers/Extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { KeyringPair, KeyringPair$Json, KeyringPair$Meta } from '@polkadot/
import type { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types';
import type { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
import type { KeypairType } from '@polkadot/util-crypto/types';
import type { AccountJson, AllowedPath, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountBatchExport, RequestAccountChangePassword, RequestAccountCreateExternal, RequestAccountCreateHardware, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAccountForget, RequestAccountShow, RequestAccountTie, RequestAccountValidate, RequestAuthorizeApprove, RequestAuthorizeReject, RequestBatchRestore, RequestDeriveCreate, RequestDeriveValidate, RequestJsonRestore, RequestMetadataApprove, RequestMetadataReject, RequestSeedCreate, RequestSeedValidate, RequestSigningApprovePassword, RequestSigningApproveSignature, RequestSigningCancel, RequestSigningIsLocked, RequestTypes, ResponseAccountExport, ResponseAccountsExport, ResponseAuthorizeList, ResponseDeriveValidate, ResponseJsonGetAccountInfo, ResponseSeedCreate, ResponseSeedValidate, ResponseSigningIsLocked, ResponseType, SigningRequest } from '../types';
import type { AccountJson, AllowedPath, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountBatchExport, RequestAccountChangePassword, RequestAccountCreateExternal, RequestAccountCreateHardware, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAccountForget, RequestAccountShow, RequestAccountTie, RequestAccountValidate, RequestAuthorizeApprove, RequestBatchRestore, RequestDeriveCreate, RequestDeriveValidate, RequestJsonRestore, RequestMetadataApprove, RequestMetadataReject, RequestSeedCreate, RequestSeedValidate, RequestSigningApprovePassword, RequestSigningApproveSignature, RequestSigningCancel, RequestSigningIsLocked, RequestTypes, RequestUpdateAuthorizedAccounts, ResponseAccountExport, ResponseAccountsExport, ResponseAuthorizeList, ResponseDeriveValidate, ResponseJsonGetAccountInfo, ResponseSeedCreate, ResponseSeedValidate, ResponseSigningIsLocked, ResponseType, SigningRequest } from '../types';

import { ALLOWED_PATH, PASSWORD_EXPIRY_MS } from '@polkadot/extension-base/defaults';
import { TypeRegistry } from '@polkadot/types';
Expand All @@ -16,7 +16,7 @@ import { assert, isHex } from '@polkadot/util';
import { keyExtractSuri, mnemonicGenerate, mnemonicValidate } from '@polkadot/util-crypto';

import { withErrorLog } from './helpers';
import State from './State';
import State, { AuthorizedAccountsDiff } from './State';
import { createSubscription, unsubscribe } from './subscriptions';

type CachedUnlocks = Record<string, number>;
Expand Down Expand Up @@ -115,6 +115,18 @@ export default class Extension {
}

private accountsForget ({ address }: RequestAccountForget): boolean {
const authorizedAccountsDiff: AuthorizedAccountsDiff = [];

// cycle through authUrls and prepare the array of diff
Object.entries(this.#state.authUrls).forEach(([url, urlInfo]) => {
if (!urlInfo.authorizedAccounts.includes(address)) {
return;
}

authorizedAccountsDiff.push([url, urlInfo.authorizedAccounts.filter((previousAddress) => previousAddress !== address)]);
});

this.#state.updateAuthorizedAccounts(authorizedAccountsDiff);
keyring.forgetAccount(address);

return true;
Expand Down Expand Up @@ -166,7 +178,6 @@ export default class Extension {
}
}

// 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 =>
Expand All @@ -181,32 +192,24 @@ export default class Extension {
return true;
}

private authorizeApprove ({ id }: RequestAuthorizeApprove): boolean {
private authorizeApprove ({ authorizedAccounts, id }: RequestAuthorizeApprove): boolean {
const queued = this.#state.getAuthRequest(id);

assert(queued, 'Unable to find request');

const { resolve } = queued;

resolve(true);
resolve({ authorizedAccounts, result: true });

return true;
}

private getAuthList (): ResponseAuthorizeList {
return { list: this.#state.authUrls };
private authorizeUpdate ({ authorizedAccounts, url }: RequestUpdateAuthorizedAccounts): void {
return this.#state.updateAuthorizedAccounts([[url, authorizedAccounts]]);
}

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;
private getAuthList (): ResponseAuthorizeList {
return { list: this.#state.authUrls };
}

// FIXME This looks very much like what we have in accounts
Expand Down Expand Up @@ -500,14 +503,14 @@ export default class Extension {
return true;
}

private toggleAuthorization (url: string): ResponseAuthorizeList {
return { list: this.#state.toggleAuthorization(url) };
}

private removeAuthorization (url: string): ResponseAuthorizeList {
return { list: this.#state.removeAuthorization(url) };
}

private deleteAuthRequest (requestId: string): void {
return this.#state.deleteAuthRequest(requestId);
}

// 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>> {
Expand All @@ -518,18 +521,18 @@ export default class Extension {
case 'pri(authorize.list)':
return this.getAuthList();

case 'pri(authorize.reject)':
return this.authorizeReject(request as RequestAuthorizeReject);

case 'pri(authorize.toggle)':
return this.toggleAuthorization(request as string);

case 'pri(authorize.remove)':
return this.removeAuthorization(request as string);

case 'pri(authorize.delete.request)':
return this.deleteAuthRequest(request as string);

case 'pri(authorize.requests)':
return this.authorizeSubscribe(id, port);

case 'pri(authorize.update)':
return this.authorizeUpdate(request as RequestUpdateAuthorizedAccounts);

case 'pri(accounts.create.external)':
return this.accountsCreateExternal(request as RequestAccountCreateExternal);

Expand Down
65 changes: 39 additions & 26 deletions packages/extension-base/src/background/handlers/State.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ interface Resolver <T> {
resolve: (result: T) => void;
}

interface AuthRequest extends Resolver<boolean> {
interface AuthRequest extends Resolver<AuthResponse> {
id: string;
idStr: string;
request: RequestAuthorizeTab;
Expand All @@ -30,12 +30,14 @@ interface AuthRequest extends Resolver<boolean> {

export type AuthUrls = Record<string, AuthUrlInfo>;

export type AuthorizedAccountsDiff = [url: string, authorizedAccounts: AuthUrlInfo['authorizedAccounts']][]

export interface AuthUrlInfo {
count: number;
id: string;
isAllowed: boolean;
origin: string;
url: string;
authorizedAccounts: string[];
}

interface MetaRequest extends Resolver<boolean> {
Expand All @@ -44,6 +46,11 @@ interface MetaRequest extends Resolver<boolean> {
url: string;
}

export interface AuthResponse {
result: boolean;
authorizedAccounts: string[];
}

// List of providers passed into constructor. This is the list of providers
// exposed by the extension.
type Providers = Record<string, {
Expand Down Expand Up @@ -219,15 +226,14 @@ export default class State {
});
}

private authComplete = (id: string, resolve: (result: boolean) => void, reject: (error: Error) => void): Resolver<boolean> => {
const complete = (result: boolean | Error) => {
const isAllowed = result === true;
private authComplete = (id: string, resolve: (resValue: AuthResponse) => void, reject: (error: Error) => void): Resolver<AuthResponse> => {
const complete = (authorizedAccounts: string[] = []) => {
const { idStr, request: { origin }, url } = this.#authRequests[id];

this.#authUrls[this.stripUrl(url)] = {
authorizedAccounts,
count: 0,
id: idStr,
isAllowed,
origin,
url
};
Expand All @@ -239,16 +245,21 @@ export default class State {

return {
reject: (error: Error): void => {
complete(error);
complete();
reject(error);
},
resolve: (result: boolean): void => {
complete(result);
resolve(result);
resolve: ({ authorizedAccounts, result }: AuthResponse): void => {
complete(authorizedAccounts);
resolve({ authorizedAccounts, result });
}
};
};

public deleteAuthRequest (requestId: string) {
delete this.#authRequests[requestId];
this.updateIconAuth(true);
}

private saveCurrentAuthList () {
localStorage.setItem(AUTH_URLS_KEY, JSON.stringify(this.#authUrls));
}
Expand Down Expand Up @@ -289,7 +300,7 @@ export default class State {
};
};

private stripUrl (url: string): string {
public stripUrl (url: string): string {
assert(url && (url.startsWith('http:') || url.startsWith('https:') || url.startsWith('ipfs:') || url.startsWith('ipns:')), `Invalid url ${url}, expected to start with http: or https: or ipfs: or ipns:`);

const parts = url.split('/');
Expand All @@ -316,17 +327,6 @@ export default class State {
}
}

public toggleAuthorization (url: string): AuthUrls {
const entry = this.#authUrls[url];

assert(entry, `The source ${url} is not known`);

this.#authUrls[url].isAllowed = !entry.isAllowed;
this.saveCurrentAuthList();

return this.#authUrls;
}

public removeAuthorization (url: string): AuthUrls {
const entry = this.#authUrls[url];

Expand All @@ -353,7 +353,18 @@ export default class State {
this.updateIcon(shouldClose);
}

public async authorizeUrl (url: string, request: RequestAuthorizeTab): Promise<boolean> {
public updateAuthorizedAccounts (authorizedAccountDiff: AuthorizedAccountsDiff): void {
authorizedAccountDiff.forEach(([url, authorizedAccountDiff]) => {
// this url was never seen in the past
assert(this.#authUrls[url].authorizedAccounts, `The source ${url} has never been authorized to interact with this extension`);

this.#authUrls[url].authorizedAccounts = authorizedAccountDiff;
});

this.saveCurrentAuthList();
}

public async authorizeUrl (url: string, request: RequestAuthorizeTab): Promise<AuthResponse> {
const idStr = this.stripUrl(url);

// Do not enqueue duplicate authorization requests.
Expand All @@ -364,9 +375,12 @@ export default class State {

if (this.#authUrls[idStr]) {
// this url was seen in the past
assert(this.#authUrls[idStr].isAllowed, `The source ${url} is not allowed to interact with this extension`);
assert(this.#authUrls[idStr].authorizedAccounts, `The source ${url} is not allowed to interact with this extension`);

return false;
return ({
authorizedAccounts: [],
result: false
});
}

return new Promise((resolve, reject): void => {
Expand All @@ -389,7 +403,6 @@ export default class State {
const entry = this.#authUrls[this.stripUrl(url)];

assert(entry, `The source ${url} has not been enabled yet`);
assert(entry.isAllowed, `The source ${url} is not allowed to interact with this extension`);

return true;
}
Expand Down
42 changes: 27 additions & 15 deletions packages/extension-base/src/background/handlers/Tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { assert, isNumber } from '@polkadot/util';
import RequestBytesSign from '../RequestBytesSign';
import RequestExtrinsicSign from '../RequestExtrinsicSign';
import { withErrorLog } from './helpers';
import State from './State';
import State, { AuthResponse } from './State';
import { createSubscription, unsubscribe } from './subscriptions';

function transformAccounts (accounts: SubjectInfo, anyType = false): InjectedAccount[] {
Expand All @@ -42,21 +42,33 @@ export default class Tabs {
this.#state = state;
}

private authorize (url: string, request: RequestAuthorizeTab): Promise<boolean> {
private filterForAuthorizedAccounts (accounts: InjectedAccount[], url: string): InjectedAccount[] {
return accounts.filter(
(allAcc) => this.#state.authUrls[this.#state.stripUrl(url)]
.authorizedAccounts
.includes(allAcc.address)
);
}

private authorize (url: string, request: RequestAuthorizeTab): Promise<AuthResponse> {
return this.#state.authorizeUrl(url, request);
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
private accountsList (url: string, { anyType }: RequestAccountList): InjectedAccount[] {
return transformAccounts(accountsObservable.subject.getValue(), anyType);
private accountsListAuthorized (url: string, { anyType }: RequestAccountList): InjectedAccount[] {
const transformedAccounts = transformAccounts(accountsObservable.subject.getValue(), anyType);

return this.filterForAuthorizedAccounts(transformedAccounts, url);
}

// FIXME This looks very much like what we have in Extension
private accountsSubscribe (url: string, id: string, port: chrome.runtime.Port): boolean {
const cb = createSubscription<'pub(accounts.subscribe)'>(id, port);
const subscription = accountsObservable.subject.subscribe((accounts: SubjectInfo): void =>
cb(transformAccounts(accounts))
);
private accountsSubscribeAuthorized (url: string, id: string, port: chrome.runtime.Port): boolean {
const cb = createSubscription<'pub(accounts.subscribeAuthorized)'>(id, port);
const subscription = accountsObservable.subject.subscribe((accounts: SubjectInfo): void => {
const transformedAccounts = transformAccounts(accounts);

return cb(
this.filterForAuthorizedAccounts(transformedAccounts, url)
);
});

port.onDisconnect.addListener((): void => {
unsubscribe(id);
Expand Down Expand Up @@ -182,11 +194,11 @@ export default class Tabs {
case 'pub(authorize.tab)':
return this.authorize(url, request as RequestAuthorizeTab);

case 'pub(accounts.list)':
return this.accountsList(url, request as RequestAccountList);
case 'pub(accounts.listAuthorized)':
return this.accountsListAuthorized(url, request as RequestAccountList);

case 'pub(accounts.subscribe)':
return this.accountsSubscribe(url, id, port);
case 'pub(accounts.subscribeAuthorized)':
return this.accountsSubscribeAuthorized(url, id, port);

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