Skip to content

Commit

Permalink
Merge pull request #344 from Concordium/add-web3Id-backup
Browse files Browse the repository at this point in the history
 Add web3 id credential backup
  • Loading branch information
shjortConcordium authored Aug 18, 2023
2 parents a0db562 + e2f4bb2 commit 70fc7a3
Show file tree
Hide file tree
Showing 17 changed files with 342 additions and 12 deletions.
2 changes: 2 additions & 0 deletions packages/browser-wallet-message-hub/src/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export enum InternalMessageType {
CreateWeb3IdProof = 'I_CreateWeb3IdProof',
ConnectAccounts = 'I_ConnectAccounts',
AddWeb3IdCredential = 'I_AddWeb3IdCredential',
LoadWeb3IdBackup = 'I_LoadWeb3IdBackup',
ImportWeb3IdBackup = 'I_ImportWeb3IdBackup',
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
3 changes: 3 additions & 0 deletions packages/browser-wallet/src/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
web3IdAddCredentialFinishHandler,
createWeb3IdProofHandler,
runIfValidWeb3IdProof,
loadWeb3IdBackupHandler,
} from './web3Id';

const rpcCallNotAllowedMessage = 'RPC Call can only be performed by whitelisted sites';
Expand Down Expand Up @@ -526,6 +527,8 @@ bgMessageHandler.handleMessage(
}
);

bgMessageHandler.handleMessage(createMessageTypeFilter(InternalMessageType.LoadWeb3IdBackup), loadWeb3IdBackupHandler);

function withPromptStart<T>(): RunCondition<MessageStatusWrapper<T | undefined>> {
return async () => {
const isPromptOpen = await sessionOpenPrompt.get();
Expand Down
21 changes: 18 additions & 3 deletions packages/browser-wallet/src/background/web3Id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,16 @@ import {
getDIDNetwork,
getPublicKeyfromPublicKeyIdentifierDID,
} from '@shared/utils/verifiable-credential-helpers';
import { ExtensionMessageHandler, MessageStatusWrapper } from '@concordium/browser-wallet-message-hub';
import { getNet } from '@shared/utils/network-helpers';
import { parse } from '@shared/utils/payload-helpers';
import { BackgroundResponseStatus, ProofBackgroundResponse } from '@shared/utils/types';
import { RunCondition } from './window-management';
import {
ExtensionMessageHandler,
InternalMessageType,
MessageStatusWrapper,
} from '@concordium/browser-wallet-message-hub';
import { getNet } from '@shared/utils/network-helpers';
import { openWindow, RunCondition } from './window-management';
import bgMessageHandler from './message-handler';

const NO_CREDENTIALS_FIT = 'No temporary credentials fit the given id';
const INVALID_CREDENTIAL_PROOF = 'Invalid credential proof given';
Expand Down Expand Up @@ -188,3 +193,13 @@ export const runIfValidWeb3IdProof: RunCondition<MessageStatusWrapper<undefined>
};
}
};

async function loadWeb3IdBackup(): Promise<void> {
await openWindow();
bgMessageHandler.sendInternalMessage(InternalMessageType.ImportWeb3IdBackup);
}

