diff --git a/.github/workflows/client-PR.yaml b/.github/workflows/client-PR.yaml index a8256a0..95dccbb 100644 --- a/.github/workflows/client-PR.yaml +++ b/.github/workflows/client-PR.yaml @@ -5,9 +5,13 @@ on: paths: - 'holo-key-manager-js-client/package.json' - '.github/workflows/client-PR.yaml' + merge_group: + branches: [main] + types: [checks_requested] jobs: build: + name: 'PR Client Build and Test' runs-on: ubuntu-latest environment: diff --git a/.github/workflows/extension-PR.yaml b/.github/workflows/extension-PR.yaml index 0702b2c..17cea47 100644 --- a/.github/workflows/extension-PR.yaml +++ b/.github/workflows/extension-PR.yaml @@ -4,9 +4,13 @@ on: branches: [main] paths: - 'holo-key-manager-extension/**' + merge_group: + branches: [main] + types: [checks_requested] jobs: build: + name: 'PR Extension Build and Test' runs-on: ubuntu-latest environment: diff --git a/README.md b/README.md index 662b3f5..0465fa6 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,16 @@ Holo Key Manager is a browser extension for generating and managing Holochain ap ## Usage +### User Installation + +To install the Holo Key Manager Extension, follow the instructions for your browser: + +- Chrome: [Install from Chrome Web Store](https://chrome.google.com/webstore/detail/holo-key-manager/eggfhkdnfdhdpmkfpihjjbnncgmhihce) +- Edge: [Install from Microsoft Edge Add-ons](https://microsoftedge.microsoft.com/addons/detail/jfecdgefjljjfcflgbhgfkbeofjenceh) +- Mozilla: [Install from Firefox Browser Add-ons](https://addons.mozilla.org/en-US/firefox/addon/holo-key-manager/) + +### Interacting with the Extension + A javascript client library is available at `@holo-host/holo-key-manager-js-client`. See client documentation [here](holo-key-manager-js-client/README.md) ## Architecture @@ -66,87 +76,21 @@ The monorepo consists of two main projects: `holo-key-manager-extension` and `ho pnpm buildExtension ``` + Build artifacts are in the `./holo-key-manager-extension/build` folder + 2. **Build the client:** ```sh pnpm buildClient ``` + Build artifacts are in the `./holo-key-manager-js-client/lib` folder + 3. **Build both projects for development:** ```sh pnpm build ``` -### Loading Extension Files into Chrome Browser - -After building the extension, you need to load it into your Chrome browser to test and use it. Follow these steps: - -1. **Open Chrome and navigate to the Extensions page:** - - Open Chrome and go to `chrome://extensions/` or click on the three dots in the upper right corner, then go to `More tools` > `Extensions`. - -2. **Enable Developer Mode:** - - In the top right corner of the Extensions page, toggle the switch to enable Developer Mode. - -3. **Load the unpacked extension:** - - Click on the "Load unpacked" button that appears after enabling Developer Mode. This will open a file dialog. - -4. **Select the build folder:** - - Navigate to the `holo-key-manager-extension/build` directory in your file system and select it. This will load the extension into Chrome. - -5. **Verify the extension is loaded:** - - You should see the Holo Key Manager extension listed on the Extensions page. Ensure there are no errors and the extension is enabled. - -By following these steps, you can load and test the Holo Key Manager extension in your Chrome browser. If you encounter any issues, check the console for error messages and ensure that the build process completed successfully. - -### Working with holo-key-manager-js-client - -If you want to work on the interaction between the extension and the client, you need to run the `buildPack` script inside the `holo-key-manager-js-client` directory. This will create a `.tgz` file that you can link to your new web app. - -1. **Build the client package:** - - ```sh - cd holo-key-manager-js-client - pnpm buildPack - ``` - -2. **Create a new web app:** - - You can use any framework of your choice. Here are examples for Create React App and a new Svelte project. - - **Create React App:** - - ```sh - npx create-react-app my-holo-app - cd my-holo-app - ``` - - **New Svelte Project:** - - ```sh - npx degit sveltejs/template my-holo-app - cd my-holo-app - pnpm install - ``` - -3. **Link the `.tgz` file:** - - After building the client package, link the generated `.tgz` file to your new web app. - - ```sh - pnpm add ../holo-key-manager-js-client/holo-key-manager-js-client-1.0.0.tgz - ``` - -4. **Call API functions:** - - You can now call the API functions defined in the `README` of `holo-key-manager-js-client`. Here is an example of how to use the API in your new web app. - -By following these steps, you can set up a new web app and interact with the Holo Key Manager extension using the `holo-key-manager-js-client` API. - ### Linting and Formatting To ensure code quality and consistency, use the following commands: @@ -198,10 +142,12 @@ There are three types of tests in this repository: ``` 3. **End-to-End (e2e) Tests** for the whole repository: - - To run e2e tests, you need to create a `.env` file with `CHROME_ID=eggfhkdnfdhdpmkfpihjjbnncgmhihce` that corresponds to the key property (which is actually the pub_key) in `holo-key-manager-extension/static/manifest.json`. - - The rest of the `.env` properties visible in `.env.example` are for CI/CD. - - Run the e2e tests using the command: + - To run e2e tests, you need to create a `.env` file with `CHROME_ID=eggfhkdnfdhdpmkfpihjjbnncgmhihce` + - The rest of the variables in `.env.example` are for deployment and not necessary for testing + - Run the e2e tests using the commands: ```sh + pnpm install + pnpm build pnpm e2e-tests ``` diff --git a/holo-key-manager-extension/README.md b/holo-key-manager-extension/README.md index 0ccc181..ec94343 100644 --- a/holo-key-manager-extension/README.md +++ b/holo-key-manager-extension/README.md @@ -1,3 +1,47 @@ -# Holo Key Manager +# Holo Key Manager Extension -Holo Key Manager is a secure key manager for creating and managing Holochain keys. Get easy access to all your keys. You can set up and manager one or more keys for each application. +Holo Key Manager is a secure key manager for creating and managing Holochain keys. Get easy access to all your keys. You can set up and manage one or more keys for each application. + +## Overview + +The Holo Key Manager Extension is a browser extension designed to manage Holochain keys securely. It facilitates communication between web applications and the Holo Key Manager, ensuring that keys are handled safely and efficiently. + +### Features + +- Secure key management for Holochain applications. +- Cross-browser compatibility. +- Easy integration with web applications. +- Secure communication between content scripts and web pages. + +## Building Extension + +``` +cd holo-key-manager-extension +pnpm build +``` + +## Loading Extension Files into Chrome Browser + +After building the extension, you need to load it into your Chrome browser to test and use it. Follow these steps: + +1. **Open Chrome and navigate to the Extensions page:** + + Open Chrome and go to `chrome://extensions/` or click on the three dots in the upper right corner, then go to `More tools` > `Extensions`. + +2. **Enable Developer Mode:** + + In the top right corner of the Extensions page, toggle the switch to enable Developer Mode. + +3. **Load the unpacked extension:** + + Click on the "Load unpacked" button that appears after enabling Developer Mode. This will open a file dialog. + +4. **Select the build folder:** + + Navigate to the `holo-key-manager-extension/build` directory in your file system and select it. This will load the extension into Chrome. + +5. **Verify the extension is loaded:** + + You should see the Holo Key Manager extension listed on the Extensions page. Ensure there are no errors and the extension is enabled. + +By following these steps, you can load and test the Holo Key Manager extension in your Chrome browser. If you encounter any issues, check the console for error messages and ensure that the build process completed successfully. diff --git a/holo-key-manager-extension/scripts/content.ts b/holo-key-manager-extension/scripts/content.ts index edfbf89..09358e6 100644 --- a/holo-key-manager-extension/scripts/content.ts +++ b/holo-key-manager-extension/scripts/content.ts @@ -9,6 +9,14 @@ const parseAndHandleMessage = async (event: MessageEvent) => { try { const response = await sendMessage({ ...parsedResult.data, origin: event.origin }); const parsedMessageSchema = parseMessageSchema(response); + + // Using '*' as the target origin is necessary for cross-browser extension compatibility. + // This allows communication between the content script and the webpage across different browsers. + // Security note: We have a dedicated test in @preventSignatureFromOtherOrigin.ts that verifies + // the security of this communication. Additionally, we check the origin in the background script + // to ensure that only authenticated origins can sign messages. + // TODO: Consider alternatives to postMessage(..., '*') in the future, as discussed in: + // https://github.com/w3c/webextensions/issues/78 window.postMessage(responseToMessage(parsedMessageSchema.data, parsedResult.data.id), '*'); } catch (error) { window.postMessage( diff --git a/holo-key-manager-extension/src/lib/queries/extensionQueries.ts b/holo-key-manager-extension/src/lib/queries/extensionQueries.ts index 6867e5b..543691f 100644 --- a/holo-key-manager-extension/src/lib/queries/extensionQueries.ts +++ b/holo-key-manager-extension/src/lib/queries/extensionQueries.ts @@ -1,11 +1,11 @@ import { createMutation, createQuery, QueryClient } from '@tanstack/svelte-query'; import { handleSuccess } from '$helpers'; -import { unlockKey } from '$services'; +import { generateKeys, unlockKey } from '$services'; import { DEVICE_KEY, LOCAL, PASSWORD, SETUP_KEY } from '$shared/const'; import { isSetupComplete, storageService } from '$shared/services'; import { EncryptedDeviceKeySchema, HashSaltSchema } from '$shared/types'; -import { deviceKeyContentStore, passphraseStore } from '$stores'; +import { deviceKeyContentStore, keysStore, passphraseStore } from '$stores'; export function createSetupDeviceKeyQuery() { return createQuery({ @@ -37,6 +37,17 @@ export function createStoreDeviceKey(queryClient: QueryClient) { }); } +export function createGenerateKeys() { + return createMutation({ + mutationFn: async (mutationData: { passphrase: string; password: string }) => { + // Promise that resolves automatically to prevent the window from freezing before the mutation enters the loading state + await new Promise((resolve) => setTimeout(resolve, 100)); + const keys = await generateKeys(mutationData.passphrase, mutationData.password); + keysStore.set(keys); + } + }); +} + export function createRecoverDeviceKeyMutation() { return createMutation({ mutationFn: async (mutationData: { deviceKey: string; passphrase: string }) => { diff --git a/holo-key-manager-extension/src/lib/queries/index.ts b/holo-key-manager-extension/src/lib/queries/index.ts index 163f893..8b14b19 100644 --- a/holo-key-manager-extension/src/lib/queries/index.ts +++ b/holo-key-manager-extension/src/lib/queries/index.ts @@ -6,6 +6,7 @@ import { createSignInWithKeyMutation } from './applicationQueries'; import { + createGenerateKeys, createRecoverDeviceKeyMutation, createSetupDeviceKeyQuery, createStoreDeviceKey @@ -32,6 +33,7 @@ export function appQueries() { const applicationsListQuery = createApplicationsListQuery(); const storeDeviceKey = createStoreDeviceKey(queryClient); const recoverDeviceKeyMutation = createRecoverDeviceKeyMutation(); + const generateKeysMutation = createGenerateKeys(); const applicationKeyMutation = createApplicationKeyMutation(queryClient); const passwordAndStoreDeviceKeyMutation = createPasswordAndStoreDeviceKeyMutation(queryClient); @@ -47,6 +49,7 @@ export function appQueries() { applicationKeyMutation, recoverDeviceKeyMutation, passwordAndStoreDeviceKeyMutation, - signInWithKeyMutation + signInWithKeyMutation, + generateKeysMutation }; } diff --git a/holo-key-manager-extension/src/lib/services/manage-keys.ts b/holo-key-manager-extension/src/lib/services/manage-keys.ts index 3224941..eac0227 100644 --- a/holo-key-manager-extension/src/lib/services/manage-keys.ts +++ b/holo-key-manager-extension/src/lib/services/manage-keys.ts @@ -9,8 +9,11 @@ import { import { base64ToUint8Array, uint8ArrayToBase64 } from '$shared/helpers'; import type { GeneratedKeys } from '$types'; +const createSeedCipherPwHash = (password: string) => + new SeedCipherPwHash(parseSecret(new TextEncoder().encode(password)), 'moderate'); + const lock = (root: UnlockedSeedBundle, password: string) => - root.lock([new SeedCipherPwHash(parseSecret(new TextEncoder().encode(password)), 'minimum')]); + root.lock([createSeedCipherPwHash(password)]); const deriveAndLock = ( master: UnlockedSeedBundle, @@ -18,13 +21,9 @@ const deriveAndLock = ( bundleType: string, passphrase: string ) => { - const root = master.derive(derivationPath, { - bundle_type: bundleType - }); - + const root = master.derive(derivationPath, { bundle_type: bundleType }); const encodedBytes = lock(root, passphrase); root.zero(); - return encodedBytes; }; @@ -34,11 +33,9 @@ export async function generateKeys( ): Promise { await seedBundleReady; - const master = UnlockedSeedBundle.newRandom({ - bundle_type: 'master' - }); + const master = UnlockedSeedBundle.newRandom({ bundle_type: 'master' }); - const encodedMasterBytes: Uint8Array = lock(master, passphrase); + const encodedMasterBytes = lock(master, passphrase); const encodedRevocationBytes = deriveAndLock(master, 0, 'revocationRoot', passphrase); const encodedDeviceBytes = deriveAndLock(master, 1, 'deviceRoot', passphrase); const encodedDeviceBytesWithExtensionPassword = deriveAndLock( @@ -60,12 +57,8 @@ export async function generateKeys( export const lockKey = async (key: UnlockedSeedBundle, password: string) => { await seedBundleReady; - - const pw = new TextEncoder().encode(password); - const encodedBytes = key.lock([new SeedCipherPwHash(parseSecret(pw), 'moderate')]); - + const encodedBytes = lock(key, password); key.zero(); - return uint8ArrayToBase64(encodedBytes); }; diff --git a/holo-key-manager-extension/src/lib/stores/keys-store.ts b/holo-key-manager-extension/src/lib/stores/keys-store.ts index bd0e0fb..1ad0394 100644 --- a/holo-key-manager-extension/src/lib/stores/keys-store.ts +++ b/holo-key-manager-extension/src/lib/stores/keys-store.ts @@ -1,17 +1,13 @@ import { writable } from 'svelte/store'; -import { generateKeys } from '$services'; -import type { KeysState } from '$types'; +import type { GeneratedKeys } from '$types'; const initKeysStore = () => { - const initialState: KeysState = { - keys: { - encodedMaster: null, - encodedDeviceWithExtensionPassword: null, - encodedDevice: null, - encodedRevocation: null - }, - loading: false + const initialState: GeneratedKeys = { + encodedMaster: null, + encodedDeviceWithExtensionPassword: null, + encodedDevice: null, + encodedRevocation: null }; const { subscribe, set, update } = writable(initialState); @@ -20,15 +16,7 @@ const initKeysStore = () => { isInitialState: () => subscribe((state) => JSON.stringify(state) === JSON.stringify(initialState)), subscribe, - generate: async (passphrase: string, password: string) => { - update((state) => ({ ...state, loading: true })); - try { - const keys = await generateKeys(passphrase, password); - set({ keys, loading: false }); - } catch (error) { - set(initialState); - } - }, + set, resetAll: () => update(() => initialState) }; }; diff --git a/holo-key-manager-extension/src/lib/types/keys.ts b/holo-key-manager-extension/src/lib/types/keys.ts index abcafa7..7048d63 100644 --- a/holo-key-manager-extension/src/lib/types/keys.ts +++ b/holo-key-manager-extension/src/lib/types/keys.ts @@ -9,11 +9,6 @@ export type GeneratedKeys = { encodedRevocation: Uint8Array | null; }; -export type KeysState = { - keys: GeneratedKeys; - loading: boolean; -}; - const requiredString = (fieldName: string) => z.string().min(1, `${fieldName} is required`); const nonNegativeNumber = (fieldName: string) => z.number().nonnegative(`${fieldName} must be a non-negative number`); diff --git a/holo-key-manager-extension/src/routes/change-password/+page.svelte b/holo-key-manager-extension/src/routes/change-password/+page.svelte index 5e74c2e..f8f62c2 100644 --- a/holo-key-manager-extension/src/routes/change-password/+page.svelte +++ b/holo-key-manager-extension/src/routes/change-password/+page.svelte @@ -2,7 +2,7 @@ import { onMount } from 'svelte'; import { goto } from '$app/navigation'; - import { AppContainer, Button, Input, Title } from '$components'; + import { AppContainer, Button, Input, Loading, Title } from '$components'; import { dismissWindow } from '$helpers'; import { appQueries } from '$queries'; import { passwordStore } from '$stores'; @@ -21,49 +21,56 @@ - - Change Password - {#if $changePasswordWithDeviceKeyMutation.error} - {$changePasswordWithDeviceKeyMutation.error.message} - {/if} -
- - - -
+ {#if $changePasswordWithDeviceKeyMutation.isPending} +
+ +

Changing password...

+
+ {:else} + + Change Password + {#if $changePasswordWithDeviceKeyMutation.error} + {$changePasswordWithDeviceKeyMutation.error.message} + {/if} +
+ + + +
-
-
+ )} + /> + + {/if}
diff --git a/holo-key-manager-extension/src/routes/setup-keys/download/+page.svelte b/holo-key-manager-extension/src/routes/setup-keys/download/+page.svelte index ca7aceb..1da381d 100644 --- a/holo-key-manager-extension/src/routes/setup-keys/download/+page.svelte +++ b/holo-key-manager-extension/src/routes/setup-keys/download/+page.svelte @@ -10,9 +10,9 @@ onMount(() => { if ( - $keysStore.keys.encodedMaster === null || - $keysStore.keys.encodedRevocation === null || - $keysStore.keys.encodedDevice === null + $keysStore.encodedMaster === null || + $keysStore.encodedRevocation === null || + $keysStore.encodedDevice === null ) { goto('/setup-pass/start'); } @@ -20,20 +20,19 @@ async function download(): Promise { const { saveAs } = fileSaver; - const keys = $keysStore?.keys; - if (keys?.encodedMaster && keys?.encodedDevice && keys?.encodedRevocation) { + if ($keysStore.encodedMaster && $keysStore.encodedDevice && $keysStore.encodedRevocation) { const files = [ { name: 'master.txt', - data: uint8ArrayToBase64(keys.encodedMaster) + data: uint8ArrayToBase64($keysStore.encodedMaster) }, { name: 'device.txt', - data: uint8ArrayToBase64(keys.encodedDevice) + data: uint8ArrayToBase64($keysStore.encodedDevice) }, { name: 'revocation.txt', - data: uint8ArrayToBase64(keys.encodedRevocation) + data: uint8ArrayToBase64($keysStore.encodedRevocation) } ]; const zip = new JSZip(); diff --git a/holo-key-manager-extension/src/routes/setup-keys/generate-keys/+page.svelte b/holo-key-manager-extension/src/routes/setup-keys/generate-keys/+page.svelte index 667216d..417b01a 100644 --- a/holo-key-manager-extension/src/routes/setup-keys/generate-keys/+page.svelte +++ b/holo-key-manager-extension/src/routes/setup-keys/generate-keys/+page.svelte @@ -6,7 +6,7 @@ import { appQueries } from '$queries'; import { keysStore, passphraseStore, passwordStore } from '$stores'; - const { storeDeviceKey } = appQueries(); + const { storeDeviceKey, generateKeysMutation } = appQueries(); onMount(() => { if ($passphraseStore === '') { @@ -15,25 +15,31 @@ }); async function generate() { - await keysStore.generate($passphraseStore, $passwordStore); + try { + await $generateKeysMutation.mutateAsync({ + passphrase: $passphraseStore, + password: $passwordStore + }); - if ($keysStore.keys) { passphraseStore.clean(); passwordStore.reset(); - if ($keysStore.keys.encodedDeviceWithExtensionPassword) { - $storeDeviceKey.mutate($keysStore.keys.encodedDeviceWithExtensionPassword, { - onSuccess: () => { - goto('download'); - } - }); + const encodedDeviceKey = $keysStore?.encodedDeviceWithExtensionPassword; + if (encodedDeviceKey) { + await $storeDeviceKey.mutateAsync(encodedDeviceKey); + goto('download'); } + } catch (error) { + console.error(error); } } -{#if $keysStore.loading} - +{#if $generateKeysMutation.isPending || $storeDeviceKey.isPending} +
+ +

Generating keys...

+
{:else} Download Generate seed and key files diff --git a/holo-key-manager-extension/src/routes/webapp-extension/create-key/+page.svelte b/holo-key-manager-extension/src/routes/webapp-extension/create-key/+page.svelte index b24613c..c602072 100644 --- a/holo-key-manager-extension/src/routes/webapp-extension/create-key/+page.svelte +++ b/holo-key-manager-extension/src/routes/webapp-extension/create-key/+page.svelte @@ -1,5 +1,5 @@ -
- Holo Logo -

Sign up

- -
+{#if $applicationKeyMutation.isPending} + +{:else} +
+ Holo Logo +

Sign up

+ +
-{#if $extractDetailsFromUrl.requireEmail} - -{/if} -{#if $extractDetailsFromUrl.requireRegistrationCode} - -{/if} - + {#if $extractDetailsFromUrl.requireEmail} + + {/if} + {#if $extractDetailsFromUrl.requireRegistrationCode} + + {/if} + -{#if $applicationKeyMutation.error} -
-

Error: {$applicationKeyMutation.error.message}

-
-{/if} + {#if $applicationKeyMutation.error} +
+

Error: {$applicationKeyMutation.error.message}

+
+ {/if} -