Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions .github/workflows/vetkeys-password-manager-with-metadata.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:
- master
pull_request:
paths:
- motoko/vetkeys/password_manager_with_metadata/**
- rust/vetkeys/password_manager_with_metadata/**
- .github/workflows/vetkeys-password-manager-with-metadata.yml

Expand All @@ -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 With Metadata Rust
working-directory: rust/vetkeys/password_manager_with_metadata/rust
run: icp network start -d && icp deploy
motoko:
- name: Deploy
working-directory: motoko/vetkeys/password_manager_with_metadata
run: |
icp network start -d
icp deploy

Comment thread
marc0olo marked this conversation as resolved.
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 With Metadata Motoko
working-directory: rust/vetkeys/password_manager_with_metadata/motoko
run: icp network start -d && icp deploy
- name: Deploy
working-directory: rust/vetkeys/password_manager_with_metadata
run: |
icp network start -d
icp deploy
78 changes: 78 additions & 0 deletions motoko/vetkeys/password_manager_with_metadata/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# VetKey Password Manager with Metadata (Motoko)

[View this sample's code on GitHub](https://github.com/dfinity/examples/tree/master/motoko/vetkeys/password_manager_with_metadata)

Also available in: [Rust](../../../rust/vetkeys/password_manager_with_metadata)

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**.

This version extends the basic password manager by supporting unencrypted metadata, such as URLs and tags, alongside encrypted passwords. The goal is to demonstrate how to make atomic updates to the Encrypted Maps canister, storing both encrypted and unencrypted data in a single update call.

## 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**.
- **Atomic Updates**: Stores encrypted passwords along with unencrypted metadata in a single update call.

## 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_with_metadata
```

### 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 stores encrypted passwords together with unencrypted metadata (URLs, tags) in atomic update calls.

> **Note on naming.** The backend methods are snake_case (rather than the usual Motoko camelCase). The standard Encrypted Maps methods are called by these exact names by the `@icp-sdk/vetkeys` Encrypted Maps client, and the custom metadata methods follow the same convention — renaming them would break the frontend. An upstream Motoko actor mixin that generates the Encrypted Maps endpoint set automatically is in progress ([dfinity/vetkeys#405](https://github.com/dfinity/vetkeys/pull/405)).

### Frontend (`frontend/`)

A **Svelte** application for managing vaults and passwords. It uses the `@icp-sdk/vetkeys` Encrypted Maps client for the crypto operations and a canister actor (bindings generated from `backend/backend.did` by the `@icp-sdk/bindgen` Vite plugin) for the metadata methods.

## 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

- **[Basic Password Manager](../../../rust/vetkeys/password_manager)** — a simpler example without metadata.
- **[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/)
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,18 @@ import Debug "mo:core/Debug";
import Runtime "mo:core/Runtime";
import VetKeys "mo:ic-vetkeys";

persistent actor class (keyName : Text) {
// This canister combines the Encrypted Maps interface with extra metadata-aware
// methods. Its public methods are intentionally snake_case (not the usual Motoko
// camelCase): the standard Encrypted Maps methods are called by these exact names
// by the `@icp-sdk/vetkeys` Encrypted Maps client, and the custom metadata
// methods follow the same convention for a consistent interface — renaming to
// camelCase would break the frontend. An upstream Motoko actor mixin that
// generates the Encrypted Maps endpoint set automatically is in progress
// (https://github.com/dfinity/vetkeys/pull/405).
actor class (keyName : Text) {

// Global state
let encryptedMapsState = VetKeys.EncryptedMaps.newEncryptedMapsState<VetKeys.AccessRights>({ curve = #bls12_381_g2; name = keyName }, "password_manager_example_dapp");
let encryptedMapsState = VetKeys.EncryptedMaps.newEncryptedMapsState<VetKeys.AccessRights>({ curve = #bls12_381_g2; name = keyName }, "password_manager_example_app");
transient let encryptedMaps = VetKeys.EncryptedMaps.EncryptedMaps<VetKeys.AccessRights>(encryptedMapsState, VetKeys.accessRightsOperations());

func compareMetadataKeys(a : MetadataKey, b : MetadataKey) : {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
type _anon_class_15_1 =
service {
get_accessible_shared_map_names: () ->
(vec record {
principal;
ByteBuf;
}) query;
get_encrypted_values_for_map_with_metadata: (map_owner: principal,
map_name: ByteBuf) -> (Result_4) query;
get_encrypted_vetkey: (map_owner: principal, map_name: ByteBuf,
transport_key: ByteBuf) -> (Result_3);
get_owned_non_empty_map_names: () -> (vec ByteBuf) query;
get_shared_user_access_for_map: (map_owner: principal, map_name:
ByteBuf) -> (Result_2) query;
get_user_rights: (map_owner: principal, map_name: ByteBuf, user:
principal) -> (Result) query;
get_vetkey_verification_key: () -> (ByteBuf);
insert_encrypted_value_with_metadata: (map_owner: principal, map_name:
ByteBuf, map_key: ByteBuf, value: ByteBuf, tags: vec text, url: text) ->
(Result_1);
remove_encrypted_value_with_metadata: (map_owner: principal, map_name:
ByteBuf, map_key: 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_4 =
variant {
Err: text;
Ok: vec record {
ByteBuf;
ByteBuf;
PasswordMetadata;
};
};
type Result_3 =
variant {
Err: text;
Ok: ByteBuf;
};
type Result_2 =
variant {
Err: text;
Ok: vec record {
principal;
AccessRights;
};
};
type Result_1 =
variant {
Err: text;
Ok: opt record {
ByteBuf;
PasswordMetadata;
};
};
type Result =
variant {
Err: text;
Ok: opt AccessRights;
};
type PasswordMetadata =
record {
creation_date: nat64;
last_modification_date: nat64;
last_modified_principal: principal;
number_of_modifications: nat64;
tags: vec text;
url: text;
};
type ByteBuf = record {inner: blob;};
type AccessRights =
variant {
Read;
ReadWrite;
ReadWriteManage;
};
service : (keyName: text) -> _anon_class_15_1
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
dist/
src/bindings/
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Password Manager with Metadata based on vetKeys</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<link rel="stylesheet" href="./bundle.css" />
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"compilerOptions": {
"moduleResolution": "bundler",
"target": "ESNext",
"module": "ESNext",
/**
* svelte-preprocess cannot figure out whether you have
* a value or a type, so tell TypeScript to enforce using
* `import type` instead of `import` for Types.
*/
"verbatimModuleSyntax": true,
"isolatedModules": true,
"resolveJsonModule": true,
/**
* To have warnings / errors of the Svelte compiler at the
* correct position, enable source maps by default.
*/
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable this if you'd like to use dynamic types.
*/
"checkJs": true
},
/**
* Use global.d.ts instead of compilerOptions.types
* to avoid limiting type declarations.
*/
"include": [
"src/**/*.d.ts",
"src/**/*.js",
"src/**/*.svelte",
"src/main.ts",
"src/main.ts"
]
}
Original file line number Diff line number Diff line change
@@ -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": {
"@icp-sdk/bindgen": "~0.2.2",
"@rollup/plugin-typescript": "^12.1.2",
"@tailwindcss/postcss": "^4.0.6",
"@tailwindcss/vite": "^4.0.0",
"@tsconfig/svelte": "^5.0.4",
"@typewriter/delta": "^1.2.4",
"autoprefixer": "^10.4.20",
"rollup-plugin-css-only": "^4.5.2",
"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",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"daisyui": "^4.12.23",
"svelte": "^4.2.19",
"svelte-icons": "^2.1.0",
"svelte-spa-router": "^4.0.1",
"tailwindcss": "^3.0.17",
"typewriter-editor": "^0.9.4"
}
}
Original file line number Diff line number Diff line change
@@ -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
},
]
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script lang="ts">
import Hero from "./components/Hero.svelte";
import LayoutAuthenticated from "./components/LayoutAuthenticated.svelte";
import Notifications from "./components/Notifications.svelte";
import { auth } from "./store/auth";
</script>

{#if $auth.state === "initialized"}
<LayoutAuthenticated />
{:else}
<Hero auth={$auth} />
{/if}
<Notifications />
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<script lang="ts">
import { fly } from "svelte/transition";
import DisclaimerCopy from "./DisclaimerCopy.svelte";
let isDismissed = !!window.localStorage.getItem("disclaimer-dismissed");

function dismiss() {
window.localStorage.setItem("disclaimer-dismissed", "yes");
isDismissed = true;
}
</script>

{#if !isDismissed}
<div
class="sticky bottom-0 mt-4 bg-base-300 p-4 text-xs sm:flex"
out:fly={{ y: 50 }}
>
<p class="opacity-90 sm:flex-1">
<DisclaimerCopy />
</p>

<button
class="btn btn-outline btn-xs mt-4 opacity-90 sm:btn-sm sm:ml-4 sm:mt-0 sm:self-start"
on:click={dismiss}>I understand</button
>
</div>
{/if}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script lang="ts"></script>

<strong>Disclaimer:</strong> 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.
Loading
Loading