diff --git a/package-lock.json b/package-lock.json index 360146e2..9f0dfaaf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@blueprintjs/icons": "^4.16.0", "@blueprintjs/popover2": "^1.14.11", "@buttercup/channel-queue": "^1.4.0", - "@buttercup/locust": "^2.2.0", + "@buttercup/locust": "^2.2.1", "@buttercup/ui": "^6.2.2", "@types/chrome": "^0.0.251", "@types/ms": "^0.7.34", @@ -2059,9 +2059,9 @@ } }, "node_modules/@buttercup/locust": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@buttercup/locust/-/locust-2.2.0.tgz", - "integrity": "sha512-weT3huHJNwK+sTr/0TRUrRgpw1TE/sZ6DM8hoieURT818/HVITMpuMe5qwWtYrJIOISyY25tAKPKkp1KBImgyA==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@buttercup/locust/-/locust-2.2.1.tgz", + "integrity": "sha512-tBOCS8bfuG2fGByTPR5V0EhQGieq4ewRHnfKlwzFsF3ZUKzc6/OvzKeH8JlmCnj90Y8nj4G3DDipMnbdv93a9g==", "dev": true, "dependencies": { "eventemitter3": "^5.0.1", @@ -16234,9 +16234,9 @@ } }, "@buttercup/locust": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@buttercup/locust/-/locust-2.2.0.tgz", - "integrity": "sha512-weT3huHJNwK+sTr/0TRUrRgpw1TE/sZ6DM8hoieURT818/HVITMpuMe5qwWtYrJIOISyY25tAKPKkp1KBImgyA==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@buttercup/locust/-/locust-2.2.1.tgz", + "integrity": "sha512-tBOCS8bfuG2fGByTPR5V0EhQGieq4ewRHnfKlwzFsF3ZUKzc6/OvzKeH8JlmCnj90Y8nj4G3DDipMnbdv93a9g==", "dev": true, "requires": { "eventemitter3": "^5.0.1", diff --git a/package.json b/package.json index 9e3217bb..e4bb0578 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "@blueprintjs/icons": "^4.16.0", "@blueprintjs/popover2": "^1.14.11", "@buttercup/channel-queue": "^1.4.0", - "@buttercup/locust": "^2.2.0", + "@buttercup/locust": "^2.2.1", "@buttercup/ui": "^6.2.2", "@types/chrome": "^0.0.251", "@types/ms": "^0.7.34", diff --git a/source/background/services/cryptoKeys.ts b/source/background/services/cryptoKeys.ts index 8b6d2fb5..ec180cbc 100644 --- a/source/background/services/cryptoKeys.ts +++ b/source/background/services/cryptoKeys.ts @@ -52,8 +52,12 @@ export async function deriveSecretKey(privateKey: CryptoKey, publicKey: CryptoKe } async function exportECDHKey(key: CryptoKey): Promise { - const exported = await window.crypto.subtle.exportKey("jwk", key); - return JSON.stringify(exported); + try { + const exported = await window.crypto.subtle.exportKey("jwk", key); + return JSON.stringify(exported); + } catch (err) { + throw new Layerr(err, "Failed exporting ECDH key"); + } } export async function generateKeys(): Promise { @@ -73,7 +77,12 @@ export async function generateKeys(): Promise { } export async function importECDHKey(key: string): Promise { - const jwk = JSON.parse(key) as JsonWebKey; + let jwk: JsonWebKey; + try { + jwk = JSON.parse(key) as JsonWebKey; + } catch (err) { + throw new Layerr(err, "Failed importing ECDH key"); + } const usages: Array = jwk.key_ops && jwk.key_ops.includes("deriveKey") ? ["deriveKey"] : []; return window.crypto.subtle.importKey( "jwk", diff --git a/source/popup/components/entries/EntryInfoDialog.tsx b/source/popup/components/entries/EntryInfoDialog.tsx new file mode 100644 index 00000000..95b527e0 --- /dev/null +++ b/source/popup/components/entries/EntryInfoDialog.tsx @@ -0,0 +1,126 @@ +import React, { useCallback, useMemo } from "react"; +import { Button, Classes, Dialog, DialogBody, InputGroup, Intent } from "@blueprintjs/core"; +import { SearchResult } from "buttercup"; +import cn from "classnames"; +import styled from "styled-components"; +import { t } from "../../../shared/i18n/trans.js"; +import { copyTextToClipboard } from "../../services/clipboard.js"; +import { getToaster } from "../../../shared/services/notifications.js"; +import { localisedErrorMessage } from "../../../shared/library/error.js"; + +interface EntryInfoDialogProps { + entry: SearchResult | null; + onClose: () => void; +} + +interface EntryProperty { + key: string; + sensitive: boolean; + title: string; + value: string; +} + +const InfoDialog = styled(Dialog)` + max-width: 90%; +`; +const InfoDialogBody = styled(DialogBody)` + display: flex; + flex-direction: row; + justify-content: stretch; + align-items: flex-start; + overflow-x: hidden; +`; +const InfoTable = styled.table` + table-layout: fixed; + width: 100%; +`; + +export function EntryInfoDialog(props: EntryInfoDialogProps) { + const { entry, onClose } = props; + const properties = useMemo(() => entry ? orderProperties(entry.properties) : [], [entry]); + const handleCopyClick = useCallback(async (property: string, value: string) => { + try { + await copyTextToClipboard(value); + getToaster().show({ + intent: Intent.SUCCESS, + message: t("popup.entries.info.copy-success", { property }), + timeout: 4000 + }); + } catch (err) { + getToaster().show({ + intent: Intent.DANGER, + message: t("popup.entries.info.copy-error", { message: localisedErrorMessage(err) }), + timeout: 10000 + }); + } + }, []); + return ( + + + + + {properties.map(property => ( + + + {property.title}
+ handleCopyClick(property.title, property.value)} + title={t("popup.entries.info.copy-tooltip")} + /> + } + /> + + + ))} + +
+
+
+ ); +} + +function orderProperties(properties: Record): Array { + const working = { ...properties }; + delete working["title"]; + const output: Array = []; + if (working["username"]) { + output.push({ + key: "username", + sensitive: false, + title: "Username", + value: properties["username"] + }); + delete working["username"]; + } + if (working["password"]) { + output.push({ + key: "password", + sensitive: true, + title: "Password", + value: properties["password"] + }); + delete working["password"]; + } + for (const prop in working) { + output.push({ + key: prop, + sensitive: false, + title: prop, + value: working[prop] + }); + } + return output; +} diff --git a/source/popup/components/entries/EntryItem.tsx b/source/popup/components/entries/EntryItem.tsx index cd5d4c59..3053c228 100644 --- a/source/popup/components/entries/EntryItem.tsx +++ b/source/popup/components/entries/EntryItem.tsx @@ -14,6 +14,7 @@ interface EntryItemProps { fetchIcons: boolean; onAutoClick: () => void; onClick: () => void; + onInfoClick: () => void; } const CenteredText = styled(Text)` @@ -66,7 +67,8 @@ export function EntryItem(props: EntryItemProps) { entry, fetchIcons, onAutoClick, - onClick + onClick, + onInfoClick } = props; const { source: popupSource } = useContext(LaunchContext); const entryDomain = useMemo(() => { @@ -91,6 +93,11 @@ export function EntryItem(props: EntryItemProps) { }, [onAutoClick] ); + const handleEntryInfoClick = useCallback((evt: MouseEvent) => { + evt.preventDefault(); + evt.stopPropagation(); + onInfoClick(); + }, [onInfoClick]); return ( @@ -119,6 +126,15 @@ export function EntryItem(props: EntryItemProps) { onClick={handleEntryLoginClick} /> + +