diff --git a/.github/workflows/vetkeys-password-manager.yml b/.github/workflows/vetkeys-password-manager.yml index 9536386238..3d16895f6a 100644 --- a/.github/workflows/vetkeys-password-manager.yml +++ b/.github/workflows/vetkeys-password-manager.yml @@ -6,6 +6,7 @@ on: - master pull_request: paths: + - motoko/vetkeys/password_manager/** - rust/vetkeys/password_manager/** - .github/workflows/vetkeys-password-manager.yml @@ -14,23 +15,28 @@ concurrency: cancel-in-progress: true jobs: - rust: + motoko: runs-on: ubuntu-24.04 - container: ghcr.io/dfinity/icp-dev-env-rust:1.0.1 + container: ghcr.io/dfinity/icp-dev-env-motoko:1.0.1 env: ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - name: Deploy Password Manager Rust - working-directory: rust/vetkeys/password_manager/rust - run: icp network start -d && icp deploy - motoko: + - name: Deploy + working-directory: motoko/vetkeys/password_manager + run: | + icp network start -d + icp deploy + + rust: runs-on: ubuntu-24.04 - container: ghcr.io/dfinity/icp-dev-env-motoko:1.0.1 + container: ghcr.io/dfinity/icp-dev-env-rust:1.0.1 env: ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - name: Deploy Password Manager Motoko - working-directory: rust/vetkeys/password_manager/motoko - run: icp network start -d && icp deploy + - name: Deploy + working-directory: rust/vetkeys/password_manager + run: | + icp network start -d + icp deploy diff --git a/motoko/vetkeys/password_manager/README.md b/motoko/vetkeys/password_manager/README.md new file mode 100644 index 0000000000..01e3647235 --- /dev/null +++ b/motoko/vetkeys/password_manager/README.md @@ -0,0 +1,75 @@ +# VetKey Password Manager (Motoko) + +[View this sample's code on GitHub](https://github.com/dfinity/examples/tree/master/motoko/vetkeys/password_manager) + +Also available in: [Rust](../../../rust/vetkeys/password_manager) + +The **VetKey Password Manager** is an example application demonstrating how to use **VetKeys** and **Encrypted Maps** to build a secure, decentralized password manager on the **Internet Computer (IC)**. This application allows users to create password vaults, store encrypted passwords, and share vaults with other users via their **Internet Identity Principal**. + +## Features + +- **Secure Password Storage**: Uses VetKey to encrypt passwords before storing them in Encrypted Maps. +- **Vault-Based Organization**: Users can create multiple vaults, each containing multiple passwords. +- **Access Control**: Vaults can be shared with other users via their **Internet Identity Principal**. + +## Build and deploy from the command line + +### Prerequisites + +- Install [Node.js](https://nodejs.org/en/download/) +- Install [icp-cli](https://cli.internetcomputer.org): `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm` +- Install [ic-mops](https://mops.one): `npm install -g ic-mops` + +### (Optionally) choose a different master key + +This example uses `test_key_1` by default. To use a different [available master key](https://docs.internetcomputer.org/concepts/vetkeys/#api-overview), change the `init_args` value in `icp.yaml` before deploying. + +### Install + +```bash +git clone https://github.com/dfinity/examples +cd examples/motoko/vetkeys/password_manager +``` + +### Deploy + +```bash +icp network start -d +icp deploy +``` + +Open the frontend URL printed by `icp deploy`. + +To run the frontend in development mode with hot reloading (after `icp deploy`): + +```bash +npm run dev +``` + +When done, stop the local network to free up the port for other projects: + +```bash +icp network stop +``` + +## Example components + +### Backend (`backend/`) + +An **Encrypted Maps**-enabled Motoko canister that securely stores passwords. + +> **Note on naming.** The backend methods are snake_case (rather than the usual Motoko camelCase) because the `@icp-sdk/vetkeys` Encrypted Maps client calls the canister by these exact names — renaming them would break the frontend. The delegation methods are hand-written for now; an upstream Motoko actor mixin that generates this endpoint set automatically is in progress ([dfinity/vetkeys#405](https://github.com/dfinity/vetkeys/pull/405)). + +### Frontend (`frontend/`) + +A **Svelte** application providing a user-friendly interface for managing vaults and passwords. It talks to the backend through the `@icp-sdk/vetkeys` Encrypted Maps client. + +## Limitations + +This example app does not implement key rotation, which is strongly recommended in a production environment. Key rotation involves periodically changing encryption keys and re-encrypting data to enhance security. In a production app, key rotation would be useful to limit the impact of a potential key compromise, or to limit access when users are added to or removed from sharing. + +## Additional resources + +- **[Password Manager with Metadata](../../../rust/vetkeys/password_manager_with_metadata)** — if you need to store additional metadata alongside passwords. +- **[What are VetKeys](https://docs.internetcomputer.org/concepts/vetkeys)** — more information about VetKeys and VetKD. +- [Security best practices](https://docs.internetcomputer.org/guides/security/overview/) diff --git a/rust/vetkeys/password_manager/motoko/backend/src/Main.mo b/motoko/vetkeys/password_manager/backend/app.mo similarity index 93% rename from rust/vetkeys/password_manager/motoko/backend/src/Main.mo rename to motoko/vetkeys/password_manager/backend/app.mo index 8a2fe463f4..78405635f6 100644 --- a/rust/vetkeys/password_manager/motoko/backend/src/Main.mo +++ b/motoko/vetkeys/password_manager/backend/app.mo @@ -6,8 +6,15 @@ import Blob "mo:core/Blob"; import Result "mo:core/Result"; import Array "mo:core/Array"; -persistent actor class (keyName : Text) { - let encryptedMapsState = IcVetkeys.EncryptedMaps.newEncryptedMapsState({ curve = #bls12_381_g2; name = keyName }, "password_manager_example_dapp"); +// This canister exposes the Encrypted Maps interface. Its public methods are +// intentionally snake_case (not the usual Motoko camelCase) because the +// `@icp-sdk/vetkeys` Encrypted Maps client calls the canister by these exact +// names — renaming them to camelCase would break the frontend. The delegation +// methods below are hand-written today; an upstream Motoko actor mixin that +// generates this endpoint set automatically is in progress +// (https://github.com/dfinity/vetkeys/pull/405). +actor class (keyName : Text) { + let encryptedMapsState = IcVetkeys.EncryptedMaps.newEncryptedMapsState({ curve = #bls12_381_g2; name = keyName }, "password_manager_example_app"); transient let encryptedMaps = IcVetkeys.EncryptedMaps.EncryptedMaps(encryptedMapsState, Types.accessRightsOperations()); /// In this canister, we use the `ByteBuf` type to represent blobs. The reason is that we want to be consistent with the Rust canister implementation. diff --git a/motoko/vetkeys/password_manager/backend/backend.did b/motoko/vetkeys/password_manager/backend/backend.did new file mode 100644 index 0000000000..a5ac3d20d6 --- /dev/null +++ b/motoko/vetkeys/password_manager/backend/backend.did @@ -0,0 +1,98 @@ +type _anon_class_9_1 = + service { + get_accessible_shared_map_names: () -> + (vec record { + principal; + ByteBuf; + }) query; + get_all_accessible_encrypted_maps: () -> (vec EncryptedMapData) query; + get_all_accessible_encrypted_values: () -> + (vec record { + record { + principal; + ByteBuf; + }; + vec record { + ByteBuf; + ByteBuf; + }; + }) query; + get_encrypted_value: (map_owner: principal, map_name: ByteBuf, map_key: + ByteBuf) -> (Result_2) query; + get_encrypted_values_for_map: (map_owner: principal, map_name: ByteBuf) -> + (Result_5) query; + get_encrypted_vetkey: (map_owner: principal, map_name: ByteBuf, + transport_key: ByteBuf) -> (Result_4); + get_owned_non_empty_map_names: () -> (vec ByteBuf) query; + get_shared_user_access_for_map: (map_owner: principal, map_name: + ByteBuf) -> (Result_3) query; + get_user_rights: (map_owner: principal, map_name: ByteBuf, user: + principal) -> (Result) query; + get_vetkey_verification_key: () -> (ByteBuf); + insert_encrypted_value: (map_owner: principal, map_name: ByteBuf, map_key: + ByteBuf, value: ByteBuf) -> (Result_2); + remove_encrypted_value: (map_owner: principal, map_name: ByteBuf, map_key: + ByteBuf) -> (Result_2); + remove_map_values: (map_owner: principal, map_name: ByteBuf) -> (Result_1); + remove_user: (map_owner: principal, map_name: ByteBuf, user: principal) -> + (Result); + set_user_rights: (map_owner: principal, map_name: ByteBuf, user: + principal, access_rights: AccessRights) -> (Result); + }; +type Result_5 = + variant { + Err: text; + Ok: vec record { + ByteBuf; + ByteBuf; + }; + }; +type Result_4 = + variant { + Err: text; + Ok: ByteBuf; + }; +type Result_3 = + variant { + Err: text; + Ok: vec record { + principal; + AccessRights; + }; + }; +type Result_2 = + variant { + Err: text; + Ok: opt ByteBuf; + }; +type Result_1 = + variant { + Err: text; + Ok: vec ByteBuf; + }; +type Result = + variant { + Err: text; + Ok: opt AccessRights; + }; +type EncryptedMapData = + record { + access_control: vec record { + principal; + AccessRights; + }; + keyvals: vec record { + ByteBuf; + ByteBuf; + }; + map_name: ByteBuf; + map_owner: principal; + }; +type ByteBuf = record {inner: blob;}; +type AccessRights = + variant { + Read; + ReadWrite; + ReadWriteManage; + }; +service : (keyName: text) -> _anon_class_9_1 diff --git a/motoko/vetkeys/password_manager/frontend/.gitignore b/motoko/vetkeys/password_manager/frontend/.gitignore new file mode 100644 index 0000000000..b947077876 --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/motoko/vetkeys/password_manager/frontend/index.html b/motoko/vetkeys/password_manager/frontend/index.html new file mode 100644 index 0000000000..89d2ddc2ac --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/index.html @@ -0,0 +1,14 @@ + + + + + + + VetKeys Password Manager + + +
+ + + + diff --git a/motoko/vetkeys/password_manager/frontend/package.json b/motoko/vetkeys/password_manager/frontend/package.json new file mode 100644 index 0000000000..ba0e1f6b23 --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/package.json @@ -0,0 +1,35 @@ +{ + "name": "frontend", + "private": true, + "type": "module", + "scripts": { + "prebuild": "npm i --include=dev", + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "@rollup/plugin-typescript": "^12.1.2", + "@tsconfig/svelte": "^5.0.4", + "@typewriter/delta": "^1.2.4", + "daisyui": "^4.12.23", + "svelte": "^4.2.19", + "tslib": "^2.8.1", + "vite": "^5.4.21" + }, + "dependencies": { + "@icp-sdk/auth": "^7.1.0", + "@icp-sdk/core": "^5.4.0", + "@icp-sdk/vetkeys": "^0.5.0-beta.0", + "@popperjs/core": "^2.11.8", + "@sveltejs/vite-plugin-svelte": "^3.0.2", + "@tailwindcss/postcss": "^4.0.6", + "@tailwindcss/vite": "^4.0.0", + "autoprefixer": "^10.4.20", + "rollup-plugin-css-only": "^4.5.2", + "svelte-icons": "^2.1.0", + "svelte-spa-router": "^4.0.1", + "tailwindcss": "^3.0.17", + "typewriter-editor": "^0.9.4" + } +} diff --git a/motoko/vetkeys/password_manager/frontend/public/.ic-assets.json5 b/motoko/vetkeys/password_manager/frontend/public/.ic-assets.json5 new file mode 100644 index 0000000000..a57140a5e5 --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/public/.ic-assets.json5 @@ -0,0 +1,10 @@ +[ + { + match: "**/*", + security_policy: "hardened", + headers: { + "Content-Security-Policy": "default-src 'self';script-src 'self';connect-src 'self' http://localhost:* https://icp0.io https://*.icp0.io https://icp-api.io;img-src 'self' data:;style-src * 'unsafe-inline';object-src 'none';base-uri 'self';frame-ancestors 'none';form-action 'self';upgrade-insecure-requests;", + }, + allow_raw_access: false + }, +] diff --git a/motoko/vetkeys/password_manager/frontend/public/img/ic-badge-powered-by-crypto_label-stripe-dark-text.png b/motoko/vetkeys/password_manager/frontend/public/img/ic-badge-powered-by-crypto_label-stripe-dark-text.png new file mode 100644 index 0000000000..1a227a2b05 Binary files /dev/null and b/motoko/vetkeys/password_manager/frontend/public/img/ic-badge-powered-by-crypto_label-stripe-dark-text.png differ diff --git a/motoko/vetkeys/password_manager/frontend/public/img/ic-badge-powered-by-crypto_label-stripe-white-text.png b/motoko/vetkeys/password_manager/frontend/public/img/ic-badge-powered-by-crypto_label-stripe-white-text.png new file mode 100644 index 0000000000..e1da198555 Binary files /dev/null and b/motoko/vetkeys/password_manager/frontend/public/img/ic-badge-powered-by-crypto_label-stripe-white-text.png differ diff --git a/motoko/vetkeys/password_manager/frontend/public/img/ic-badge-powered-by-crypto_transparent-dark-text.png b/motoko/vetkeys/password_manager/frontend/public/img/ic-badge-powered-by-crypto_transparent-dark-text.png new file mode 100644 index 0000000000..90029c464b Binary files /dev/null and b/motoko/vetkeys/password_manager/frontend/public/img/ic-badge-powered-by-crypto_transparent-dark-text.png differ diff --git a/motoko/vetkeys/password_manager/frontend/public/img/ic-badge-powered-by-crypto_transparent-white-text.png b/motoko/vetkeys/password_manager/frontend/public/img/ic-badge-powered-by-crypto_transparent-white-text.png new file mode 100644 index 0000000000..f1a2d80812 Binary files /dev/null and b/motoko/vetkeys/password_manager/frontend/public/img/ic-badge-powered-by-crypto_transparent-white-text.png differ diff --git a/motoko/vetkeys/password_manager/frontend/public/vite.svg b/motoko/vetkeys/password_manager/frontend/public/vite.svg new file mode 100644 index 0000000000..e7b8dfb1b2 --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/motoko/vetkeys/password_manager/frontend/src/App.svelte b/motoko/vetkeys/password_manager/frontend/src/App.svelte new file mode 100644 index 0000000000..ed34e5e20d --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/src/App.svelte @@ -0,0 +1,13 @@ + + +{#if $auth.state === "initialized"} + +{:else} + +{/if} + diff --git a/motoko/vetkeys/password_manager/frontend/src/app.css b/motoko/vetkeys/password_manager/frontend/src/app.css new file mode 100644 index 0000000000..b5c61c9567 --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/src/app.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/motoko/vetkeys/password_manager/frontend/src/assets/svelte.svg b/motoko/vetkeys/password_manager/frontend/src/assets/svelte.svg new file mode 100644 index 0000000000..c5e08481f8 --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/src/assets/svelte.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/motoko/vetkeys/password_manager/frontend/src/components/Disclaimer.svelte b/motoko/vetkeys/password_manager/frontend/src/components/Disclaimer.svelte new file mode 100644 index 0000000000..2ded84f8fe --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/src/components/Disclaimer.svelte @@ -0,0 +1,26 @@ + + +{#if !isDismissed} +
+