export const loadWeb3IdBackupHandler: ExtensionMessageHandler = (_msg, _sender, respond) => {
loadWeb3IdBackup();
respond(undefined);
};
3 changes: 3 additions & 0 deletions packages/browser-wallet/src/popup/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ export const relativeRoutes = {
recovery: {
path: 'recovery',
},
importWeb3IdBackup: {
path: 'import-web3Id-Backup',
},
addTokens: {
path: 'add-tokens',
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import {
storedVerifiableCredentialMetadataAtom,
storedVerifiableCredentialSchemasAtom,
storedVerifiableCredentialsAtom,
} from '@popup/store/verifiable-credential';
import { useAtomValue, useAtom } from 'jotai';
import Topbar from '@popup/shared/Topbar/Topbar';
import Topbar, { ButtonTypes } from '@popup/shared/Topbar/Topbar';
import { useTranslation } from 'react-i18next';
import { VerifiableCredential, VerifiableCredentialSchema, VerifiableCredentialStatus } from '@shared/storage/types';
import {
VerifiableCredentialMetadata,
getChangesToCredentialMetadata,
getChangesToCredentialSchemas,
} from '@shared/utils/verifiable-credential-helpers';
import { popupMessageHandler } from '@popup/shared/message-handler';
import { InternalMessageType } from '@concordium/browser-wallet-message-hub';
import {
useCredentialMetadata,
useCredentialSchema,
Expand All @@ -21,6 +23,12 @@ import {
} from './VerifiableCredentialHooks';
import { VerifiableCredentialCard } from './VerifiableCredentialCard';
import VerifiableCredentialDetails from './VerifiableCredentialDetails';
import { useVerifiableCredentialExport } from '../VerifiableCredentialBackup/utils';

async function goToImportPage() {
await popupMessageHandler.sendInternalMessage(InternalMessageType.LoadWeb3IdBackup);
window.close();
}

/**
* Component to display while loading verifiable credentials from storage.
Expand All @@ -34,9 +42,22 @@ function LoadingVerifiableCredentials() {
*/
function NoVerifiableCredentials() {
const { t } = useTranslation('verifiableCredential');

const menuButton = useMemo(() => {
const importButton = {
title: t('menu.import'),
onClick: goToImportPage,
};

return {
type: ButtonTypes.More,
items: [importButton],
};
}, []);

return (
<>
<Topbar title={t('topbar.list')} />
<Topbar title={t('topbar.list')} menuButton={menuButton} />
<div className="verifiable-credential-wrapper">
<div className="flex-column align-center">
<p className="m-t-20 m-h-30">You do not have any verifiable credentials in your wallet.</p>
Expand All @@ -46,7 +67,7 @@ function NoVerifiableCredentials() {
);
}

function VerifiableCredentialCardWithStatusFromChain({
export function VerifiableCredentialCardWithStatusFromChain({
credential,
onClick,
className,
Expand Down Expand Up @@ -101,6 +122,24 @@ export default function VerifiableCredentialList() {
const [schemas, setSchemas] = useAtom(storedVerifiableCredentialSchemasAtom);
const [storedMetadata, setStoredMetadata] = useAtom(storedVerifiableCredentialMetadataAtom);

const exportCredentials = useVerifiableCredentialExport();

const menuButton = useMemo(() => {
const backupButton = {
title: t('menu.export'),
onClick: exportCredentials,
};
const importButton = {
title: t('menu.import'),
onClick: goToImportPage,
};

return {
type: ButtonTypes.More,
items: [backupButton, importButton],
};
}, [exportCredentials]);

// Hooks that update the stored credential schemas and stored credential metadata.
useFetchingEffect<VerifiableCredentialMetadata>(
verifiableCredentials,
Expand All @@ -115,9 +154,10 @@ export default function VerifiableCredentialList() {
getChangesToCredentialSchemas
);

if (verifiableCredentials.loading) {
if (verifiableCredentials.loading || !exportCredentials) {
return <LoadingVerifiableCredentials />;
}

if (verifiableCredentials.value.length === 0) {
return <NoVerifiableCredentials />;
}
Expand All @@ -134,10 +174,9 @@ export default function VerifiableCredentialList() {
/>
);
}

return (
<>
<Topbar title={t('topbar.list')} />
<Topbar title={t('topbar.list')} menuButton={menuButton} />
<div className="verifiable-credential-wrapper">
{verifiableCredentials.value.map((credential) => {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const t: typeof en = {
menu: {
revoke: 'Ophæv',
details: 'Detaljer',
import: 'Åben import vinduet',
export: 'Download export fil',
},
details: {
id: 'Legitimationholders ID',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const t = {
menu: {
revoke: 'Revoke',
details: 'Details',
import: 'Open import window',
export: 'Download export file',
},
details: {
id: 'Credential holder ID',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
$header-height: rem(56px);

.verifiable-credential-import {
display: flex;
flex-direction: column;
height: calc(100% - $header-height);
background-color: $color-bg;

&__header {
background-color: $color-bg;
}

&__empty {
text-align: center;
}

&__list {
overflow-y: auto;
margin-bottom: rem(10px);
}

&__card {
margin: rem(16px);
}

&__button {
margin: auto auto 20px;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import React, { useContext, useState } from 'react';
import {
storedVerifiableCredentialSchemasAtom,
storedVerifiableCredentialsAtom,
storedVerifiableCredentialMetadataAtom,
} from '@popup/store/verifiable-credential';
import { useAtom } from 'jotai';
import PageHeader from '@popup/shared/PageHeader';
import { EncryptedData, VerifiableCredential } from '@shared/storage/types';
import { useTranslation } from 'react-i18next';
import Button from '@popup/shared/Button';
import { fullscreenPromptContext } from '@popup/page-layouts/FullscreenPromptLayout';
import { noOp } from 'wallet-common-helpers';
import { decrypt } from '@shared/utils/crypto';
import { useHdWallet } from '@popup/shared/utils/account-helpers';
import { VerifiableCredentialCardWithStatusFromChain } from '../VerifiableCredential/VerifiableCredentialList';
import { ExportFormat, VerifiableCredentialExport } from './utils';

function DisplayResult({ imported }: { imported: VerifiableCredential[] }) {
const { t } = useTranslation('verifiableCredentialBackup');

return (
<>
{imported.length === 0 && <p className="verifiable-credential-import__empty">{t('noImported')}</p>}
{imported.length > 0 && (
<div className="verifiable-credential-import__list">
{imported.map((credential) => {
return (
<VerifiableCredentialCardWithStatusFromChain
key={credential.id}
className="verifiable-credential-import__card"
credential={credential}
/>
);
})}
</div>
)}
</>
);
}

async function parseExport(data: EncryptedData, encryptionKey: string): Promise<VerifiableCredentialExport> {
// TODO handle bigints
const backup: ExportFormat = JSON.parse(await decrypt(data, encryptionKey));
// TODO validation
return backup.value;
}

/**
* Adds items from toAdd that does not exist in stored, using the given update. Returns the items from toAdd that was actually added.
*/
function updateList<T>(stored: T[], toAdd: T[], isEqual: (a: T, b: T) => boolean, update: (updated: T[]) => void): T[] {
const filtered = toAdd.filter((item) => stored.every((existing) => !isEqual(item, existing)));
update([...stored, ...filtered]);
return filtered;
}

/**
* Adds items from toAdd that does not exist in stored, using the given update.
*/
function updateRecord<T>(
stored: Record<string, T>,
toAdd: Record<string, T>,
update: (updated: Record<string, T>) => void
) {
const updated = { ...stored };
Object.entries(toAdd).forEach(([key, value]) => {
if (!stored[key]) {
updated[key] = value;
}
});

update(updated);
}

export default function VerifiableCredentialImport() {
const [storedVerifiableCredentials, setVerifiableCredentials] = useAtom(storedVerifiableCredentialsAtom);
const [imported, setImported] = useState<VerifiableCredential[]>();
const [storedSchemas, setSchemas] = useAtom(storedVerifiableCredentialSchemasAtom);
const [storedMetadata, setMetadata] = useAtom(storedVerifiableCredentialMetadataAtom);
const { t } = useTranslation('verifiableCredentialBackup');
const { withClose } = useContext(fullscreenPromptContext);
const wallet = useHdWallet();
const [error, setError] = useState<string>();

if (storedSchemas.loading || storedMetadata.loading || storedVerifiableCredentials.loading || !wallet) {
return null;
}

const handleImport = async (event: React.ChangeEvent<HTMLInputElement>) => {
try {
const file = event.target.files?.[0];
if (file) {
const encryptedBackup: EncryptedData = JSON.parse(await file.text());
const key = wallet.getVerifiableCredentialBackupEncryptionKey().toString('hex');
const { verifiableCredentials, schemas, metadata } = await parseExport(encryptedBackup, key);
const filteredCredentials = updateList(
storedVerifiableCredentials.value,
verifiableCredentials,
(a, b) => a.id === b.id,
setVerifiableCredentials
);
updateRecord(storedSchemas.value, schemas, setSchemas);
updateRecord(storedMetadata.value, metadata, setMetadata);
setImported(filteredCredentials);
}
} catch (e) {
setError(t('error'));
}
};

// TODO drag and drop
return (
<>
<PageHeader className="verifiable-credential-import__header">{t('title')}</PageHeader>
<div className="verifiable-credential-import">
{imported && <DisplayResult imported={imported} />}
{!imported && (
<>
<input type="file" onChange={handleImport} />
{error && <p className="m-h-10 form-error-message">{error}</p>}
</>
)}
<Button className="verifiable-credential-import__button" width="wide" onClick={withClose(noOp)}>
{t('close')}
</Button>
</div>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import en from './en';

const t: typeof en = {
title: 'Importer Web3 ID Credentials',
noImported: 'Ingen Web3 ID Credentials blev importeret',
error: 'Det var ikke muligt at importere den valgte fil. Filen skal være en backup lavet med en samme seed phrase.',
close: 'Luk',
};

export default t;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
title: 'Import Web3 ID Credentials',
noImported: 'No Web3 ID Credentials were imported',
error: 'Unable to import the chosen file. The file must be a backup created with the same seed phrase.',
close: 'Close',
};
Loading

0 comments on commit 70fc7a3

Please sign in to comment.