diff --git a/packages/extension-base/src/background/handlers/Extension.ts b/packages/extension-base/src/background/handlers/Extension.ts index 32d815f6..3bc1ab8d 100644 --- a/packages/extension-base/src/background/handlers/Extension.ts +++ b/packages/extension-base/src/background/handlers/Extension.ts @@ -2,16 +2,16 @@ // SPDX-License-Identifier: Apache-2.0 import type { MetadataDef } from '@polkadot/extension-inject/types'; -import type { KeyringPair, KeyringPair$Meta } from '@polkadot/keyring/types'; +import type { KeyringPair, KeyringPair$Json, KeyringPair$Meta } from '@polkadot/keyring/types'; import type { SubjectInfo } from '@polkadot/ui-keyring/observable/types'; -import type { AccountJson, AllowedPath, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountChangePassword, RequestAccountCreateExternal, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAccountShow, RequestAccountTie, RequestAccountValidate, RequestAuthorizeApprove, RequestAuthorizeReject, RequestDeriveCreate, ResponseDeriveValidate, RequestMetadataApprove, RequestMetadataReject, RequestSigningApprovePassword, RequestSigningApproveSignature, RequestSigningCancel, RequestSigningIsLocked, RequestSeedCreate, RequestTypes, ResponseAccountExport, RequestAccountForget, ResponseSeedCreate, RequestSeedValidate, RequestDeriveValidate, RequestJsonRestore, ResponseJsonRestore, ResponseSeedValidate, ResponseSigningIsLocked, ResponseType, SigningRequest } from '../types'; +import type { AccountJson, AllowedPath, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountChangePassword, RequestAccountCreateExternal, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAccountShow, RequestAccountTie, RequestAccountValidate, RequestAuthorizeApprove, RequestAuthorizeReject, RequestDeriveCreate, ResponseDeriveValidate, RequestMetadataApprove, RequestMetadataReject, RequestSigningApprovePassword, RequestSigningApproveSignature, RequestSigningCancel, RequestSigningIsLocked, RequestSeedCreate, RequestTypes, ResponseAccountExport, RequestAccountForget, ResponseSeedCreate, RequestSeedValidate, RequestDeriveValidate, RequestJsonRestore, ResponseSeedValidate, ResponseSigningIsLocked, ResponseType, SigningRequest, ResponseJsonGetAccountInfo } from '../types'; import { ALLOWED_PATH, PASSWORD_EXPIRY_MS } from '@polkadot/extension-base/defaults'; 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 { assert, isHex, isObject } from '@polkadot/util'; +import { assert, isHex } from '@polkadot/util'; import { keyExtractSuri, mnemonicGenerate, mnemonicValidate } from '@polkadot/util-crypto'; import State from './State'; @@ -242,39 +242,27 @@ export default class Extension { return true; } - private jsonRestore ({ json, password }: RequestJsonRestore): ResponseJsonRestore { + private jsonRestore ({ file, password }: RequestJsonRestore): void { try { - const pair = keyring.restoreAccount(json, password); - - if (pair) { - return { error: null }; - } + keyring.restoreAccount(file, password); } catch (error) { - return { error: (error as Error).message }; + throw new Error((error as Error).message); } - - return { error: 'Could not restore account.' }; } - private jsonVerifyFile ({ json }: RequestJsonRestore): boolean { + private jsonGetAccountInfo (json: KeyringPair$Json): ResponseJsonGetAccountInfo { try { - const publicKey = keyring.decodeAddress(json.address, true); - const isFileValid = publicKey.length === 32 && !!json.encoded && isObject(json.meta) && ( - Array.isArray(json.encoding.content) - ? json.encoding.content[0] === 'pkcs8' - : json.encoding.content === 'pkcs8' - ); + const pair = keyring.createFromJson(json); - return isFileValid; - } catch (error) { - console.error(error); + return { + address: pair.address, + genesisHash: pair.meta.genesisHash, + name: pair.meta.name + } as ResponseJsonGetAccountInfo; + } catch (e) { + console.error(e); + throw new Error((e as Error).message); } - - return false; - } - - private jsonVerifyPassword (password: string): boolean { - return keyring.isPassValid(password); } private seedCreate ({ length = SEED_DEFAULT_LENGTH, type }: RequestSeedCreate): ResponseSeedCreate { @@ -526,11 +514,8 @@ export default class Extension { 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(json.account.info)': + return this.jsonGetAccountInfo(request as KeyringPair$Json); case 'pri(seed.create)': return this.seedCreate(request as RequestSeedCreate); diff --git a/packages/extension-base/src/background/types.ts b/packages/extension-base/src/background/types.ts index f4c27a98..e95079d1 100644 --- a/packages/extension-base/src/background/types.ts +++ b/packages/extension-base/src/background/types.ts @@ -85,9 +85,8 @@ export interface RequestSignatures { 'pri(authorize.requests)': [RequestAuthorizeSubscribe, boolean, AuthorizeRequest[]]; 'pri(derivation.create)': [RequestDeriveCreate, boolean]; 'pri(derivation.validate)': [RequestDeriveValidate, ResponseDeriveValidate]; - 'pri(json.restore)': [RequestJsonRestore, ResponseJsonRestore]; - 'pri(json.verify.file)': [RequestJsonRestore, boolean]; - 'pri(json.verify.password)': [string, boolean]; + 'pri(json.restore)': [RequestJsonRestore, void]; + 'pri(json.account.info)': [KeyringPair$Json, ResponseJsonGetAccountInfo]; 'pri(metadata.approve)': [RequestMetadataApprove, boolean]; 'pri(metadata.get)': [string | null, MetadataDef | null]; 'pri(metadata.reject)': [RequestMetadataReject, boolean]; @@ -349,7 +348,7 @@ export interface RequestSign { } export interface RequestJsonRestore { - json: KeyringPair$Json; + file: KeyringPair$Json; password: string; } @@ -358,3 +357,9 @@ export interface ResponseJsonRestore { } export type AllowedPath = typeof ALLOWED_PATH[number]; + +export interface ResponseJsonGetAccountInfo { + address: string; + name: string; + genesisHash: string; +} diff --git a/packages/extension-ui/src/Popup/RestoreJson.tsx b/packages/extension-ui/src/Popup/RestoreJson.tsx index e32e1ee9..784a4b8d 100644 --- a/packages/extension-ui/src/Popup/RestoreJson.tsx +++ b/packages/extension-ui/src/Popup/RestoreJson.tsx @@ -7,120 +7,129 @@ import React, { useCallback, useContext, useEffect, useState } from 'react'; import styled from 'styled-components'; import { u8aToString } from '@polkadot/util'; -import { AccountContext, ActionContext, InputWithLabel, InputFileWithLabel, Button, Address } from '../components'; +import { AccountContext, ActionContext, InputWithLabel, InputFileWithLabel, Button, Address, Warning } from '../components'; import useTranslation from '../hooks/useTranslation'; -import { jsonRestore, jsonVerifyPassword, jsonVerifyFile } from '../messaging'; +import { jsonRestore, jsonGetAccountInfo } from '../messaging'; import { Header } from '../partials'; - -interface FileState { - address: string | null; - isFileValid: boolean; - json: KeyringPair$Json | null; -} - -// FIXME We want to display the decodeError -interface PassState { - decodeError?: string | null; - isPassValid: boolean; - password: string; -} +import { ResponseJsonGetAccountInfo } from '@polkadot/extension-base/background/types'; const acceptedFormats = ['application/json', 'text/plain'].join(', '); -async function parseFile (file: Uint8Array): Promise { - try { - const json = JSON.parse(u8aToString(file)) as KeyringPair$Json; - const isFileValid = await jsonVerifyFile(json); - const address = json.address; - - return { address, isFileValid, json }; - } catch (error) { - console.error(error); - } - - return { address: null, isFileValid: false, json: null }; +interface Props { + className?: string; } -export default function Upload (): React.ReactElement { +function Upload ({ className } : Props): React.ReactElement { const { t } = useTranslation(); const { accounts } = useContext(AccountContext); const onAction = useContext(ActionContext); const [isBusy, setIsBusy] = useState(false); - const [{ address, isFileValid, json }, setJson] = useState({ address: null, isFileValid: false, json: null }); - const [{ isPassValid, password }, setPass] = useState({ isPassValid: false, password: '' }); + const [{ address, genesisHash, name }, setAccountInfo] = useState({ address: '', genesisHash: '', name: '' }); + const [password, setPassword] = useState(''); + const [isFileError, setFileError] = useState(false); + const [isPasswordError, setIsPasswordError] = useState(false); + // don't use the info from the file directly + // rather use what comes from the background from jsonGetAccountInfo + const [file, setFile] = useState(undefined); useEffect((): void => { !accounts.length && onAction(); }, [accounts, onAction]); const _onChangePass = useCallback( - (password: string): void => { - jsonVerifyPassword(password) - .then((isPassValid) => setPass({ isPassValid, password })) - .catch(console.error); + (pass: string): void => { + setPassword(pass); + setIsPasswordError(false); }, [] ); const _onChangeFile = useCallback( (file: Uint8Array): void => { - parseFile(file) - .then(setJson) - .catch(console.error); + let json: KeyringPair$Json | undefined; + + try { + json = JSON.parse(u8aToString(file)) as KeyringPair$Json; + setFile(json); + } catch (e) { + console.error(e); + setFileError(true); + } + + json && jsonGetAccountInfo(json) + .then((accountInfo : ResponseJsonGetAccountInfo) => setAccountInfo(accountInfo)) + .catch((e) => { + setFileError(true); + console.error(e); + }); }, [] ); const _onRestore = useCallback( (): void => { - if (!json || !password) { + if (!file || !password) { return; } setIsBusy(true); - jsonRestore(json, password) - .then(({ error }): void => { - if (error) { - setIsBusy(false); - setPass(({ password }) => ({ decodeError: error, isPassValid: false, password })); - } else { - onAction('/'); - } - }) - .catch(console.error); + jsonRestore(file, password) + .then(() => { onAction('/'); }) + .catch((e) => { + console.error(e); + setIsBusy(false); + setIsPasswordError(true); + }); }, - [json, onAction, password] + [file, onAction, password] ); return ( <> - ('Restore from JSON')} /> -
+
('backup file')} onChange={_onChangeFile} withLabel /> + {isFileError && ( + + {t('Invalid Json file')} + + )} ('Password for this file')} onChange={_onChangePass} type='password' /> + {isPasswordError && ( + + {t('Unable to decode using the supplied passphrase')} + + )}