+ +

+ + +
+{/if} diff --git a/motoko/vetkeys/password_manager/frontend/src/components/DisclaimerCopy.svelte b/motoko/vetkeys/password_manager/frontend/src/components/DisclaimerCopy.svelte new file mode 100644 index 0000000000..7cf5977604 --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/src/components/DisclaimerCopy.svelte @@ -0,0 +1,5 @@ + + +Disclaimer: This sample app is intended exclusively for experimental +purpose. You are advised not to use this app for storing your critical data such +as keys or passwords. diff --git a/motoko/vetkeys/password_manager/frontend/src/components/EditPassword.svelte b/motoko/vetkeys/password_manager/frontend/src/components/EditPassword.svelte new file mode 100644 index 0000000000..71d0523bea --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/src/components/EditPassword.svelte @@ -0,0 +1,309 @@ + + +{#if editedPassword.parentVaultName.length > 0} +
+ Edit password + +
+
+ {#if $vaultsStore.state === "loaded"} +
+ + + +
+ + + + Back + + + +
+ {:else if $vaultsStore.state === "loading"} + Loading password... + {/if} +
+{:else} +
+ Edit password +
+
+ {#if $vaultsStore.state === "loading"} + + Loading password... + {:else if $vaultsStore.state === "loaded"} +
Could not find password.
+ {/if} +
+{/if} diff --git a/motoko/vetkeys/password_manager/frontend/src/components/EditVault.svelte b/motoko/vetkeys/password_manager/frontend/src/components/EditVault.svelte new file mode 100644 index 0000000000..1b81df2a67 --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/src/components/EditVault.svelte @@ -0,0 +1,99 @@ + + +{#if editedVault} +
+ Edit vault + +
+
+ {#if $vaultsStore.state === "loaded"} +
+ + {:else if $vaultsStore.state === "loading"} + Loading vaults... + {/if} +
+{:else} +
+ Edit vault +
+
+ {#if $vaultsStore.state === "loading"} + + Loading vault... + {:else if $vaultsStore.state === "loaded"} +
Could not find vault.
+ {/if} +
+{/if} diff --git a/motoko/vetkeys/password_manager/frontend/src/components/Header.svelte b/motoko/vetkeys/password_manager/frontend/src/components/Header.svelte new file mode 100644 index 0000000000..9245e48b6b --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/src/components/Header.svelte @@ -0,0 +1,27 @@ + + + diff --git a/motoko/vetkeys/password_manager/frontend/src/components/Hero.svelte b/motoko/vetkeys/password_manager/frontend/src/components/Hero.svelte new file mode 100644 index 0000000000..3f7ba7b226 --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/src/components/Hero.svelte @@ -0,0 +1,60 @@ + + +
+
+
+

+ Password Manager +

+

+ Your private passwords on the Internet Computer. +

+

+ A safe place to store your personal lists, thoughts, ideas or + passphrases and much more... +

+ + {#if auth.state === "initializing-auth"} +
+ + Initializing... +
+ {:else if auth.state === "anonymous"} + + {:else if auth.state === "error"} +
An error occurred.
+ {/if} + +
+ +
+
+
+
+ + Powered by the Internet Computer +
+
diff --git a/motoko/vetkeys/password_manager/frontend/src/components/LayoutAuthenticated.svelte b/motoko/vetkeys/password_manager/frontend/src/components/LayoutAuthenticated.svelte new file mode 100644 index 0000000000..090735f212 --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/src/components/LayoutAuthenticated.svelte @@ -0,0 +1,29 @@ + + + + + diff --git a/motoko/vetkeys/password_manager/frontend/src/components/NewPassword.svelte b/motoko/vetkeys/password_manager/frontend/src/components/NewPassword.svelte new file mode 100644 index 0000000000..282411f822 --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/src/components/NewPassword.svelte @@ -0,0 +1,118 @@ + + + + +
+ New password +
+ +
+ +
+ + + +
+ + +
diff --git a/motoko/vetkeys/password_manager/frontend/src/components/Notifications.svelte b/motoko/vetkeys/password_manager/frontend/src/components/Notifications.svelte new file mode 100644 index 0000000000..af1b01eb57 --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/src/components/Notifications.svelte @@ -0,0 +1,22 @@ + + +
+ {#each $notifications as n (n.id)} +
+

{n.message}

+
+ {/each} +
diff --git a/motoko/vetkeys/password_manager/frontend/src/components/Password.svelte b/motoko/vetkeys/password_manager/frontend/src/components/Password.svelte new file mode 100644 index 0000000000..a1db1f011c --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/src/components/Password.svelte @@ -0,0 +1,106 @@ + + +
+ + Password: {password.passwordName} + + + {#if $vaultsStore.state === "loaded" && $vaultsStore.list.length > 0} + New password + {/if} + +
+ +
+ {#if $vaultsStore.state === "loading"} + + Loading password... + {:else if $vaultsStore.state === "loaded"} + {#if password.parentVaultName === ""} +
+ There is no such password in this vault. +
+ + {:else} +
+
+

+ {password.passwordName}: "{password.content}" +

+
+
+ {/if} +
+ + {/if} +
diff --git a/motoko/vetkeys/password_manager/frontend/src/components/PasswordEditor.svelte b/motoko/vetkeys/password_manager/frontend/src/components/PasswordEditor.svelte new file mode 100644 index 0000000000..a27c1ba12a --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/src/components/PasswordEditor.svelte @@ -0,0 +1,68 @@ + + + +
+ + + + +
+
+ +
+ + diff --git a/motoko/vetkeys/password_manager/frontend/src/components/Passwords.svelte b/motoko/vetkeys/password_manager/frontend/src/components/Passwords.svelte new file mode 100644 index 0000000000..70e7f30b31 --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/src/components/Passwords.svelte @@ -0,0 +1,79 @@ + + +
+ Your passwords + + {#if $vaultsStore.state === "loaded" && $vaultsStore.list.length > 0} + New Password + {/if} + +
+
+ {#if $vaultsStore.state === "loading"} + + Loading passwords... + {:else if $vaultsStore.state === "loaded"} + {#if $vaultsStore.list.length > 0} +
+ +
+ +
+ {#each filteredVaults as vault (vault.name)} + {#each Array.from(vault.passwords.map(([, password]) => password)) as password (password.passwordName)} + + {/each} + {/each} +
+ {:else} +
You don't have any notes.
+ + {/if} + {:else if $vaultsStore.state === "error"} +
Could not load passwords.
+ {/if} +
diff --git a/motoko/vetkeys/password_manager/frontend/src/components/SharingEditor.svelte b/motoko/vetkeys/password_manager/frontend/src/components/SharingEditor.svelte new file mode 100644 index 0000000000..07038a0353 --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/src/components/SharingEditor.svelte @@ -0,0 +1,218 @@ + + +

Users

+{#if canManage} +

+ Add users by their principal to allow them viewing or editing the vault. +

+{:else} +

+ This vault is shared with you. It is + owned by {editedVault.owner}. +

+

Users with whom the vault is shared:

+{/if} +
+ {#each editedVault.users as sharing (sharing[0].toText())} + + {/each} + + + +
diff --git a/motoko/vetkeys/password_manager/frontend/src/components/SidebarLayout.svelte b/motoko/vetkeys/password_manager/frontend/src/components/SidebarLayout.svelte new file mode 100644 index 0000000000..159dc6564e --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/src/components/SidebarLayout.svelte @@ -0,0 +1,81 @@ + + +
+ +
+
+ +
+ +
+
+
+
diff --git a/motoko/vetkeys/password_manager/frontend/src/components/Spinner.svelte b/motoko/vetkeys/password_manager/frontend/src/components/Spinner.svelte new file mode 100644 index 0000000000..cc26ba2516 --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/src/components/Spinner.svelte @@ -0,0 +1,5 @@ + + + diff --git a/motoko/vetkeys/password_manager/frontend/src/components/Vault.svelte b/motoko/vetkeys/password_manager/frontend/src/components/Vault.svelte new file mode 100644 index 0000000000..f6ed54213c --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/src/components/Vault.svelte @@ -0,0 +1,149 @@ + + +
+ + + + + Vault: {vault.name} + + + {#if $vaultsStore.state === "loaded" && $vaultsStore.list.length > 0} + New password + {/if} + +
+ +
+ {#if $vaultsStore.state === "loading"} + + Loading vault... + {:else if $vaultsStore.state === "loaded"} +
+

+ {vaultSummary} +

+
+
+ + +
+ +
+

Passwords

+
+ {#if vault.passwords.length === 0} +
+ You don't have any passwords in this vault. +
+ + {:else} +
+ {#each vault.passwords as password ((password[1].owner, password[1].parentVaultName, password[1].passwordName))} + +
+

+ {password[1].passwordName}: "{password[1] + .content}" +

+
+
+ {/each} +
+ {/if} +
+ + {/if} +
diff --git a/motoko/vetkeys/password_manager/frontend/src/components/Vaults.svelte b/motoko/vetkeys/password_manager/frontend/src/components/Vaults.svelte new file mode 100644 index 0000000000..fc99e9b3f7 --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/src/components/Vaults.svelte @@ -0,0 +1,90 @@ + + +
+ Your vaults + + {#if $vaultsStore.state === "loaded" && $vaultsStore.list.length > 0} + New password + {/if} + +
+
+ {#if $vaultsStore.state === "loading"} + + Loading vaults... + {:else if $vaultsStore.state === "loaded"} + {#if $vaultsStore.list.length > 0} +
+ +
+ +
+ {#each filteredVaults as vault ([vault.owner, vault.name])} + +
+

+ "{vault.name}" owned by {vault.owner.toText()} +

+
+
+ {/each} +
+ {:else} +
+ You don't have any vaults. +
+ + {/if} + {:else if $vaultsStore.state === "error"} +
Could not load vaults.
+ {/if} +
diff --git a/motoko/vetkeys/password_manager/frontend/src/lib/encrypted_maps.ts b/motoko/vetkeys/password_manager/frontend/src/lib/encrypted_maps.ts new file mode 100644 index 0000000000..9972d6965c --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/src/lib/encrypted_maps.ts @@ -0,0 +1,31 @@ +import "./init.ts"; +import { HttpAgent, type HttpAgentOptions } from "@icp-sdk/core/agent"; +import { + DefaultEncryptedMapsClient, + EncryptedMaps, +} from "@icp-sdk/vetkeys/encrypted_maps"; +import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env"; + +const canisterEnv = safeGetCanisterEnv<{ + "PUBLIC_CANISTER_ID:backend": string; +}>(); + +export async function createEncryptedMaps( + agentOptions?: HttpAgentOptions, +): Promise { + const canisterId = + canisterEnv?.["PUBLIC_CANISTER_ID:backend"]; + if (!canisterId) { + throw new Error( + "Canister ID for backend is not set", + ); + } + + const agent = await HttpAgent.create({ + ...agentOptions, + host: window.location.origin, + rootKey: canisterEnv?.IC_ROOT_KEY, + }); + + return new EncryptedMaps(new DefaultEncryptedMapsClient(agent, canisterId)); +} diff --git a/motoko/vetkeys/password_manager/frontend/src/lib/init.ts b/motoko/vetkeys/password_manager/frontend/src/lib/init.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/motoko/vetkeys/password_manager/frontend/src/lib/password.ts b/motoko/vetkeys/password_manager/frontend/src/lib/password.ts new file mode 100644 index 0000000000..1bd7c6ba1b --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/src/lib/password.ts @@ -0,0 +1,56 @@ +import type { Principal } from "@icp-sdk/core/principal"; + +export interface PasswordModel { + owner: Principal; + parentVaultName: string; + passwordName: string; + content: string; +} + +export function passwordFromContent( + owner: Principal, + parentVaultName: string, + passwordName: string, + content: string, +): PasswordModel { + return { + owner, + parentVaultName, + passwordName, + content, + }; +} + +export function summarize(note: PasswordModel, maxLength = 50) { + const div = document.createElement("div"); + div.innerHTML = note.content; + + let text = div.innerText; + const title = extractTitleFromDomEl(div); + if (title) { + text = text.replace(title, ""); + } + + return text.slice(0, maxLength) + (text.length > maxLength ? "..." : ""); +} + +function extractTitleFromDomEl(el: HTMLElement): string { + const title = el.querySelector("h1"); + if (title) { + return title.innerText; + } + + const blockElements = el.querySelectorAll("h1,h2,p,li"); + for (const el of blockElements) { + if (el.textContent && el.textContent.trim().length > 0) { + return el.textContent.trim(); + } + } + return ""; +} + +export function extractTitle(html: string) { + const div = document.createElement("div"); + div.innerHTML = html; + return extractTitleFromDomEl(div); +} diff --git a/motoko/vetkeys/password_manager/frontend/src/lib/sleep.ts b/motoko/vetkeys/password_manager/frontend/src/lib/sleep.ts new file mode 100644 index 0000000000..38caca0c10 --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/src/lib/sleep.ts @@ -0,0 +1,3 @@ +export function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/motoko/vetkeys/password_manager/frontend/src/lib/vault.ts b/motoko/vetkeys/password_manager/frontend/src/lib/vault.ts new file mode 100644 index 0000000000..c67041207e --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/src/lib/vault.ts @@ -0,0 +1,60 @@ +import type { Principal } from "@icp-sdk/core/principal"; +import type { PasswordModel } from "./password"; +import type { AccessRights } from "@icp-sdk/vetkeys/encrypted_maps"; + +export interface VaultModel { + owner: Principal; + name: string; + passwords: Array<[string, PasswordModel]>; + users: Array<[Principal, AccessRights]>; +} + +export function vaultFromContent( + owner: Principal, + name: string, + passwords: Array<[string, PasswordModel]>, + users: Array<[Principal, AccessRights]>, +): VaultModel { + return { owner, name, passwords, users }; +} + +export function summarize(vault: VaultModel, maxLength = 1500) { + const div = document.createElement("div"); + + div.innerHTML += + "Owner: " + + vault.owner.toString() + + ", " + + vault.users.length + + " users"; + div.innerHTML += ", " + vault.passwords.length + " passwords.\n"; + + let text = div.innerText; + const title = extractTitleFromDomEl(div); + if (title) { + text = text.replace(title, ""); + } + + return text.slice(0, maxLength) + (text.length > maxLength ? "..." : ""); +} + +function extractTitleFromDomEl(el: HTMLElement): string { + const title = el.querySelector("h1"); + if (title) { + return title.innerText; + } + + const blockElements = el.querySelectorAll("h1,h2,p,li"); + for (const el of blockElements) { + if (el.textContent && el.textContent?.trim().length > 0) { + return el.textContent.trim(); + } + } + return ""; +} + +export function extractTitle(html: string) { + const div = document.createElement("div"); + div.innerHTML = html; + return extractTitleFromDomEl(div); +} diff --git a/motoko/vetkeys/password_manager/frontend/src/main.ts b/motoko/vetkeys/password_manager/frontend/src/main.ts new file mode 100644 index 0000000000..ff634fcc2b --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/src/main.ts @@ -0,0 +1,8 @@ +import "./app.css"; +import App from "./App.svelte"; + +const app = new App({ + target: document.body, +}); + +export default app; diff --git a/motoko/vetkeys/password_manager/frontend/src/store/auth.ts b/motoko/vetkeys/password_manager/frontend/src/store/auth.ts new file mode 100644 index 0000000000..54a75e50ae --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/src/store/auth.ts @@ -0,0 +1,129 @@ +import "../lib/init.ts"; +import { get, writable } from "svelte/store"; +import { AuthClient, LocalStorage } from "@icp-sdk/auth/client"; +import { DelegationIdentity } from "@icp-sdk/core/identity"; +import type { Principal } from "@icp-sdk/core/principal"; +import { replace } from "svelte-spa-router"; +import { createEncryptedMaps } from "../lib/encrypted_maps.js"; +import { EncryptedMaps } from "@icp-sdk/vetkeys/encrypted_maps"; + +export type AuthState = + | { + state: "initializing-auth"; + } + | { + state: "anonymous"; + client: AuthClient; + } + | { + state: "initialized"; + encryptedMaps: EncryptedMaps; + client: AuthClient; + principal: Principal; + } + | { + state: "error"; + error: string; + }; + +export const auth = writable({ + state: "initializing-auth", +}); + +async function initAuth() { + const isLocalEnv = + window.location.hostname === "localhost" || + window.location.hostname.endsWith(".localhost"); + // Workaround for https://github.com/dfinity/icp-js-auth/issues/120 + // IdbStorage has a race condition on localhost dev servers. LocalStorage + // avoids IDB on local but uses plain string storage (less secure), so + // production deployments keep the default secure IdbStorage + ECDSA key. + const client = new AuthClient({ + identityProvider: isLocalEnv + ? "http://id.ai.localhost:8000/authorize" + : "https://id.ai/authorize", + ...(isLocalEnv + ? { storage: new LocalStorage(), keyType: "Ed25519" as const } + : {}), + }); + if (client.isAuthenticated()) { + void authenticate(client); + } else { + auth.update(() => ({ + state: "anonymous", + client, + })); + } +} + +void initAuth(); + +export function login() { + const currentAuth = get(auth); + + if (currentAuth.state === "anonymous") { + void (async () => { + try { + await currentAuth.client.signIn({ + maxTimeToLive: BigInt(1800) * BigInt(1_000_000_000), + }); + void authenticate(currentAuth.client); + } catch (error: unknown) { + console.error("Login failed:", error); + } + })(); + } +} + +export async function logout() { + const currentAuth = get(auth); + + if (currentAuth.state === "initialized") { + await currentAuth.client.signOut(); + auth.update(() => ({ + state: "anonymous", + client: currentAuth.client, + })); + void replace("/"); + } +} + +export async function authenticate(client: AuthClient) { + void handleSessionTimeout(client); + + try { + const identity = await client.getIdentity(); + const encryptedMaps = await createEncryptedMaps({ identity }); + + auth.update(() => ({ + state: "initialized", + encryptedMaps, + client, + principal: identity.getPrincipal(), + })); + } catch (e) { + auth.update(() => ({ + state: "error", + error: (e as Error).message || "An error occurred", + })); + } +} + +// set a timer when the II session will expire and log the user out +async function handleSessionTimeout(client: AuthClient) { + try { + const identity = await client.getIdentity(); + if (!(identity instanceof DelegationIdentity)) return; + + const chain = identity.getDelegation(); + // expiration is a BigInt of nanoseconds since epoch + const expirationMs = + Number(chain.delegations[0].delegation.expiration) / 1_000_000; + + setTimeout(() => { + void logout(); + }, expirationMs - Date.now()); + } catch { + console.error("Could not handle delegation expiry."); + } +} diff --git a/motoko/vetkeys/password_manager/frontend/src/store/draft.ts b/motoko/vetkeys/password_manager/frontend/src/store/draft.ts new file mode 100644 index 0000000000..05835819ee --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/src/store/draft.ts @@ -0,0 +1,36 @@ +import { writable } from "svelte/store"; +import { auth } from "./auth"; + +interface DraftModel { + content: string; +} + +let initialDraft: DraftModel = { + content: "", +}; + +try { + const getDraft = localStorage.getItem("draft"); + if (getDraft) { + const savedDraft: DraftModel = JSON.parse(getDraft) as DraftModel; + if ("content" in savedDraft && "tags" in savedDraft) { + initialDraft = savedDraft; + } + } else { + throw new Error("Draft not found"); + } +} catch { + // ignore error +} + +export const draft = writable(initialDraft); + +draft.subscribe((draft) => { + localStorage.setItem("draft", JSON.stringify(draft)); +}); + +auth.subscribe(($auth) => { + if ($auth.state === "anonymous") { + draft.set(initialDraft); + } +}); diff --git a/motoko/vetkeys/password_manager/frontend/src/store/notifications.ts b/motoko/vetkeys/password_manager/frontend/src/store/notifications.ts new file mode 100644 index 0000000000..d876301531 --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/src/store/notifications.ts @@ -0,0 +1,30 @@ +import { writable } from "svelte/store"; + +export interface Notification { + type: "error" | "info" | "success"; + message: string; + id: number; +} + +export type NewNotification = Omit; + +let nextId = 0; + +export const notifications = writable([]); + +export function addNotification(notification: NewNotification, timeout = 2000) { + const id = nextId++; + + notifications.update(($n) => [...$n, { ...notification, id }]); + + setTimeout(() => { + notifications.update(($n) => $n.filter((n) => n.id != id)); + }, timeout); +} + +export function showError(e: Error, message: string): never { + addNotification({ type: "error", message }); + console.error(e); + console.error(e.stack); + throw e; +} diff --git a/motoko/vetkeys/password_manager/frontend/src/store/vaults.ts b/motoko/vetkeys/password_manager/frontend/src/store/vaults.ts new file mode 100644 index 0000000000..288d172b73 --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/src/store/vaults.ts @@ -0,0 +1,166 @@ +import { writable } from "svelte/store"; +import { passwordFromContent, type PasswordModel } from "../lib/password"; +import { vaultFromContent, type VaultModel } from "../lib/vault"; +import { auth } from "./auth"; +import { showError } from "./notifications"; +import { + type AccessRights, + EncryptedMaps, +} from "@icp-sdk/vetkeys/encrypted_maps"; +import type { Principal } from "@icp-sdk/core/principal"; + +export const vaultsStore = writable< + | { + state: "uninitialized"; + } + | { + state: "loading"; + } + | { + state: "loaded"; + list: VaultModel[]; + } + | { + state: "error"; + } +>({ state: "uninitialized" }); + +let vaultPollerHandle: ReturnType | null; + +function updateVaults(vaults: VaultModel[]) { + vaultsStore.set({ + state: "loaded", + list: vaults, + }); +} + +export async function refreshVaults(encryptedMaps: EncryptedMaps) { + const allMaps = await encryptedMaps.getAllAccessibleMaps(); + const vaults = allMaps.map((mapData) => { + const vaultName = new TextDecoder().decode(mapData.mapName); + const passwords = new Array<[string, PasswordModel]>(); + for (const [passwordNameBytes, data] of mapData.keyvals) { + const passwordName = new TextDecoder().decode(passwordNameBytes); + const passwordContent = new TextDecoder().decode( + Uint8Array.from(data), + ); + const password = passwordFromContent( + mapData.mapOwner, + vaultName, + passwordName, + passwordContent, + ); + passwords.push([passwordName, password]); + } + return vaultFromContent( + mapData.mapOwner, + vaultName, + passwords, + mapData.accessControl, + ); + }); + + updateVaults(vaults); +} + +export async function addPassword( + password: PasswordModel, + encryptedMaps: EncryptedMaps, +) { + await encryptedMaps.setValue( + password.owner, + new TextEncoder().encode(password.parentVaultName), + new TextEncoder().encode(password.passwordName), + new TextEncoder().encode(password.content), + ); +} + +export async function removePassword( + password: PasswordModel, + encryptedMaps: EncryptedMaps, +) { + await encryptedMaps.removeEncryptedValue( + password.owner, + new TextEncoder().encode(password.parentVaultName), + new TextEncoder().encode(password.passwordName), + ); +} + +export async function updatePassword( + password: PasswordModel, + encryptedMaps: EncryptedMaps, +) { + await encryptedMaps.setValue( + password.owner, + new TextEncoder().encode(password.parentVaultName), + new TextEncoder().encode(password.passwordName), + new TextEncoder().encode(password.content), + ); +} + +export async function addUser( + owner: Principal, + vaultName: string, + user: Principal, + userRights: AccessRights, + encryptedMaps: EncryptedMaps, +) { + await encryptedMaps.setUserRights( + owner, + new TextEncoder().encode(vaultName), + user, + userRights, + ); +} + +export async function removeUser( + owner: Principal, + vaultName: string, + user: Principal, + encryptedMaps: EncryptedMaps, +) { + await encryptedMaps.removeUser( + owner, + new TextEncoder().encode(vaultName), + user, + ); +} + +auth.subscribe((auth) => { + void (async () => { + if (auth && auth.state === "initialized") { + if (vaultPollerHandle !== null) { + clearInterval(vaultPollerHandle); + vaultPollerHandle = null; + } + + vaultsStore.set({ + state: "loading", + }); + try { + await refreshVaults(auth.encryptedMaps).catch((e: Error) => + showError(e, "Could not poll vaults."), + ); + + vaultPollerHandle = setInterval(() => { + void (async () => { + await refreshVaults(auth.encryptedMaps).catch( + (e: Error) => + showError(e, "Could not poll vaults."), + ); + })(); + }, 3000); + } catch { + vaultsStore.set({ + state: "error", + }); + } + } else if (auth.state === "anonymous" && vaultPollerHandle !== null) { + clearInterval(vaultPollerHandle); + vaultPollerHandle = null; + vaultsStore.set({ + state: "uninitialized", + }); + } + })(); +}); diff --git a/motoko/vetkeys/password_manager/frontend/src/vite-env.d.ts b/motoko/vetkeys/password_manager/frontend/src/vite-env.d.ts new file mode 100644 index 0000000000..4078e7476a --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/motoko/vetkeys/password_manager/frontend/svelte.config.js b/motoko/vetkeys/password_manager/frontend/svelte.config.js new file mode 100644 index 0000000000..b0683fd24d --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/svelte.config.js @@ -0,0 +1,7 @@ +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' + +export default { + // Consult https://svelte.dev/docs#compile-time-svelte-preprocess + // for more information about preprocessors + preprocess: vitePreprocess(), +} diff --git a/motoko/vetkeys/password_manager/frontend/tailwind.config.mjs b/motoko/vetkeys/password_manager/frontend/tailwind.config.mjs new file mode 100644 index 0000000000..4bcec25673 --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/tailwind.config.mjs @@ -0,0 +1,6 @@ +import daisyui from "daisyui"; + +export default { + content: ["./index.html", "./src/**/*.{svelte,js,ts,jsx,tsx}"], + plugins: [daisyui], +}; diff --git a/motoko/vetkeys/password_manager/frontend/tsconfig.json b/motoko/vetkeys/password_manager/frontend/tsconfig.json new file mode 100644 index 0000000000..2f9865d464 --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "@tsconfig/svelte/tsconfig.json", + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ES2020", + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable" + ], + "skipLibCheck": true, + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "noUncheckedSideEffectImports": true + }, + "include": [ + "src" + ] + } + \ No newline at end of file diff --git a/motoko/vetkeys/password_manager/frontend/vite.config.ts b/motoko/vetkeys/password_manager/frontend/vite.config.ts new file mode 100644 index 0000000000..cc35e4ee7c --- /dev/null +++ b/motoko/vetkeys/password_manager/frontend/vite.config.ts @@ -0,0 +1,54 @@ +import { defineConfig } from "vite"; +import { svelte } from "@sveltejs/vite-plugin-svelte"; +import tailwindcss from "tailwindcss"; +import autoprefixer from "autoprefixer"; +import css from "rollup-plugin-css-only"; +import { execSync } from "child_process"; + +function getDevServerConfig() { + try { + const canisterId = execSync("icp canister status backend -e local -i", { + encoding: "utf-8", + stdio: "pipe", + }).trim(); + const networkStatus = JSON.parse( + execSync("icp network status --json", { + encoding: "utf-8", + stdio: "pipe", + }) + ); + return { + headers: { + "Set-Cookie": `ic_env=${encodeURIComponent( + `ic_root_key=${networkStatus.root_key}&PUBLIC_CANISTER_ID:backend=${canisterId}` + )}; SameSite=Lax;`, + }, + proxy: { + "/api": { target: "http://127.0.0.1:8000", changeOrigin: true }, + }, + }; + } catch {} + + throw new Error( + "No local network running. Start with:\n icp network start -d && icp deploy" + ); +} + +export default defineConfig(({ command }) => ({ + base: "./", + plugins: [svelte(), css({ output: "bundle.css" })], + css: { + postcss: { + plugins: [autoprefixer(), tailwindcss()], + }, + }, + build: { + sourcemap: true, + rollupOptions: { + output: { + inlineDynamicImports: true, + }, + }, + }, + server: command === "serve" ? getDevServerConfig() : undefined, +})); diff --git a/rust/vetkeys/password_manager/motoko/icp.yaml b/motoko/vetkeys/password_manager/icp.yaml similarity index 59% rename from rust/vetkeys/password_manager/motoko/icp.yaml rename to motoko/vetkeys/password_manager/icp.yaml index 457ba76c1b..ba5979ac3f 100644 --- a/rust/vetkeys/password_manager/motoko/icp.yaml +++ b/motoko/vetkeys/password_manager/icp.yaml @@ -1,18 +1,19 @@ canisters: - - name: ic_vetkeys_encrypted_maps_canister + - name: backend recipe: type: "@dfinity/motoko@v5.0.0" init_args: type: text value: "(\"test_key_1\")" - - name: www + - name: frontend recipe: type: "@dfinity/asset-canister@v2.2.1" configuration: - dir: dist + dir: frontend/dist build: - - cd frontend && npm i --include=dev && npm run build && cd - && rm -rf dist; mv frontend/dist ./ + - npm install --prefix frontend + - npm run build --prefix frontend networks: - name: local diff --git a/motoko/vetkeys/password_manager/mops.toml b/motoko/vetkeys/password_manager/mops.toml new file mode 100644 index 0000000000..5dae136934 --- /dev/null +++ b/motoko/vetkeys/password_manager/mops.toml @@ -0,0 +1,16 @@ +[toolchain] +moc = "1.11.0" + +[dependencies] +core = "2.5.0" +ic-vetkeys = "0.5.0" + +[moc] +# M0236: use context dot notation +# M0237: redundant explicit implicit arguments +# M0223: redundant type instantiation +args = ["--default-persistent-actors", "-W=M0236,M0237,M0223"] + +[canisters.backend] +main = "backend/app.mo" +candid = "backend/backend.did" diff --git a/motoko/vetkeys/password_manager/package.json b/motoko/vetkeys/password_manager/package.json new file mode 100644 index 0000000000..5be9e1f131 --- /dev/null +++ b/motoko/vetkeys/password_manager/package.json @@ -0,0 +1,13 @@ +{ + "name": "password_manager", + "private": true, + "type": "module", + "scripts": { + "build": "npm run build --workspaces --if-present", + "prebuild": "npm run prebuild --workspaces --if-present", + "dev": "npm run dev --workspaces --if-present" + }, + "workspaces": [ + "frontend" + ] +} diff --git a/rust/vetkeys/password_manager/rust/Cargo.toml b/rust/vetkeys/password_manager/Cargo.toml similarity index 100% rename from rust/vetkeys/password_manager/rust/Cargo.toml rename to rust/vetkeys/password_manager/Cargo.toml diff --git a/rust/vetkeys/password_manager/README.md b/rust/vetkeys/password_manager/README.md index 0b2478c9fe..966be9e4bf 100644 --- a/rust/vetkeys/password_manager/README.md +++ b/rust/vetkeys/password_manager/README.md @@ -1,10 +1,8 @@ -# VetKey Password Manager +# VetKey Password Manager (Rust) - +[View this sample's code on GitHub](https://github.com/dfinity/examples/tree/master/rust/vetkeys/password_manager) + +Also available in: [Motoko](../../../motoko/vetkeys/password_manager) The **VetKey Password Manager** is an example application demonstrating how to use **VetKeys** and **Encrypted Maps** to build a secure, decentralized password manager on the **Internet Computer (IC)**. This application allows users to create password vaults, store encrypted passwords, and share vaults with other users via their **Internet Identity Principal**. @@ -14,71 +12,72 @@ The **VetKey Password Manager** is an example application demonstrating how to u - **Vault-Based Organization**: Users can create multiple vaults, each containing multiple passwords. - **Access Control**: Vaults can be shared with other users via their **Internet Identity Principal**. -## Setup +## Build and deploy from the command line ### Prerequisites -- [ICP CLI](https://cli.internetcomputer.org) -- [npm](https://www.npmjs.com/package/npm) - -### (Optionally) Choose a Different Master Key +- Install [Node.js](https://nodejs.org/en/download/) +- Install [icp-cli](https://cli.internetcomputer.org): `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm` +- Install the [Rust toolchain](https://www.rust-lang.org/tools/install), then add the WASM target: `rustup target add wasm32-unknown-unknown` -This example uses `test_key_1` by default. To use a different [available master key](https://docs.internetcomputer.org/concepts/vetkeys/#api-overview), change the `init_args` value in `icp.yaml` to the desired key before running `icp deploy` in the next step. +### (Optionally) choose a different master key -### Folder Structure +This example uses `test_key_1` by default. To use a different [available master key](https://docs.internetcomputer.org/concepts/vetkeys/#api-overview), change the `init_args` value in `icp.yaml` before deploying. -This example provides both a **Rust** and a **Motoko** backend, sharing a common `frontend/`: +### Install -``` -password_manager/ -├── frontend/ ← shared frontend (symlinked into rust/ and motoko/) -├── motoko/ ← Motoko backend + icp.yaml -└── rust/ ← Rust backend + icp.yaml +```bash +git clone https://github.com/dfinity/examples +cd examples/rust/vetkeys/password_manager ``` -### Deploy the Canisters Locally +### Deploy -Deploy with the **Motoko** backend: ```bash -cd motoko -icp network start -d && icp deploy +icp network start -d +icp deploy ``` -Or deploy with the **Rust** backend: -```bash -cd rust -icp network start -d && icp deploy -``` +Open the frontend URL printed by `icp deploy`. + +To run the frontend in development mode with hot reloading (after `icp deploy`): -To run the frontend in development mode with hot reloading (after running `icp deploy`): ```bash -cd frontend -npm run dev:motoko # if you deployed the Motoko backend -# or -npm run dev:rust # if you deployed the Rust backend +npm run dev ``` -When you are done testing, stop the local network to free up resources and unblock the default port for other projects: +When done, stop the local network to free up the port for other projects: + ```bash icp network stop ``` -## Example Components +## Example components + +### Backend (`backend/`) + +An **Encrypted Maps**-enabled Rust canister that securely stores passwords. + +> **Note.** This backend is hand-written today. An upstream Rust macro that generates an entire Encrypted Maps canister in one line — `ic_vetkeys::export_encrypted_maps_canister!(...)` — is in progress ([dfinity/vetkeys#404](https://github.com/dfinity/vetkeys/pull/404)); it produces the same Candid interface with far less boilerplate. -### Backend +### Frontend (`frontend/`) -The backend consists of an **Encrypted Maps**-enabled canister that securely stores passwords. +A **Svelte** application providing a user-friendly interface for managing vaults and passwords. It talks to the backend through the `@icp-sdk/vetkeys` Encrypted Maps client. -### Frontend +## Updating the Candid interface -The frontend is a **Svelte** application providing a user-friendly interface for managing vaults and passwords. +`backend/backend.did` defines the backend's public interface. If you change the backend's public API, regenerate it: + +```bash +icp build backend && candid-extractor target/wasm32-unknown-unknown/release/backend.wasm > backend/backend.did +``` ## Limitations -This example dapp does not implement key rotation, which is strongly recommended in a production environment. -Key rotation involves periodically changing encryption keys and re-encrypting data to enhance security. -In a production dapp, key rotation would be useful to limit the impact of potential key compromise if a malicious party gains access to a key, or to limit access when users are added or removed from note sharing. +This example app does not implement key rotation, which is strongly recommended in a production environment. Key rotation involves periodically changing encryption keys and re-encrypting data to enhance security. In a production app, key rotation would be useful to limit the impact of a potential key compromise, or to limit access when users are added to or removed from sharing. -## Additional Resources +## Additional resources -- **[Password Manager with Metadata](../password_manager_with_metadata/)** - If you need to store additional metadata alongside passwords. +- **[Password Manager with Metadata](../password_manager_with_metadata)** — if you need to store additional metadata alongside passwords. +- **[What are VetKeys](https://docs.internetcomputer.org/concepts/vetkeys)** — more information about VetKeys and VetKD. +- [Security best practices](https://docs.internetcomputer.org/guides/security/overview/) diff --git a/rust/vetkeys/password_manager/rust/backend/Cargo.toml b/rust/vetkeys/password_manager/backend/Cargo.toml similarity index 93% rename from rust/vetkeys/password_manager/rust/backend/Cargo.toml rename to rust/vetkeys/password_manager/backend/Cargo.toml index 7e3c821a23..fd6843d1da 100644 --- a/rust/vetkeys/password_manager/rust/backend/Cargo.toml +++ b/rust/vetkeys/password_manager/backend/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "ic-vetkeys-encrypted-maps-canister" +name = "backend" authors = ["DFINITY Stiftung"] version = "0.1.0" edition = "2021" diff --git a/rust/vetkeys/password_manager/motoko/backend/README.md b/rust/vetkeys/password_manager/backend/README.md similarity index 100% rename from rust/vetkeys/password_manager/motoko/backend/README.md rename to rust/vetkeys/password_manager/backend/README.md diff --git a/rust/vetkeys/password_manager/rust/backend/ic_vetkeys_encrypted_maps_canister.did b/rust/vetkeys/password_manager/backend/backend.did similarity index 100% rename from rust/vetkeys/password_manager/rust/backend/ic_vetkeys_encrypted_maps_canister.did rename to rust/vetkeys/password_manager/backend/backend.did diff --git a/rust/vetkeys/password_manager/rust/backend/src/lib.rs b/rust/vetkeys/password_manager/backend/src/lib.rs similarity index 100% rename from rust/vetkeys/password_manager/rust/backend/src/lib.rs rename to rust/vetkeys/password_manager/backend/src/lib.rs diff --git a/rust/vetkeys/password_manager/frontend/.gitignore b/rust/vetkeys/password_manager/frontend/.gitignore new file mode 100644 index 0000000000..b947077876 --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/rust/vetkeys/password_manager/frontend/.prettierignore b/rust/vetkeys/password_manager/frontend/.prettierignore deleted file mode 100644 index 987c289398..0000000000 --- a/rust/vetkeys/password_manager/frontend/.prettierignore +++ /dev/null @@ -1,7 +0,0 @@ -# Ignore artifacts: -build -coverage -dist -README.md -**/declarations/ - diff --git a/rust/vetkeys/password_manager/frontend/.prettierrc b/rust/vetkeys/password_manager/frontend/.prettierrc deleted file mode 100644 index 2b9bd83ee0..0000000000 --- a/rust/vetkeys/password_manager/frontend/.prettierrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "plugins": ["prettier-plugin-svelte"], - "tabWidth": 4, - "overrides": [{ "files": ["*.svelte"], "options": { "parser": "svelte" } }] -} diff --git a/rust/vetkeys/password_manager/frontend/README.md b/rust/vetkeys/password_manager/frontend/README.md deleted file mode 100644 index 134a1abb0c..0000000000 --- a/rust/vetkeys/password_manager/frontend/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# VetKD Password Manager frontend -Uses the defaults provided by the devkit to implement a VetKD-based password -manager. This utilizes the encrypted maps canister example to realize the -password manager, i.e., there is no dedicated canister implementation, only the -frontend implementation that uses all defaults from the SDK. - -## Step 1: Deploy `encrypted_maps_example` canister and the internet identity canister. - -## Step 2: Tell `frontend` what canisters to communicate with, so the following environment variables must be defined. For a local deployment, one can run `deploy_locally.sh` from that folder. -* `CANISTER_ID_IC_VETKEYS_ENCRYPTED_MAPS_CANISTER` - -## Step 3: Deploy frontend. This returns a link that can be used to access the frontend from the asset canister. -```shell -dfx deploy www -``` -Note: if this returns a URL with the IP `0.0.0.0` and the fronetned does not -work, a potential fix is to replace it with `localhost`. \ No newline at end of file diff --git a/rust/vetkeys/password_manager/frontend/eslint.config.mjs b/rust/vetkeys/password_manager/frontend/eslint.config.mjs deleted file mode 100644 index 96aee8e5ce..0000000000 --- a/rust/vetkeys/password_manager/frontend/eslint.config.mjs +++ /dev/null @@ -1,56 +0,0 @@ -// @ts-check - -import eslint from "@eslint/js"; -import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; -import globals from "globals"; -import tseslint from "typescript-eslint"; -import svelteConfig from "./svelte.config.js"; -import svelte from "eslint-plugin-svelte"; - -export default tseslint.config( - eslint.configs.recommended, - tseslint.configs.recommendedTypeChecked, - ...svelte.configs.recommended, - eslintPluginPrettierRecommended, - { - languageOptions: { - parserOptions: { - projectService: { - defaultProject: "./tsconfig.json", - }, - tsconfigRootDir: import.meta.dirname, - }, - globals: { - ...globals.browser, - ...globals.es2020, - }, - }, - }, - { - files: ["**/*.svelte", "**/*.svelte.ts", "**/*.svelte.js"], - languageOptions: { - parserOptions: { - projectService: true, - extraFileExtensions: [".svelte"], - parser: tseslint.parser, - svelteConfig, - }, - }, - }, - { - ignores: [ - "dist/", - "src/declarations", - "*.config.js", - "*.config.cjs", - "*.config.mjs", - "*.config.ts", - ], - }, - { - rules: { - "@typescript-eslint/no-unsafe-argument": "off", - "@typescript-eslint/no-unsafe-member-access": "off", - }, - }, -); diff --git a/rust/vetkeys/password_manager/frontend/package.json b/rust/vetkeys/password_manager/frontend/package.json index a88da97ae1..ba0e1f6b23 100644 --- a/rust/vetkeys/password_manager/frontend/package.json +++ b/rust/vetkeys/password_manager/frontend/package.json @@ -1,15 +1,11 @@ { - "name": "password-manager-frontend", - "version": "0.1.0", + "name": "frontend", + "private": true, "type": "module", "scripts": { - "dev": "printf '\\nNo backend specified. Use one of:\\n\\n npm run dev:motoko\\n npm run dev:rust\\n\\n' && exit 1", - "dev:motoko": "BACKEND=motoko vite", - "dev:rust": "BACKEND=rust vite", + "prebuild": "npm i --include=dev", + "dev": "vite", "build": "vite build", - "lint": "eslint", - "prettier": "prettier --write .", - "prettier-check": "prettier --check .", "preview": "vite preview" }, "devDependencies": { @@ -17,14 +13,8 @@ "@tsconfig/svelte": "^5.0.4", "@typewriter/delta": "^1.2.4", "daisyui": "^4.12.23", - "eslint-config-prettier": "^10.1.5", - "eslint-plugin-prettier": "^5.2.6", - "eslint-plugin-svelte": "^3.5.1", - "globals": "^16.0.0", - "prettier-plugin-svelte": "^3.4.0", "svelte": "^4.2.19", "tslib": "^2.8.1", - "typescript-eslint": "^8.35.1", "vite": "^5.4.21" }, "dependencies": { diff --git a/rust/vetkeys/password_manager/frontend/src/lib/encrypted_maps.ts b/rust/vetkeys/password_manager/frontend/src/lib/encrypted_maps.ts index ce034b20f5..9972d6965c 100644 --- a/rust/vetkeys/password_manager/frontend/src/lib/encrypted_maps.ts +++ b/rust/vetkeys/password_manager/frontend/src/lib/encrypted_maps.ts @@ -7,17 +7,17 @@ import { import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env"; const canisterEnv = safeGetCanisterEnv<{ - "PUBLIC_CANISTER_ID:ic_vetkeys_encrypted_maps_canister": string; + "PUBLIC_CANISTER_ID:backend": string; }>(); export async function createEncryptedMaps( agentOptions?: HttpAgentOptions, ): Promise { const canisterId = - canisterEnv?.["PUBLIC_CANISTER_ID:ic_vetkeys_encrypted_maps_canister"]; + canisterEnv?.["PUBLIC_CANISTER_ID:backend"]; if (!canisterId) { throw new Error( - "Canister ID for ic_vetkeys_encrypted_maps_canister is not set", + "Canister ID for backend is not set", ); } diff --git a/rust/vetkeys/password_manager/frontend/vite.config.ts b/rust/vetkeys/password_manager/frontend/vite.config.ts index 0146433f6d..cc35e4ee7c 100644 --- a/rust/vetkeys/password_manager/frontend/vite.config.ts +++ b/rust/vetkeys/password_manager/frontend/vite.config.ts @@ -5,43 +5,37 @@ import autoprefixer from "autoprefixer"; import css from "rollup-plugin-css-only"; import { execSync } from "child_process"; -const environment = process.env.ICP_ENVIRONMENT || "local"; -const CANISTER_NAMES = ["ic_vetkeys_encrypted_maps_canister"]; - function getDevServerConfig() { - const backend = process.env.BACKEND; - if (!backend) { - throw new Error( - "BACKEND env var is required. Use `npm run dev:motoko` or `npm run dev:rust`.", + try { + const canisterId = execSync("icp canister status backend -e local -i", { + encoding: "utf-8", + stdio: "pipe", + }).trim(); + const networkStatus = JSON.parse( + execSync("icp network status --json", { + encoding: "utf-8", + stdio: "pipe", + }) ); - } + return { + headers: { + "Set-Cookie": `ic_env=${encodeURIComponent( + `ic_root_key=${networkStatus.root_key}&PUBLIC_CANISTER_ID:backend=${canisterId}` + )}; SameSite=Lax;`, + }, + proxy: { + "/api": { target: "http://127.0.0.1:8000", changeOrigin: true }, + }, + }; + } catch {} - const networkStatus = JSON.parse( - execSync(`icp network status -e ${environment} --json --project-root-override ../${backend}`, { - encoding: "utf-8", - }), + throw new Error( + "No local network running. Start with:\n icp network start -d && icp deploy" ); - const canisterParams = CANISTER_NAMES.map((name) => { - const id = execSync( - `icp canister status ${name} -e ${environment} --id-only --project-root-override ../${backend}`, - { encoding: "utf-8", stdio: "pipe" }, - ).trim(); - return `PUBLIC_CANISTER_ID:${name}=${id}`; - }).join("&"); - return { - headers: { - "Set-Cookie": `ic_env=${encodeURIComponent( - `${canisterParams}&ic_root_key=${networkStatus.root_key}`, - )}; SameSite=Lax;`, - }, - proxy: { - "/api": { target: networkStatus.api_url, changeOrigin: true }, - }, - hmr: false, - }; } export default defineConfig(({ command }) => ({ + base: "./", plugins: [svelte(), css({ output: "bundle.css" })], css: { postcss: { @@ -56,6 +50,5 @@ export default defineConfig(({ command }) => ({ }, }, }, - root: "./", - ...(command === "serve" ? { server: getDevServerConfig() } : {}), + server: command === "serve" ? getDevServerConfig() : undefined, })); diff --git a/rust/vetkeys/password_manager/icp.yaml b/rust/vetkeys/password_manager/icp.yaml new file mode 100644 index 0000000000..3b7759d854 --- /dev/null +++ b/rust/vetkeys/password_manager/icp.yaml @@ -0,0 +1,23 @@ +canisters: + - name: backend + recipe: + type: "@dfinity/rust@v3.3.0" + configuration: + candid: backend/backend.did + init_args: + type: text + value: "(\"test_key_1\")" + + - name: frontend + recipe: + type: "@dfinity/asset-canister@v2.2.1" + configuration: + dir: frontend/dist + build: + - npm install --prefix frontend + - npm run build --prefix frontend + +networks: + - name: local + mode: managed + ii: true diff --git a/rust/vetkeys/password_manager/motoko/backend/.prettierrc b/rust/vetkeys/password_manager/motoko/backend/.prettierrc deleted file mode 100644 index a42bc32c36..0000000000 --- a/rust/vetkeys/password_manager/motoko/backend/.prettierrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "overrides": [{ - "files": "*.mo", - "options": { - "tabWidth": 4 - } - }] - } \ No newline at end of file diff --git a/rust/vetkeys/password_manager/motoko/backend/Makefile b/rust/vetkeys/password_manager/motoko/backend/Makefile deleted file mode 100644 index ffaeb96426..0000000000 --- a/rust/vetkeys/password_manager/motoko/backend/Makefile +++ /dev/null @@ -1,14 +0,0 @@ -PWD:=$(shell pwd) - -.PHONY: compile-wasm -.SILENT: compile-wasm -compile-wasm: - icp build - -# Test the APIs of this canister using the respective Rust canister tests. -# This has the advantage that the tests are consistent (less room for bugs by having only one implementation of the tests) and the checked expected behavior is consistent across Rust and Motoko. -.PHONY: test -.SILENT: test -test: compile-wasm - @echo "Testing Motoko canister WASM: $(PWD)/.icp/cache/artifacts/ic_vetkeys_encrypted_maps_canister" - CUSTOM_WASM_PATH=$(PWD)/.icp/cache/artifacts/ic_vetkeys_encrypted_maps_canister cargo test -p ic-vetkeys-encrypted-maps-canister diff --git a/rust/vetkeys/password_manager/motoko/frontend b/rust/vetkeys/password_manager/motoko/frontend deleted file mode 120000 index af288785f3..0000000000 --- a/rust/vetkeys/password_manager/motoko/frontend +++ /dev/null @@ -1 +0,0 @@ -../frontend \ No newline at end of file diff --git a/rust/vetkeys/password_manager/motoko/mops.toml b/rust/vetkeys/password_manager/motoko/mops.toml deleted file mode 100644 index 45b392e374..0000000000 --- a/rust/vetkeys/password_manager/motoko/mops.toml +++ /dev/null @@ -1,9 +0,0 @@ -[toolchain] -moc = "1.9.0" - -[dependencies] -core = "2.5.0" -ic-vetkeys = "0.5.0" - -[canisters.ic_vetkeys_encrypted_maps_canister] -main = "backend/src/Main.mo" diff --git a/rust/vetkeys/password_manager/package.json b/rust/vetkeys/password_manager/package.json new file mode 100644 index 0000000000..5be9e1f131 --- /dev/null +++ b/rust/vetkeys/password_manager/package.json @@ -0,0 +1,13 @@ +{ + "name": "password_manager", + "private": true, + "type": "module", + "scripts": { + "build": "npm run build --workspaces --if-present", + "prebuild": "npm run prebuild --workspaces --if-present", + "dev": "npm run dev --workspaces --if-present" + }, + "workspaces": [ + "frontend" + ] +} diff --git a/rust/vetkeys/password_manager/rust/rust-toolchain.toml b/rust/vetkeys/password_manager/rust-toolchain.toml similarity index 100% rename from rust/vetkeys/password_manager/rust/rust-toolchain.toml rename to rust/vetkeys/password_manager/rust-toolchain.toml diff --git a/rust/vetkeys/password_manager/rust/backend/Makefile b/rust/vetkeys/password_manager/rust/backend/Makefile deleted file mode 100644 index b6dc7595ee..0000000000 --- a/rust/vetkeys/password_manager/rust/backend/Makefile +++ /dev/null @@ -1,22 +0,0 @@ -ROOT_DIR := $(shell git rev-parse --show-toplevel) - -.PHONY: compile-wasm -.SILENT: compile-wasm -compile-wasm: - cargo build --release --target wasm32-unknown-unknown - -.PHONY: test -.SILENT: test -test: compile-wasm - cargo test -p ic-vetkeys-encrypted-maps-canister - -.PHONY: extract-candid -.SILENT: extract-candid -extract-candid: compile-wasm - candid-extractor $(ROOT_DIR)/target/wasm32-unknown-unknown/release/ic_vetkeys_encrypted_maps_canister.wasm > ic_vetkeys_encrypted_maps_canister.did - -.PHONY: clean -.SILENT: clean -clean: - cargo clean - rm -rf .icp/cache \ No newline at end of file diff --git a/rust/vetkeys/password_manager/rust/backend/README.md b/rust/vetkeys/password_manager/rust/backend/README.md deleted file mode 100644 index 71f0ac924b..0000000000 --- a/rust/vetkeys/password_manager/rust/backend/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# ic-vetkeys-encrypted-maps-canister - -The canister implemented in this folder directly exposes the methods of the encrypted maps. -This is useful for: - -1. running canister tests -2. implementing dapps that only require encrypted maps \ No newline at end of file diff --git a/rust/vetkeys/password_manager/rust/frontend b/rust/vetkeys/password_manager/rust/frontend deleted file mode 120000 index af288785f3..0000000000 --- a/rust/vetkeys/password_manager/rust/frontend +++ /dev/null @@ -1 +0,0 @@ -../frontend \ No newline at end of file diff --git a/rust/vetkeys/password_manager/rust/icp.yaml b/rust/vetkeys/password_manager/rust/icp.yaml deleted file mode 100644 index 2f3c18726b..0000000000 --- a/rust/vetkeys/password_manager/rust/icp.yaml +++ /dev/null @@ -1,23 +0,0 @@ -canisters: - - name: ic_vetkeys_encrypted_maps_canister - recipe: - type: "@dfinity/rust@v3.2.0" - configuration: - package: ic-vetkeys-encrypted-maps-canister - candid: backend/ic_vetkeys_encrypted_maps_canister.did - init_args: - type: text - value: "(\"test_key_1\")" - - - name: www - recipe: - type: "@dfinity/asset-canister@v2.2.1" - configuration: - dir: dist - build: - - cd frontend && npm i --include=dev && npm run build && cd - && rm -rf dist; mv frontend/dist ./ - -networks: - - name: local - mode: managed - ii: true