From 660eae7b0e26d8f4255a6b5e8fc0889604f9810c Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 16 Jul 2026 17:48:41 +0200 Subject: [PATCH 1/7] chore(vetkeys): split encrypted_notes_app_vetkd into self-contained motoko + rust examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same split as basic_ibe (#1443) / basic_bls_signing (#1444): move the Motoko variant to motoko/vetkeys/encrypted_notes_app_vetkd and flatten the Rust variant to rust/vetkeys/encrypted_notes_app_vetkd, each self-contained with its own frontend copy (previously a shared Svelte frontend symlinked into motoko/ and rust/ subdirs). - Frontend (Svelte): per-language copy; bindings via the @icp-sdk/bindgen Vite plugin from the committed backend/backend.did (replaces scripts/gen_bindings.sh). Unified the three inconsistent binding import paths (declarations/…/encrypted_notes_rust, …/backend.did, ../lib/backend) to a single ../bindings/backend. Root workspace package.json so `npm run dev` runs from the example root; kept all Svelte deps. - Motoko: idiomatic camelCase public interface (createNote, getNotes, updateNote, addUser, removeUser, deleteNote, encryptedSymmetricKeyForNote, symmetricKeyVerificationKeyForNote; EncryptedNote.encryptedText). The vetKD management-canister interface is kept snake_case (fixed system API). moc 1.11.0 + --default-persistent-actors (dropped `persistent`). Committed backend/backend.did. - Rust: unchanged (snake_case); relocated, and the hand-maintained .did moved to backend/backend.did. - READMEs split per language (linked prerequisites); test.sh added (all methods are update calls here — no query); CI split into motoko + rust jobs. Note: the frontend carries pre-existing svelte-check type issues (auth store typing, missing lib type decls) that the build never gated on (build is `vite build`); this change introduces none. CODEOWNERS unchanged: /motoko/vetkeys/ and /rust/vetkeys/ both map to @dfinity/core-protocol. Verified: Motoko deploy + test.sh + a note survives an upgrade; Rust cargo build + deploy + test.sh; both frontends build/serve. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../vetkeys-encrypted-notes-app-vetkd.yml | 28 +-- .../encrypted_notes_app_vetkd/README.md | 76 ++++++++ .../encrypted_notes_app_vetkd/backend/app.mo | 30 ++-- .../backend/backend.did | 23 +++ .../backend/utils/Hex.mo | 0 .../frontend/.gitignore | 3 + .../encrypted_notes_app_vetkd/frontend/.npmrc | 1 + .../frontend/index.html | 12 ++ .../frontend/package.json | 37 ++++ .../frontend/postcss.config.mjs | 6 + .../frontend/public/.ic-assets.json5 | 10 ++ .../frontend/public/favicon.png | Bin 0 -> 3127 bytes ...wered-by-crypto_label-stripe-dark-text.png | Bin 0 -> 6265 bytes ...ered-by-crypto_label-stripe-white-text.png | Bin 0 -> 7672 bytes ...owered-by-crypto_transparent-dark-text.png | Bin 0 -> 12190 bytes ...wered-by-crypto_transparent-white-text.png | Bin 0 -> 11664 bytes .../frontend/src/App.svelte | 14 ++ .../frontend/src/app.css | 3 + .../frontend/src/components/Disclaimer.svelte | 26 +++ .../src/components/DisclaimerCopy.svelte | 3 + .../frontend/src/components/EditNote.svelte | 162 +++++++++++++++++ .../frontend/src/components/Header.svelte | 25 +++ .../frontend/src/components/Hero.svelte | 71 ++++++++ .../src/components/LayoutAuthenticated.svelte | 18 ++ .../frontend/src/components/NewNote.svelte | 93 ++++++++++ .../frontend/src/components/Note.svelte | 46 +++++ .../frontend/src/components/NoteEditor.svelte | 68 ++++++++ .../frontend/src/components/Notes.svelte | 75 ++++++++ .../src/components/Notifications.svelte | 23 +++ .../src/components/SharingEditor.svelte | 128 ++++++++++++++ .../src/components/SidebarLayout.svelte | 75 ++++++++ .../frontend/src/components/Spinner.svelte | 3 + .../frontend/src/components/TagEditor.svelte | 74 ++++++++ .../frontend/src/global.d.ts | 1 + .../frontend/src/lib/actor.ts | 19 ++ .../frontend/src/lib/crypto.ts | 78 +++++++++ .../frontend/src/lib/enums.ts | 8 + .../frontend/src/lib/note.ts | 106 +++++++++++ .../frontend/src/lib/sleep.ts | 3 + .../frontend/src/main.ts | 10 ++ .../frontend/src/store/auth.ts | 132 ++++++++++++++ .../frontend/src/store/draft.ts | 31 ++++ .../frontend/src/store/notes.ts | 133 ++++++++++++++ .../frontend/src/store/notifications.ts | 30 ++++ .../frontend/svelte.config.js | 5 + .../frontend/tailwind.config.mjs | 13 ++ .../frontend/tsconfig.json | 5 + .../frontend/vite.config.ts | 53 ++++++ .../encrypted_notes_app_vetkd}/icp.yaml | 11 +- .../encrypted_notes_app_vetkd/mops.toml | 15 ++ .../encrypted_notes_app_vetkd/package.json | 13 ++ .../security-checklist.md | 165 ++++++++++++++++++ .../vetkeys/encrypted_notes_app_vetkd/test.sh | 14 ++ .../{rust => }/Cargo.toml | 0 .../encrypted_notes_app_vetkd/README.md | 74 ++++---- .../{rust => }/backend/Cargo.toml | 0 .../backend.did} | 0 .../{rust => }/backend/src/lib.rs | 0 .../frontend/.gitignore | 3 + .../frontend/package.json | 74 ++++---- .../frontend/scripts/gen_bindings.sh | 18 -- .../frontend/src/lib/actor.ts | 6 +- .../frontend/src/lib/note.ts | 2 +- .../frontend/src/store/notes.ts | 2 +- .../frontend/vite.config.ts | 61 ++++--- .../{rust => }/icp.yaml | 13 +- .../encrypted_notes_app_vetkd/motoko/frontend | 1 - .../motoko/mops.toml | 8 - .../encrypted_notes_app_vetkd/package.json | 13 ++ .../{rust => }/rust-toolchain.toml | 0 .../encrypted_notes_app_vetkd/rust/frontend | 1 - .../vetkeys/encrypted_notes_app_vetkd/test.sh | 14 ++ 72 files changed, 2093 insertions(+), 175 deletions(-) create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/README.md rename rust/vetkeys/encrypted_notes_app_vetkd/motoko/backend/main.mo => motoko/vetkeys/encrypted_notes_app_vetkd/backend/app.mo (94%) create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/backend/backend.did rename {rust/vetkeys/encrypted_notes_app_vetkd/motoko => motoko/vetkeys/encrypted_notes_app_vetkd}/backend/utils/Hex.mo (100%) create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/.gitignore create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/.npmrc create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/index.html create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/package.json create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/postcss.config.mjs create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/public/.ic-assets.json5 create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/public/favicon.png create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/public/img/ic-badge-powered-by-crypto_label-stripe-dark-text.png create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/public/img/ic-badge-powered-by-crypto_label-stripe-white-text.png create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/public/img/ic-badge-powered-by-crypto_transparent-dark-text.png create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/public/img/ic-badge-powered-by-crypto_transparent-white-text.png create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/App.svelte create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/app.css create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/Disclaimer.svelte create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/DisclaimerCopy.svelte create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/EditNote.svelte create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/Header.svelte create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/Hero.svelte create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/LayoutAuthenticated.svelte create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/NewNote.svelte create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/Note.svelte create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/NoteEditor.svelte create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/Notes.svelte create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/Notifications.svelte create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/SharingEditor.svelte create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/SidebarLayout.svelte create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/Spinner.svelte create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/TagEditor.svelte create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/global.d.ts create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/lib/actor.ts create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/lib/crypto.ts create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/lib/enums.ts create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/lib/note.ts create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/lib/sleep.ts create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/main.ts create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/store/auth.ts create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/store/draft.ts create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/store/notes.ts create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/store/notifications.ts create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/svelte.config.js create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/tailwind.config.mjs create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/tsconfig.json create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/frontend/vite.config.ts rename {rust/vetkeys/encrypted_notes_app_vetkd/motoko => motoko/vetkeys/encrypted_notes_app_vetkd}/icp.yaml (50%) create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/mops.toml create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/package.json create mode 100644 motoko/vetkeys/encrypted_notes_app_vetkd/security-checklist.md create mode 100755 motoko/vetkeys/encrypted_notes_app_vetkd/test.sh rename rust/vetkeys/encrypted_notes_app_vetkd/{rust => }/Cargo.toml (100%) rename rust/vetkeys/encrypted_notes_app_vetkd/{rust => }/backend/Cargo.toml (100%) rename rust/vetkeys/encrypted_notes_app_vetkd/{rust/backend/src/encrypted_notes_rust.did => backend/backend.did} (100%) rename rust/vetkeys/encrypted_notes_app_vetkd/{rust => }/backend/src/lib.rs (100%) create mode 100644 rust/vetkeys/encrypted_notes_app_vetkd/frontend/.gitignore delete mode 100755 rust/vetkeys/encrypted_notes_app_vetkd/frontend/scripts/gen_bindings.sh rename rust/vetkeys/encrypted_notes_app_vetkd/{rust => }/icp.yaml (53%) delete mode 120000 rust/vetkeys/encrypted_notes_app_vetkd/motoko/frontend delete mode 100644 rust/vetkeys/encrypted_notes_app_vetkd/motoko/mops.toml create mode 100644 rust/vetkeys/encrypted_notes_app_vetkd/package.json rename rust/vetkeys/encrypted_notes_app_vetkd/{rust => }/rust-toolchain.toml (100%) delete mode 120000 rust/vetkeys/encrypted_notes_app_vetkd/rust/frontend create mode 100755 rust/vetkeys/encrypted_notes_app_vetkd/test.sh diff --git a/.github/workflows/vetkeys-encrypted-notes-app-vetkd.yml b/.github/workflows/vetkeys-encrypted-notes-app-vetkd.yml index a166f927a5..51566fc0f3 100644 --- a/.github/workflows/vetkeys-encrypted-notes-app-vetkd.yml +++ b/.github/workflows/vetkeys-encrypted-notes-app-vetkd.yml @@ -6,6 +6,7 @@ on: - master pull_request: paths: + - motoko/vetkeys/encrypted_notes_app_vetkd/** - rust/vetkeys/encrypted_notes_app_vetkd/** - .github/workflows/vetkeys-encrypted-notes-app-vetkd.yml @@ -14,23 +15,30 @@ 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 Encrypted Notes App Vetkd Rust - working-directory: rust/vetkeys/encrypted_notes_app_vetkd/rust - run: icp network start -d && icp deploy - motoko: + - name: Deploy and test + working-directory: motoko/vetkeys/encrypted_notes_app_vetkd + run: | + icp network start -d + icp deploy + bash test.sh + + 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 Encrypted Notes App Vetkd Motoko - working-directory: rust/vetkeys/encrypted_notes_app_vetkd/motoko - run: icp network start -d && icp deploy + - name: Deploy and test + working-directory: rust/vetkeys/encrypted_notes_app_vetkd + run: | + icp network start -d + icp deploy + bash test.sh diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/README.md b/motoko/vetkeys/encrypted_notes_app_vetkd/README.md new file mode 100644 index 0000000000..b05d984a12 --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/README.md @@ -0,0 +1,76 @@ +# Encrypted Notes: vetKD (Motoko) + +[View this sample's code on GitHub](https://github.com/dfinity/examples/tree/master/motoko/vetkeys/encrypted_notes_app_vetkd) + +Also available in: [Rust](../../../rust/vetkeys/encrypted_notes_app_vetkd) + +Encrypted notes is an example dapp for authoring and storing confidential information on the Internet Computer (ICP) in the form of short pieces of text. Users can create and access their notes via any number of automatically synchronized devices authenticated via Internet Identity (II). Notes are stored confidentially using vetKeys. The end-to-end encryption is performed by the dapp's frontend. + +In particular, the notes are encrypted with an AES key that is derived (directly in the browser) from a note-ID-specific vetKey obtained from the backend canister (in encrypted form, using an ephemeral transport key), which itself obtains it from the vetKD system API. This way, there is no need for any device management in the dapp, plus sharing of notes becomes possible. + +The vetKey used to encrypt and decrypt a note is note-ID-specific (and not, for example, principal-specific) to enable the sharing of notes between users. The derived AES keys are stored as non-extractable CryptoKeys in an IndexedDB in the browser for efficiency so that their respective vetKey only has to be fetched from the server once. + +## 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` + +### Install + +```bash +git clone https://github.com/dfinity/examples +cd examples/motoko/vetkeys/encrypted_notes_app_vetkd +``` + +### 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/`) + +A single Motoko canister that stores encrypted notes. It is deployed automatically with `icp deploy`. + +### Frontend (`frontend/`) + +A **Svelte** application providing a user-friendly interface for managing encrypted notes. Canister bindings are generated from `backend/backend.did` at build time by the `@icp-sdk/bindgen` Vite plugin. + +## Limitations + +This example dapp does not implement key rotation, which is strongly recommended in a production environment. + +## Troubleshooting + +If you run into issues, clearing all the application-specific IndexedDBs in the browser might help. For example in Chrome, go to Inspect → Application → Local Storage → Clear All, and then reload. + +## API level + +This example intentionally uses the **raw vetKD management canister API** (`encryptedSymmetricKeyForNote`, `symmetricKeyVerificationKeyForNote`) to demonstrate how vetKD works at the protocol level. + +For most applications, the higher-level [`EncryptedMaps`](https://github.com/dfinity/vetkeys/tree/main/frontend/ic_vetkeys/src/encrypted_maps) abstraction from `@icp-sdk/vetkeys` is the recommended approach — it handles key derivation, caching, and access control internally without requiring a custom crypto layer. See the **VetKD Password Manager** ([`../../../rust/vetkeys/password_manager`](../../../rust/vetkeys/password_manager)) and **Password Manager with Metadata** ([`../../../rust/vetkeys/password_manager_with_metadata`](../../../rust/vetkeys/password_manager_with_metadata)) examples for how `EncryptedMaps` is used in practice. + +## Additional resources + +- **[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/encrypted_notes_app_vetkd/motoko/backend/main.mo b/motoko/vetkeys/encrypted_notes_app_vetkd/backend/app.mo similarity index 94% rename from rust/vetkeys/encrypted_notes_app_vetkd/motoko/backend/main.mo rename to motoko/vetkeys/encrypted_notes_app_vetkd/backend/app.mo index 01abfd5c89..47c85787fc 100644 --- a/rust/vetkeys/encrypted_notes_app_vetkd/motoko/backend/main.mo +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/backend/app.mo @@ -16,7 +16,7 @@ import Hex "./utils/Hex"; // Declare a shared actor class // Bind the caller and the initializer -shared ({ caller = initializer }) persistent actor class (keyName: Text) { +shared ({ caller = initializer }) actor class (keyName: Text) { // Currently, a single canister smart contract is limited to 4 GB of heap size. // For the current limits see https://internetcomputer.org/docs/current/developer-docs/production/resource-limits. @@ -41,7 +41,7 @@ shared ({ caller = initializer }) persistent actor class (keyName: Text) { // Here we assume that the notes are encrypted end- // to-end by the front-end (at client side). public type EncryptedNote = { - encrypted_text : Text; + encryptedText : Text; id : Nat; owner : PrincipalName; // Principals with whom this note is shared. Does not include the owner. @@ -109,13 +109,13 @@ shared ({ caller = initializer }) persistent actor class (keyName: Text) { // [caller] is the anonymous identity // [caller] already has [MAX_NOTES_PER_USER] notes // This is the first note for [caller] and [MAX_USERS] is exceeded - public shared ({ caller }) func create_note() : async NoteId { + public shared ({ caller }) func createNote() : async NoteId { assert not Principal.isAnonymous(caller); let owner = Principal.toText(caller); let newNote : EncryptedNote = { id = nextNoteId; - encrypted_text = ""; + encryptedText = ""; owner = owner; users = []; }; @@ -152,7 +152,7 @@ shared ({ caller = initializer }) persistent actor class (keyName: Text) { // Future of array of EncryptedNote // Traps: // [caller] is the anonymous identity - public shared ({ caller }) func get_notes() : async [EncryptedNote] { + public shared ({ caller }) func getNotes() : async [EncryptedNote] { assert not Principal.isAnonymous(caller); let user = Principal.toText(caller); @@ -175,7 +175,7 @@ shared ({ caller = initializer }) persistent actor class (keyName: Text) { List.toArray(buf); }; - // Replaces the encrypted text of note with ID [id] with [encrypted_text]. + // Replaces the encrypted text of note with ID [id] with [encryptedText]. // // Returns: // Future of unit @@ -183,16 +183,16 @@ shared ({ caller = initializer }) persistent actor class (keyName: Text) { // [caller] is the anonymous identity // note with ID [id] does not exist // [caller] is not the note's owner and not a user with whom the note is shared - // [encrypted_text] exceeds [MAX_NOTE_CHARS] - public shared ({ caller }) func update_note(id : NoteId, encrypted_text : Text) : async () { + // [encryptedText] exceeds [MAX_NOTE_CHARS] + public shared ({ caller }) func updateNote(id : NoteId, encryptedText : Text) : async () { assert not Principal.isAnonymous(caller); let caller_text = Principal.toText(caller); let (?note_to_update) = Map.get(notesById, Nat.compare, id) else Runtime.trap("note with id " # Nat.toText(id) # "not found"); if (not is_authorized(caller_text, note_to_update)) { Runtime.trap("unauthorized"); }; - assert note_to_update.encrypted_text.size() <= MAX_NOTE_CHARS; - ignore Map.insert(notesById, Nat.compare, id, { note_to_update with encrypted_text }); + assert note_to_update.encryptedText.size() <= MAX_NOTE_CHARS; + ignore Map.insert(notesById, Nat.compare, id, { note_to_update with encryptedText }); }; // Shares the note with ID [note_id] with the [user]. @@ -204,7 +204,7 @@ shared ({ caller = initializer }) persistent actor class (keyName: Text) { // [caller] is the anonymous identity // note with ID [id] does not exist // [caller] is not the note's owner - public shared ({ caller }) func add_user(note_id : NoteId, user : PrincipalName) : async () { + public shared ({ caller }) func addUser(note_id : NoteId, user : PrincipalName) : async () { assert not Principal.isAnonymous(caller); let caller_text = Principal.toText(caller); let (?note) = Map.get(notesById, Nat.compare, note_id) else Runtime.trap("note with id " # Nat.toText(note_id) # "not found"); @@ -239,7 +239,7 @@ shared ({ caller = initializer }) persistent actor class (keyName: Text) { // [caller] is the anonymous identity // note with ID [id] does not exist // [caller] is not the note's owner - public shared ({ caller }) func remove_user(note_id : NoteId, user : PrincipalName) : async () { + public shared ({ caller }) func removeUser(note_id : NoteId, user : PrincipalName) : async () { assert not Principal.isAnonymous(caller); let caller_text = Principal.toText(caller); let (?note) = Map.get(notesById, Nat.compare, note_id) else Runtime.trap("note with id " # Nat.toText(note_id) # "not found"); @@ -270,7 +270,7 @@ shared ({ caller = initializer }) persistent actor class (keyName: Text) { // [caller] is the anonymous identity // note with ID [id] does not exist // [caller] is not the note's owner - public shared ({ caller }) func delete_note(note_id : NoteId) : async () { + public shared ({ caller }) func deleteNote(note_id : NoteId) : async () { assert not Principal.isAnonymous(caller); let caller_text = Principal.toText(caller); let (?note_to_delete) = Map.get(notesById, Nat.compare, note_id) else Runtime.trap("note with id " # Nat.toText(note_id) # "not found"); @@ -322,7 +322,7 @@ shared ({ caller = initializer }) persistent actor class (keyName: Text) { transient let management_canister : VETKD_API = actor ("aaaaa-aa"); - public shared func symmetric_key_verification_key_for_note() : async Text { + public shared func symmetricKeyVerificationKeyForNote() : async Text { let { public_key } = await management_canister.vetkd_public_key({ canister_id = null; context = Text.encodeUtf8("note_symmetric_key"); @@ -331,7 +331,7 @@ shared ({ caller = initializer }) persistent actor class (keyName: Text) { Hex.encode(Blob.toArray(public_key)); }; - public shared ({ caller }) func encrypted_symmetric_key_for_note(note_id : NoteId, transport_public_key : Blob) : async Text { + public shared ({ caller }) func encryptedSymmetricKeyForNote(note_id : NoteId, transport_public_key : Blob) : async Text { let caller_text = Principal.toText(caller); let (?note) = Map.get(notesById, Nat.compare, note_id) else Runtime.trap("note with id " # Nat.toText(note_id) # "not found"); if (not is_authorized(caller_text, note)) { diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/backend/backend.did b/motoko/vetkeys/encrypted_notes_app_vetkd/backend/backend.did new file mode 100644 index 0000000000..8c355cf4b4 --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/backend/backend.did @@ -0,0 +1,23 @@ +type _anon_class_19_1 = + service { + addUser: (note_id: NoteId, user: PrincipalName) -> (); + createNote: () -> (NoteId); + deleteNote: (note_id: NoteId) -> (); + encryptedSymmetricKeyForNote: (note_id: NoteId, transport_public_key: + blob) -> (text); + getNotes: () -> (vec EncryptedNote); + removeUser: (note_id: NoteId, user: PrincipalName) -> (); + symmetricKeyVerificationKeyForNote: () -> (text); + updateNote: (id: NoteId, encryptedText: text) -> (); + whoami: () -> (text); + }; +type PrincipalName = text; +type NoteId = nat; +type EncryptedNote = + record { + encryptedText: text; + id: nat; + owner: PrincipalName; + users: vec PrincipalName; + }; +service : (keyName: text) -> _anon_class_19_1 diff --git a/rust/vetkeys/encrypted_notes_app_vetkd/motoko/backend/utils/Hex.mo b/motoko/vetkeys/encrypted_notes_app_vetkd/backend/utils/Hex.mo similarity index 100% rename from rust/vetkeys/encrypted_notes_app_vetkd/motoko/backend/utils/Hex.mo rename to motoko/vetkeys/encrypted_notes_app_vetkd/backend/utils/Hex.mo diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/.gitignore b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/.gitignore new file mode 100644 index 0000000000..061e2e66e1 --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +src/bindings/ diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/.npmrc b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/.npmrc new file mode 100644 index 0000000000..521a9f7c07 --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/.npmrc @@ -0,0 +1 @@ +legacy-peer-deps=true diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/index.html b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/index.html new file mode 100644 index 0000000000..a494e91eac --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + Encrypted Notes + + + + + + diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/package.json b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/package.json new file mode 100644 index 0000000000..a47e8cdc67 --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/package.json @@ -0,0 +1,37 @@ +{ + "name": "frontend", + "private": true, + "type": "module", + "scripts": { + "prebuild": "npm i --include=dev", + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-check --tsconfig ./tsconfig.json" + }, + "devDependencies": { + "@icp-sdk/bindgen": "~0.2.2", + "@sveltejs/vite-plugin-svelte": "^3.0.2", + "@tsconfig/svelte": "^5.0.4", + "@types/node": "^24.0.10", + "autoprefixer": "^10.4.2", + "postcss": "^8.4.31", + "svelte": "^4.2.19", + "svelte-check": "^3.8.6", + "tailwindcss": "^3.0.17", + "tslib": "^2.8.1", + "typescript": "~5.7.2", + "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", + "daisyui": "^1.25.4", + "idb-keyval": "6.2.1", + "isomorphic-dompurify": "^2.25.0", + "svelte-icons": "^2.1.0", + "svelte-spa-router": "^4.0.1", + "typewriter-editor": "^0.6.45" + } +} diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/postcss.config.mjs b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/postcss.config.mjs new file mode 100644 index 0000000000..8409575db5 --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/postcss.config.mjs @@ -0,0 +1,6 @@ +import tailwindcss from 'tailwindcss'; +import autoprefixer from 'autoprefixer'; + +export default { + plugins: [tailwindcss(), autoprefixer()], +}; diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/public/.ic-assets.json5 b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/public/.ic-assets.json5 new file mode 100644 index 0000000000..6d07243f3f --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/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';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/encrypted_notes_app_vetkd/frontend/public/favicon.png b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..7e6f5eb5a2f1f1c882d265cf479de25caa925645 GIT binary patch literal 3127 zcmV-749N3|P)i z7)}s4L53SJCkR}iVi00SFk;`MXX*#X*kkwKs@nFGS}c;=?XFjU|G$3t^5sjIVS2G+ zw)WGF83CpoGXhLGW(1gW%uV|X7>1P6VhCX=Ux)Lb!*DZ%@I3!{Gsf7d?gtIQ%nQiK z3%(LUSkBji;C5Rfgd6$VsF@H`Pk@xtY6t<>FNR-pD}=C~$?)9pdm3XZ36N5PNWYjb z$xd$yNQR9N!dfj-Vd@BwQo^FIIWPPmT&sZyQ$v81(sCBV=PGy{0wltEjB%~h157*t zvbe_!{=I_783x!0t1-r#-d{Y?ae$Q4N_Nd^Ui^@y(%)Gjou6y<3^XJdu{rmUf-Me?)zZ>9OR&6U5H*cK; z$gUlB{g0O4gN0sLSO|Of?hU(l?;h(jA3uH!Z{EBKuV23ouU@^Y6#%v+QG;>e*E}%?wlu-NT4DG zs)z)7WbLr)vGAu(ohrKc^em@OpO&f~6_>E61n_e0_V3@{U3^O;j{`^mNCJUj_>;7v zsMs6Hu3g7+@v+lSo;=yTYFqq}jZmQ-BK8K{C4kqi_i*jBaQE(Au0607V-zKeT;EPg zX(`vrn=L+e74+-Tqeok@_`tDa$G9I|$nTU5H*2V8@y()n*zqM?J1G!-1aX;CfDC9B zTnJ#j_%*n8Qb1)re*Bno7g0RG{Eb;IK14irJYJp$5Z6ac9~b_P?+5t~95~SRG$g?1 znFJ7p$xV&GZ18m~79TGRdfsc-BcX$9yXTR*n)mPD@1~O(_?cT$ZvFPucRmGlq&se0 zKrcUf^k}4hM*biEJOWKzz!qQe;CB_ZtSOO9Owg#lZAc=s65^rb{fZe(TYu_rk!wKkEf}RIt=#Om( zR8mN`DM<^xj~59euMMspBolVN zAPTr8sSDI104orIAdmL$uOXn*6hga1G+0WD0E?UtabxC#VC~vf3|10|phW;yQ3CY8 z2CM=)ErF;xq-YJ5G|um}>*1#E+O_Mu|Nr#qQ&G1P-NMq@f?@*XUcSbV?tX=)ilM-Q zBZP|!Bpv0V;#ojKcpc7$=eqO;#Uy~#?^kNI{vSZfLx&DEt~LTmaKWXcx=joubklI<*Aw z>LtMaQ7DR<1I2LkWvwyu#Rwn~;ezT}_g(@5l3h?W%-a86Y-t#O1PubP+z<%?V5D(U zy57A6{h+{?kOZp7&WKZR+=sznMJ}+Dnpo=C_0%R_x_t~J5T?E_{+))l5v1%52>)d-`iiZyx|5!%M2Fb2dU zW3~MwwpEH9Rhue+k$UIOoo($Ds!NbOyMR36fRHu;*15(YcA7siIZk#%JWz>P!qX1?IUojG&nKR>^gArBt2 zit(ETyZ=@V&7mv_Fi4bABcnwP+jzQuHcfU&BrAV91u-rFvEi7y-KnWsvHH=d2 zgAk(GKm_S8RcTJ>2N3~&Hbwp{Z3NF_Xeh}g4Eke)V&dY{W(3&b1j9t4yK_aYJisZZ{1rcU5- z;eD>K;ndPq&B-8yA_S0F!4ThA&{1{x)H<#?k9a#6Pc6L?V^s0``ynL&D;p(!Nmx`Y zFkHex{4p!Ggm^@DlehW}iHHVi}~u=$&N? z(NEBLQ#UxxAkdW>X9LnqUr#t4Lu0=9L8&o>JsqTtT5|%gb3QA~hr0pED71+iFFr)dZ=Q=E6ng{NE{Z~0)C?deO#?Aj zSDQ$z#TeC2T^|=}6GBo-&$;E{HL3!q3Z-szuf)O=G#zDjin4SSP%o%6+2IT#sLjQa ziyxFFz~LMjWY+_a5H!U6%a<=b7QVP^ z*90a62;bVq{?@)P6^DWd^Yilq4|YTV2Nw!Yu;a1lPI-sxR)rf@Fe5DhDP7FH zZZ%4S*1C30P;|O+jB!1;m|rXT90Sm5*RBbQN`PKu+hDD*S^yE(CdtSfg=z>u$cIj> z4JGP);? zU+kXbOTp~vd$xO6WfohkXBC(<%+*|yK?*2!9<=3VjkFV4eY zFc>~ySTGn2hBbmsFElT^qJB`wuyGDFwE|i2d4S2&`>$zZz$~_Qu=;x!4+b_`;K}K+ zY*9_tgsWSygyRkPjYiGga`=Cb!eB5MJ|GyuXfPPogq=_S{Gi3Tt1T4!8K7|F!lL-r z`TD^CaFKdB&S49sSVvbdl30|YG2Ftp8`jvCM}O+UU@#aqI7ToU42D&OtcqK%sLhC7 z2LnKK6i>bV*3zUuw!MQAocqkRP7qkrLN>DgbCwlWiJ}`iv@DV^tXUilafYmmHQ}dj z_lQTU0e8#8KbeHVU@&Z8j9@ev46DYDV++h`)aFEYFT)@qYvYB+$mx4N<`)Te_TKBL zJA5kq#k&!aNF;$6LzYDnh<2|XZg1~}tykdfk8Z`ekAE2NG_ODbIvh*;PT!?BDdx@l zt`k;8w7NfdKsH26ipJ%w6z#$){GNx+q6?a+39&W=bj703SRK);JNb7%dw5DGE~{Rn zu4E_AZ@*~luB~{!J|_g>aPJsJ^p0EPm=E~HeOsUOeQ5CZW5ce!$(j%Xkqhx?#bX)m zXcCM)-?;lnyJ0XG3>yd=27|#cK!)CV*WLE*A4Fo2cdd#;kyVlQZElU|h(+S`J=^`4 z?z(1#gXhjd7Jl5FPo5c}u z!X5LcPI*{ zG8)B=l=Dt>-jB{FF)Z8!($|2?N@q*Bq^>c##}mlEKPKGajB9(sO%@DW<{71-=C8^r z;ihupKNy8S5~Zad`CP|PKAla(WosC5`I?QcoA;kL7}E1F@A)w$?7k?BiY!+?(#xJ2 zN73uc{LN!NJx6#eQylYGGK&%^CS0Vbb81-FkLr#1TTV6&*o;XG&yYoV@ahmtBM&-aIdlNnGuvH@4ZrCh|3q1sWGf9rxX{}VLr0U2}5>WrQ#vI-cqkw zrrwjJmH$&w_`JN4$a;uKYC{;}`_`$eT1ajaB=laVm2(FDJl1)yi$K?GA*=(`y3~VS zTb4u%VP2^56>0k=488f1RNtogO?syNq?Jb>VO!dUej16i_v|3@pIhF$p+fJ7mvKI? zjnJt>=2DKY&;UHcEg&oeSo5av&n`){Fq`5aaJ9ZopEfa=eX!lsp< zyn9z2Lpl>0X~=umbNPb@{%!P+c09)KxZyFr|At5TeZsY_f0Tdr`bYTZuX|(;6`?Nb zoll)v*m?X_p5Yds`}`mBv-e)xb#LBx-SYnOv3sv;p(1>GxcOfD_kVrm zj^E5L%-q5+%!=*oEu7CdI*mbU^pTs^@av-YStga!wsJm;7WB*8Lcw$u4G&a*_X&pl z_svnxw~)^q%C{#D2A9W0V93wU!v*8Z%ZCMI*GnpHUsyN8@8Leb(+Ir9<$=?a+aYVWH0fgzJG*OyejqvkWnFqSp zo{hpbmFq&eZf>_-4ixv1XrCeef}TeD7Ybcde(6mh{;Hzz@;Um@w*;s#gyn`Q@tla# zDa$C~3U?OiU-T+%gyoZvU)c;{+Q$<7Q@TBbWsvUY2;#4;7UHz&<~_xCD&hKvQ?5^6 z>3fvlvit+R<@hAhts-=9uJYT9{5KI>8_7{#lT~aUCHdI*|8p-@FEnORS`{17tZ188 zE}z{lY9Sg&teQQSVQFms>1~Jc_5V18>(9P|72_9SjeY2e|7)Wn*4TXKz3<%gxGT^%hXA-jml>>n7Q@xCqO#2YncN z>#C}F3CFVTZ62$W$5}A+)}?A&t=6To&d$plmJ^?>*QCF+wm%CpIJhb2${HXfvK?3J zz!t*vyI6hN!fjK2Q#xdO?xVLZFH#1)gkweD(|#RD(EHM+oir6XrJPH%JhT))bRm}D zrwzTY7xj898(J=IWF*+zm8_D+# z{iJu<#mf6Dg|IBn?H3ls&j`!nf$JU#?iZHDF9M}ZSRMC9O4LvA(^BGUWyGagpd4lY zfRdh0-!VdM06nZNl%A8(eiLhtk6x?P0SuN-%6>np<%kL#Q_`YZeFqi(gm`b{j!hZ3%C6>Ufc2;(#q&w`MsB`h`1GOx#!2eFsF8U zX43%OCBc^vFswbY9;JHG#m2=){OlR@@s#}{-LLOw7~;CrM`%Ute{bmczG&+qyUuQE z#;1-2J&0m;oQ!WG;hcKyL7-Z7*P|W3lu>nAGfOazD*H*5vT8yl6Q+-JPAFlV6hCU0 z#hz%(6#plbct#OF*u6@4A9^?mt_%#J*;Za(hv5NTTjC+UZwJ+JBG*FvLC{9F81v98 zsAi&lA9~%yY3Yz;nbfI3Q+J1p;xNQz&Cl`cBF+P9Xk?`|2~?WQGw8~%pIu7)vl;%{ z%D#h=2(j3t<#WO%bhsiKEgu7hL?if;mT3H+tv60x`NYeUSOqLwUO)U0@BaT7PD@Z& zS7PmP5$6ZJzYLcK{q^23s?kE+2Kdm2KG2RRX>w5=r_V85S~$vn);mCL*mi`&Qhd$$ zvOd)5xcm^!YWtH=+Y_~{FvRf;>V0PD?OK07<6y95aj&xP*ZVFs7wxNJ&_l8qV#*&9^)Ui7J(*s9+f7{kG5B$mP&9}B}O7ugb@&1vo zjqbShTStoi?`a(Z$We>o-_V+AVV$6{yi|9}S;16ajbQyrs`n^e9`vAlNot>nqQb#N zTvyTi*hYC6s>&9)^M&*5zKc2NRrDl8#!@0)4JEExA}I~BVU^ZH)e1=u*-^q)5nLS<%TNb# z9#J?FRBe>UM>Pg%$?(zJ-Qz%SCZfy-2|bTC2E!oBnzVFCu_n{)YJ{aq%u{|(qIlX= zON%NNU91kP@t&q4x}-ayKn#UqY;6pRG#1)4gY$=fcj1@2zB2tU*&aEVRXab!rWXdD z4vWj@D2FYDVlevd4^5;rR4T!6cMKzmMq1mNcmKgvF!tC(9h4@JS^gk8XUD+;i$p(v3^j!B$xZ5ceqFo%xaL- z&FMoQdS3Ld%9RC(-6&s|N>S}Zu55YmvC@KD!~n4tsA+MKT~{`dpLSo_r)QDX{^_>; zNl;zwAYMC?(#4{4ReNM+5#Q%o^nPT$U-41BEJ__H$DGpd-SVW}=h0Q1hip`l<{-U6 zV2B%MKPO(*f~ZcYX)F6)w56L-Es$#kW#%|~$Fr>`>NurA=Yy;4k78|6E1|3b({zp+ z#s%mvHcZdY*NA&Hpz`BJZVIbkNvT%*@eF^e6*;Q`$fV8#uWMPPv5Qu;q)zuZntyYx z>tH}EUegteu6peD=YEDiKk-aRT7Ry%;T2V+_4LR%P+~+@&NI3H5!>ni$Ct)}!Q{41XSa+v&7-No}KvqV1EX zgxQPU@!lS)LFH&Ha^ofAOS*44oM*ktfRAcrQR+Z-oUbEK+Ng#)c7)9O{VyFuSXf!~ zTj#4lZQuwWy#-DEGEhy)Yl-~YB^at#TxF1!gDK@$(Xl*vILiK7zW{VS`CJ)#*Zh)D zWi{0oMi;e9Y8)FE${+RPrvF}Lmz)#*Zli()ykP~Wx+0NB;~1lT>XxooUtqq=8`nHI z5m*Ub_(G}e|M#EZkALudb_4?wjql9d#B7W2iI;f{r6T=)4OinE*k81P95ie3L9*6( zi0`TDq#hrGuQM-2e8hQ10)WPovOYibo9ftRwC`ucr?!7hB-hQ&=$i6)8|B)Nq}3bN z^O^a|e!uu;h|{>LX`qQa6{Y8K+AB3|ZNY zr&9FcEF`$1qCw42Y6x{3pSgr<>yDwBQNk=@<&r5rsv*ULbm~C$Hr5{15=a%DEL-I0 z{#CK4GciZSLvO=VNjzM_P#IP8hD2lkJKBoQ`KVYKUw#3zL$DU*g5Aqw7^!>g?C}om zJLF^iQLSj}3DqZaeXeZpjA}Wp`Vmr9R7=6s?=H2EP<5>5D8e*S{Vtyi)dMU#ud@`| z6ng9C7ozy4qBzyIpZpcApCxUqT;6RK>xYNle5Cu7_pGkrxYkB2VvuDq(I}=0#yv!DV&XbW)C~8S0??wH?i{O{|;^?IRqwFoo*K zu_Dq*5j6-gS_aBvx;OQCJLsJ-LVk*lBbBHPPYbI@CuP43W!}>^;x*O?hTe%kgopg) zG;(o$wtAyvF^tmO?si1g`wIFx>0y4F!DvA3!Opn!K57} zO@2z)HsXh)D-_x}=Vx=mHnDQ-jf*%#J!HqtGS1KEoI8Y-m7=t?dZ>UohM(;$^G$ zA}&v-FyH~-mtEvjyoQ6cCZk+phsHB@XHIjg*|6(#|IGes{L}uIv_#df*u#z;yhmT~ zjDAavNMcd_w}0Hi*#^CT>qXCR57Ng5x(M@*;ya0A_)1!eieQ

zT4!T4l(=`W*G2Q>gY~OnF5j*hTzr5M_H@cBZv# z%XQ12O4hwoe7f&hfBBQWQwQX63kHM1Fkl|P)*au(akw7+AL-@w84*u}5y#pbwaF3R z^^@XwT?oa-($a*`g%15*7KlQv!S4R(E_z5yWpj&nEsII<2+`Qi6pa!KJ`XUNCiX7O z+~inRZJPH9S4zg4{4)G)aHkNApB9GArQI*?-(FPE8w>`+C5;h`27_T$`1WtPb3>dv z%d@39mIWhqZG;nH0@}`t- zM6oO;vE%p|*McLz@Fc?;aY38!cHlGs$k%eOzVyih{nsUV6F}!8o4SOX$Ez;sP}SLb>!&S@@%~5A2WBYi@mi~C$RaxtNpKf zD8SCA7e*1-hec!z1I36%gTb(|sk*F=!C)|?a*yx&4Q`K!Zt2->*EFP6(TaqkA{MEu zT8hP}&$s`JHX63***4!@R4fuN-MM|1F9jnamVu2iq;6;<77Yf&M#oqf4F3>zFH7!3x)n(+dAkkwIdHF$7fNC>rh5I}5Ela|G30q&?LI-KOfc_#or zQnxoxZS`H3S14H7K4hXTdjDee&5o#IehBrVe7E~R*Jr?^9Lf>UVmU_aiU z@AJpAGpBR2cel5*Gq(0)Lhj!+ z+Y1Sbe{cVQKdmwUMqFHQP4e;Y_3W840V)m-PNHOYy|S?CFP}l=B_bp#+@!lpanv&C z1nNWE=;y6}@VFFxt2j@eULe?H9c|}CG_EdM`YuUq8<1H%2w>Z!HSxi5yJ`vxu0SVy zkuFP1ODij@de!quS2Fk3@y`lW!UM<&Id0a3=Y)1ZY5zq=TnYoWl)H7V5QNZ2kd0T7K{}A5kL_=a<vMI9yb2_Fqb&(NsV&@lQd4BIBR(g2| z8d-pm#QB3>3tah`0Hd}OEw0A=^}>2}UvrOYtt9TE`7@_~t3UOwOs@RJB^;yS)xg8$ z%|@iLVm2X$1TzieY*GDHJ_ipIQEw!3R0?AZPb6f|5xigad<1}%k`p`}hULEc@4-Vb zRVjG5xqZ?6q_0n~pSVbmfW2Eoz`m{LQ1sZE4Wv|P&j9u%9grP~z;psg0uZ^~lV>}g zd|sheWrt1JOq7k3=7%n(-xbZ0AfI+_^Lh@xNyDDu1_KLN>j%yDQDEPWgWr?>^`{uw zq;Mh%p@)K;sMJwLaOL9n_X#@h;~nc>WXZlRdb+pQdQR*RZY4m&$LVEvA+$!TS<_mwd~d7IYlbw! zV_rO2DiZZSA42V)8e8O2mbxcZBx{}G#OJ7H4Dl1?vS&z)9wVT^%M0DC5x`g%!ltY( z|NS_?rGv;GW_(IaQ>1xS-4)kO-kU`Yud+$^IJTJQgpk;;+Ds$U&@(k}Mr;kv9Ecje z$O%*3j~`c`vh$CvmJIRwJX%e8G18E^xSUxgq3kI2XkPh30>^Wr1d|(n8zG5ne=JN^ z4SRzUQ~F9Y@+2q&GI|FR$%iNl8$F5Ni*+?M>Guj|h$5pgF}Z(E0d9=e?cg|+@qaCI4!^TCRKr|P zJv)GaA%sbHpy7I`d6RQty-(Akmo*JZJ-+t_75aQv-OsG%Cd;p;QbxyKVb(Oh&e9j= z!LhTqxK}9`-z4y&FzAsl@ucl)5seT?< z40d~WaqVuwK`g}N*A*!spV7tWRdJ|c&zzXsvOhg;RrFbPiZ_T`6v~=s$eESK{*=R~BT>6a z-)kMQ!8Jk0NADoxwKVSLUlKv-K~y7;`8KbwmM_~JOPSz=A*r9`!Gi$?MxP|uAkhq_ z6x7u5r~3g%qGK%+s2_?`cGN^ug0OLo-ngdQ@+L-Ukt+o*?}rw;qzA@lG^;EQ{%}+i zdw0*29M9)cW5?kvZoz3WBg(}4Vcl(QC>Bdf91J#A2d}!O3^U4a2hcN&B0ET$CXAp^ zchmR*+`Znt+wnG*FB|!C4WiD9Y<*Sno><=$3*#8q&iE}=^c?2DP^V}QlxKCt1nv>F z_|7xpHOcMl|-#3&L;uFS!ip1I0^Ip=P&oe3+1)C90NThMz$RR5aU_m7wM zzOPyH$4n8aPpM(VXr=o@8<65V}a`EB)rZL!O(Yxjv`jOUDp1b;Jq1w?e` z7y_)uOMcMkzsYA!d9^1`UZBZdT@{Qsa`994cfcrWZN;zz)2<`Mn8(D$?}Ams$w+yN zpc3zUKai2GERQLIHxN*f|Fn*=7o}eQ#H6;*QT`cYF(Q^QF7}R|Je9NAs&^Y6n2bs%xb~CAefi9g=SU zRuq_gWc~JN2zX6U)^BFQPQqFbY_1t_QwK2k#6=c8v0s}GDDEK;gGl-YVIwIS^iku;Xc85`l zy7AC_bn>DCb+T8Gm_u=}LL|trT zBP?QE%6vo5*Kv&8`t42ljyR@lwR6RBoK`RFaF*R{$8@_YLXzTh4VW1YsC^z?8Xk3P zAnYI9x}Uzr_-GMvBC{0f<<=FfVzc3|z5>jPn7qH%LaDWko6vo33!A}MHJxX;!HN00 zgwTQ4Bvkr~79$Fgs;hjKHS6q|Ph7+po?D}-C*&KuveEi)b0>Y-5>M}SX?U~Wk%XDZ z&WPfMZ2DL{P3m-$8toQH&4f+E)9O<`$g(=E__}g85)g(3P1K=$Ji+dBc7Yy- zlVSXB7RguR0;$cmm;|!7jlI#dmL&}H|4yG&G`W7W$u3Cy91Jk?xF2Ipt3^&4>cx>3 zCjiUmoqCV7ar1~nbhwTL;K{5pl*M|rwn9JCmhD}td|VeF>Lt1M>O1qu4E<}Ie{yb{ zrz>-JH_;2Gie<)|ZU!t*cv3w`$iQJ|#Ii-5VFD;0!T??R+lsoKa19ZyOeJM>%d zp0=yQQ1gxNq5h9!&upQu`-#-Io5MQ88p?ka}8{fgvlX?)LE&k}=gFqBTS#m|*S z&h+mq`I|VjZCqMEqqAZ?j!4Ss3M-DWG=}I6BP&(_FWG>KZ*m5|VzCr5-fXszMHc0Q z-^#~vn>m1}NOkmKP?gL!{#mA+1f1mZ?9086@M(=%vXTNwV#I`3HqMp#G=HkWrNp1M z6I}xMFT5)?y%6rTm~1|rjDKIr7sJgF)@lvV?~jVEAaja#u*a9C z4HJM7^JbKWjETHpdgDwtR@Z=?nP25xXn1BmX0g`UZL&TC&(|vbp>}jCdQl9H52IeS z1?XvE;bvVdrZd4#fvAGRi&rF!3-}OvIhGNUvWD{*^WdFdAP4x0$`{O!yw@CoPq5Ka ze6fgAKgAGr7iqDUR`bIw$a_VszYC5BM35={cK8N?jTnn2p}Jq1B65X^e4*))p17jS zI*2=%)DuB%{ig4|=_uR^4;{1G(MBuT8EtaopYiR^bZQSYE>y*tlFKjn>HdOG1`v2~ zj9j5}oa-A3c6yU>%v)GYO0mx4I$|y}LNa~5)D)UxQh8ciRmtiqP0yK1nVwMo)=1?l z9(wLry(T9|SyW^InmuMt1WW$=JG&x2L$)_3P;t2=tnLw%=WKL~h~#Wf(^$WaGI zA&O0gUlX}IQ#AKEmIqVg*?-n78C0g^9;SNgN>jrAcwL7U*@Kh3<+U%gB}MDp^r=mN z5-3lgc`CxX3N*W&N&w_1qUoixQk}RveU&TOe26xLYv3Q)H|%b3c?1KpOgYwgWMGip zyg1Q?H)%TW3bp7L(KL~HYuU(*A4%!H^OT#iOQ6YHW@GlkaI9HvWQ6r?mb^Dmj3`=G4A7wv;UI=KPvQt40V# zV`_-O2edv%Zfy=V$_3tXd@*BQO;A!KaSGjGc3 z)AI>4^l2{^VVhf3cQ@ES%Vwo|RWxyg|E~GuGEFfxDrup&AzjR^no2BDJ{$*4V)Uf< z8LZzcpRsrPAIO{tDNt>gJ@wz#r%!V4hTM3QMHl=egIko3+tiUefO^SIMI;hmbMUMr zti+&PQ>?;i*_uned^7mCl=e%TTIt|AH8J5Fm7wz2JgZT)WVyXN8<8mn)qCF8`)|1< zhg9eGyZ;$YPQpoocKiIs@Dl7VRMguKPoiEsy@Toa3pDXJq+p13|f!28LA zb^3B(4Pe44WOV{Hz`3RnA?##QcBOh$h{1Hn!g{BJ`z1buOe=(Erb4bV@5gl-t57b` zT->d}akgj;m2aHdD8CeK8eH-2t|aI}EKSB5AVb&vfKf8i-SgUmQnTQ=@Z*52DQsUp z%CG?*dc=%vZyBOaR7R}Ov>$OOn&HPbCrml-YaB_q&-pn$Oa%@kqc-|?M%3PBt(4(y zMy#Mh5;s@c-S;a)k(Q*78roHVSGYqbc5`+}?sc9M^ln7DS?t`4uZB&oCX6UdeiPVD z+l@OX_pt#z`c&8p_I$8H>^*GG9BhBV`{q;C%ms{f9(6F;jK(i0CRbpKv(>oxY-!NG zQOv|RTE^rj$^oVIP&n3)D}7ab0=J1a$F%QsgMYg-&w?y@A2`4zwg`C?{nG|SWpnMP z8%JyvFM^@bp09u?9u@Fe*U040b zqKqrvQim%=1y!P&m*m%{-vm^!W-(wzdkjoPDyr9G>(MxhD#vYDp1n|Z%MxsHw3F8n zjOBfY27$3azO&X-e2-W;766%4k^)tXim`%9mSuO#QI|XV>buC!eE&T${|)jkY`V}z zceai0rSq>Qxund=@XeFy2(<%|%yr?U!wMC_3R$SEOI$r$K};tlVQh$-o-0k0@}_Mg z_NzVM&AK?TO_zm&N=AaCxyQ{02epH6y} z5Xr7Hq6NaGl9pF~aGr{$1;^e$=vz7TjJ9q8F*q4Ib#V@$j?ML;v*ciUa_^5IcO(AY z&XC8CDu$DDHlsnA?zxjb(DZ==fBp7=emUj53~qItV4a@g>Hx4xXj4cd%f}c#L5Q#u z3+4c4b`S64=(ti}PKkpzA}J;B%Mr^@(NoS$gZ7IP6}p3Fpc5@5Q4q{(8?{W1F!h4f z9(zqp|AFrr%_C>6Mwi@Igrhjj8T@o2P&-X$B=)(0JsD8n$s`wWeausjy*uM|;2V9s z+C}e*?&y@D{Krli*A7O~MQd|1SQ)FVv;GqS(WH)={Cp#0*Ql;7c0Kjbih744tJ>0M z%H~)U8f_dTF!Jf3#X?6TA|?w>Wp^}HD}?Pk2qxIpCT!EA@h8xq)rQ~1qM$MKe7kv* zpzGiGB^tT{m4$g=szR7^a z;Z3~4Fsz3k(cF>JK@O~O^?i!1xQHn-5F5zU2E#djREtOD+KP>Zd#nv&@6p}i8CcD` z1i9Cx7FPUpI#}C@CCm+U3C@Ij2sD}nYuv^7;V#j`duw#-IUDAM2S7M4u^OOX;LQF8bu#3m>Ay^Dh`S$sbH&PJpEC??MlgY3yX5AuaQCP16??!NV z!aw_dSWe?T+D>EIwNv-c@Dq$`$)KUX;zUDKIkxjn3dPsKGa+0cv1hzoej7offQs8( zUx8Nnahej-!$Vi0G8!}89$?$^g8K2SoBRZIbYEkk)K9FLPyKeOjcYM#4BI#BQ z<9xApl4+eW)u5f=$%w4U2N|8&B`+9gn9-GFW!;1yFE^KgH+ujxCE_if4H@C`(SvuV zRmo0@tHL*(F^ca!{(BuzmpGld8ww8eT{@6}0O6MITuOI|;JM|nL#QUrY{^FKLXIX1 zlO@{6ogjCuz2AJxq^y=hQ+)Rt7Dc*<)!n|3n_Sa=DPO9jW}pn{JWg{Qfn5vIjeJ8h zvNg1-z>lD`wt{Othx7%^hOF1#O1B;Z^ZO@68A3Ntd0bY87{LM86ZX=^tq)2Z z8dsHj%L|6zlE-fXt=vxxuqtT9^KyVETIXsm7T#tn2%xkY4-@a7=;K^vS!xpBja2k_ z2$QRNIIO|*N*k)le2guYvG5?#MW^huw_@bViXFP?pP|6*nW$d;dR zrxh>?Zz>dCMojk`kegh_@@$N{(i0~Y4Qhaa&9o4?LbBKx$EE8JUX*n_XVQX$RTR}G(adIKC)v3bvGAx=ThI;Lt# zIBWTo|K*o)Dd@tWwns>DVq1L<9e))asD6~g`WP|%ngF_yEucK*Z-x%S_L_z-fnAX> zy*i@McWCbtXU2Xo#*UwthB-O0yhtmGaa2BAoiWHn)t#jwB*l-=fOAmej_3Sl>k&e+ zk&-aJ7hg&%0ZW`RdD-yy$^X#;<@03`J^ffw;E!*`VlQ8YLxe588sypeHs}h;nE4B9 zZ4fUI-%}Ls$$Zv?p>-`c@Y8xd!b0(<(+Yl7e_%~ z!Z@rmhxnT_H8RVWA6UCEI*HbKK#7Z+WF4O-r4;u({>Mzv|37B3|Bw(iwZ#P?457LG zwt=ul@1F`yj+BZ05;Qz0p?fj?l#GAY05n%>C`(TSadyy#;tnm5-sPiE<1VXXQMK=G$vTgYP~(QIs|z6) z1UAL1RIDfIeS0V#m8#Ex@#kq~Pc(W0pnx)jj;u=dLUpc1uDy=K{-Zo8aglE{H{Vi@ z&y4kg@Qf>mNwi+VV@`#Tk*@H@1ZlOGNfj7hIz5Yu*W z)79tnKT7n9`U!(Ijede&>~el@y`w;Vihdm(HH9G^{3GG^iu38R$3Xf%J%9o!MG|p> z_Tgtv-j8Ge$^^_FDDBaR{Xb*|_&;Pum3K*ZEqTt0Eg>NotuT%w-~GJlH2$p~*^g^D z<{}U?xCe?qL5I(Yx)yc1#PnIpe*$gCgB8QGYR2R}Vgo$X+uOUnkMV!Hu!J2~ztr)E zrq%Co?@B=d(jRT=QzAKJIt>SQaQvS5x(-{(F_W2gSRUR4b;gem)B`SAlfcA_R8JPR k5>+ca(Es=%(OZCZ_*S7t&NA}<0giOF*Z=?k literal 0 HcmV?d00001 diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/public/img/ic-badge-powered-by-crypto_transparent-dark-text.png b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/public/img/ic-badge-powered-by-crypto_transparent-dark-text.png new file mode 100644 index 0000000000000000000000000000000000000000..90029c464bf6080f78d19b48c0aea12073448600 GIT binary patch literal 12190 zcmX9^WmFu^5(N_6-5nMW?z;FA++BjZyL)hVmmtC2J!o)uf@E=bf8={Vrl+Q>`c79( zpPoMV+z3T^DP#l!1PBNSWRSGDG6V$V$cOz64(6ksR}x$NxWPL}YdJ$e;86d&AVJDh z7ayIF&dO4v5Y>}JhaV50&4F@22#C5k#1|uI2nc3&kT_7)1M)P-H=bzNb=hF*43bPd z5;5Wd?KcIORyio$$-x051%tOwzfIZkO3I^Fe8{BOuqII^yEu`b_AiEMLOX8+N5TvF zx*El(6Mc8^>30keHVQV4qw1H<*;ut|Y1UZz$PgiW5jFs%^pMgY*np0lN$*$IId=YM z&u2%nP1N+Y$AiPev%^E~^V?$*Nc=BPsA&?7tmGo=QCUJ9KsZQc8Ewe?ExE9$ga=g2 zX|nou9@;nB#H0B3c(l>BNw>TB&C(*-L52xC1rw-4S3*WiHH>lHGOgB5Mot8cG$DsE z)uWvL>yUBrY}`^_vOe|<>v%l)!}^4bS~h1C+bB57JK28Kx1`U-XZ}Qag2FjlH_h

B-^J2-2d7W%HLj zUiX<9yDsx)vrKUhKF_k$!^)3IfMJTP;k}o6p(L{L8tE_zyya5j;Ap?Xoo}lTsTliI}TC9lF7wmg93&n1remsmY?Oa;iTbP`TDZF z6m2{0+S%ZtTLM8>X_4$|X-VNct}&{5EuP%e@#x8ppKO?Oy)31a*7T>i#80zT{275%$nEIjf1eUL4e<$9uF7i($@*~t$Tb>eUcjCNFU$xJ z3}Z$aWKIc>2G2|G=QLcJLOEs2Echkh1JTINq5G;^pp7zEpY z8DMk15xrw+dH?gIf978iMgJEP7EuQ^`{|!yq?>$AZyo;;i?oZDv7)0T@^4&pFb8I4 zL%@GF2eZ*O3*jki(Dh>e@}L_@ku#)Jh*pcGT)M zC2`T>GA2l)(~aG(|BN(n18u3j#dOj8?jUZ3}3FRH=( zyvGa;GB8ZZ)K=~RWg`oe=QM92m1gjoL*Ou*fXZ!{tV%WGQu2h~1VIH%*HkPoQu5yY)vrR^ zc)?(Gs`Tn5nf1%Sk^_X1Tpv&%$Rq-k?oq@l-SoO)zUQ=OBD1YS&~~PhDrf5rIAa<( zXa|29aDjLApONL;tcCShzut>NOizXggRrc)7kIrA6>c2;n$j`cnL}x=Xd>6DU36xj zd|5aL*?;P!s@;X{A%iCRO<$s@b&*8Sd7bB66O!QkKBy)3eH{uZZw}mogv5@w{CUC# z;RlaQCM^Wvu90Ot6;T~~Bb3FlCG+Ok+BqJB0frmXxmVjfz#mPoA?3v41En? z*1!mGa4K7@WC!Aku(O^w)m;8=qE5z4cu*|!7QZ2T41PD)z-^Kj`c||%adZHQV^P;e z;J5q(f=e7~`#9~FvENX|HF|8G>2?@WDlAjQ523NAdas^g7kCr9-|kizCsy>#>2UF))G8{QUD_!BF>&>UEgVB zjMwUn*G7Gq(+g)R%*D*eVA)P~a5nwCw@d1TBlnbXJV!1Acoi#_;3ha

kGJ?@15| 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 literal 0 HcmV?d00001 diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/App.svelte b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/App.svelte new file mode 100644 index 0000000000..b3b5d6eec9 --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/App.svelte @@ -0,0 +1,14 @@ + + +{#if $auth.state === 'initialized'} + +{:else} + +{/if} + + diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/app.css b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/app.css new file mode 100644 index 0000000000..b5c61c9567 --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/app.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/Disclaimer.svelte b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/Disclaimer.svelte new file mode 100644 index 0000000000..16ce216fca --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/Disclaimer.svelte @@ -0,0 +1,26 @@ + + +{#if !isDismissed} +

+

+ +

+ + +
+{/if} diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/DisclaimerCopy.svelte b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/DisclaimerCopy.svelte new file mode 100644 index 0000000000..2bf836e940 --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/DisclaimerCopy.svelte @@ -0,0 +1,3 @@ +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/encrypted_notes_app_vetkd/frontend/src/components/EditNote.svelte b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/EditNote.svelte new file mode 100644 index 0000000000..1f7a041d9a --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/EditNote.svelte @@ -0,0 +1,162 @@ + + +{#if editedNote} +
+ Edit note + +
+
+ {#if $notesStore.state === 'loaded'} + + addTag(e.detail)} + on:remove={(e) => removeTag(e.detail)} + disabled={updating || deleting} + /> + +
+ + {:else if $notesStore.state === 'loading'} + Loading notes... + {/if} +
+{:else} +
+ Edit note +
+
+ {#if $notesStore.state === 'loading'} + + Loading note... + {:else if $notesStore.state === 'loaded'} +
Could not find note.
+ {/if} +
+{/if} diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/Header.svelte b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/Header.svelte new file mode 100644 index 0000000000..922f3f8c0b --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/Header.svelte @@ -0,0 +1,25 @@ + diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/Hero.svelte b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/Hero.svelte new file mode 100644 index 0000000000..19aa54e29c --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/Hero.svelte @@ -0,0 +1,71 @@ + + +
+
+
+

+ Encrypted Notes +

+

+ Your private notes on the Internet Computer. +

+

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

+ + {#if auth.state === 'initializing-auth' || auth.state === 'initializing-crypto'} +
+ + Initializing... +
+ {:else if auth.state === 'synchronizing'} +
+ + Synchronizing... Please keep the app open on a device that's already added. +
+ {:else if auth.state === 'anonymous'} + + {:else if auth.state === 'error'} +
An error occurred.
+ {/if} + +
+ +
+
+
+
+ + Powered by the Internet Computer +
+
diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/LayoutAuthenticated.svelte b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/LayoutAuthenticated.svelte new file mode 100644 index 0000000000..acf0e17c7f --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/LayoutAuthenticated.svelte @@ -0,0 +1,18 @@ + + + + + diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/NewNote.svelte b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/NewNote.svelte new file mode 100644 index 0000000000..31e71eda2b --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/NewNote.svelte @@ -0,0 +1,93 @@ + + + + +
+ New note +
+ +
+ + addTag(e.detail)} + on:remove={(e) => removeTag(e.detail)} + disabled={creating} + /> + +
+ + diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/Note.svelte b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/Note.svelte new file mode 100644 index 0000000000..bec2d02a83 --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/Note.svelte @@ -0,0 +1,46 @@ + + + +
+

+ {#if note.title} + {note.title} + {:else} + Unnamed note + {/if} +

+ {contentSummary} + {#if note.tags.length > 0} +
+ {#each note.tags as tag} + + {/each} +
+ {/if} +
+
diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/NoteEditor.svelte b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/NoteEditor.svelte new file mode 100644 index 0000000000..120bbe78f3 --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/NoteEditor.svelte @@ -0,0 +1,68 @@ + + + +
+ + + + +
+
+ +
+ + diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/Notes.svelte b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/Notes.svelte new file mode 100644 index 0000000000..7b142660a7 --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/Notes.svelte @@ -0,0 +1,75 @@ + + +
+ Your notes + + {#if $notesStore.state === 'loaded' && $notesStore.list.length > 0} + New Note + {/if} + +
+
+ {#if $notesStore.state === 'loading'} + + Loading notes... + {:else if $notesStore.state === 'loaded'} + {#if $notesStore.list.length > 0} +
+ +
+ +
+ {#each filteredNotes as note (note.id)} + (filter = e.detail)} /> + {/each} +
+ {:else} +
You don't have any notes.
+ + {/if} + {:else if $notesStore.state === 'error'} +
Could not load notes.
+ {/if} +
diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/Notifications.svelte b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/Notifications.svelte new file mode 100644 index 0000000000..85528b1d15 --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/Notifications.svelte @@ -0,0 +1,23 @@ + + +
+ {#each $notifications as n (n.id)} +
+

{n.message}

+
+ {/each} +
diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/SharingEditor.svelte b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/SharingEditor.svelte new file mode 100644 index 0000000000..086758babb --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/SharingEditor.svelte @@ -0,0 +1,128 @@ + + +
+

Users

+ {#if ownedByMe} +

+ Add users by their principal to allow them editing the note. +

+ {:else} +

+ This note is shared with you. It is owned + by {editedNote.owner}. +

+

Users with whom the owner shared the note:

+ {/if} +
+ {#each editedNote.users as sharing} + + {/each} + + +
+
diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/SidebarLayout.svelte b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/SidebarLayout.svelte new file mode 100644 index 0000000000..d6d27f2bfd --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/SidebarLayout.svelte @@ -0,0 +1,75 @@ + + +
+ +
+
+ +
+ +
+
+
+
diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/Spinner.svelte b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/Spinner.svelte new file mode 100644 index 0000000000..fd7812cce0 --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/Spinner.svelte @@ -0,0 +1,3 @@ + diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/TagEditor.svelte b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/TagEditor.svelte new file mode 100644 index 0000000000..0b4df389e8 --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/components/TagEditor.svelte @@ -0,0 +1,74 @@ + + +
+ {#each tags as tag} + + {/each} + + +
diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/global.d.ts b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/global.d.ts new file mode 100644 index 0000000000..0e7296906a --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/global.d.ts @@ -0,0 +1 @@ +/// \ No newline at end of file diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/lib/actor.ts b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/lib/actor.ts new file mode 100644 index 0000000000..d953820035 --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/lib/actor.ts @@ -0,0 +1,19 @@ +import { HttpAgent } from "@icp-sdk/core/agent"; +import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env"; +import { createActor as createEncryptedNotesActor, type Backend } from "../bindings/backend"; + +export type BackendActor = Backend; + +const canisterEnv = safeGetCanisterEnv<{ + "PUBLIC_CANISTER_ID:backend": string; +}>(); + +export async function createActor(options?: { identity?: any }): Promise { + const canisterId = canisterEnv?.["PUBLIC_CANISTER_ID:backend"]; + const agent = await HttpAgent.create({ + identity: options?.identity, + host: window.location.origin, + rootKey: canisterEnv?.IC_ROOT_KEY, + }); + return createEncryptedNotesActor(canisterId, { agent }); +} diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/lib/crypto.ts b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/lib/crypto.ts new file mode 100644 index 0000000000..ac8a0e785f --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/lib/crypto.ts @@ -0,0 +1,78 @@ +import type { BackendActor } from './actor'; +import { get, set } from 'idb-keyval'; +import * as vetkd from "@icp-sdk/vetkeys"; + +export class CryptoService { + private keyMaterialCache = new Map(); + + constructor(private actor: BackendActor) {} + + public async encryptWithNoteKey(note_id: bigint, owner: string, data: string): Promise { + const keyMaterial = await this.fetchNoteKeyMaterial(note_id, owner); + const associatedData = buildInput(note_id, owner); + const encrypted = await keyMaterial.encryptMessage(data, "note-key", associatedData); + return String.fromCharCode(...encrypted); + } + + public async decryptWithNoteKey(note_id: bigint, owner: string, data: string): Promise { + const keyMaterial = await this.fetchNoteKeyMaterial(note_id, owner); + const associatedData = buildInput(note_id, owner); + const ciphertext = Uint8Array.from([...data].map(ch => ch.charCodeAt(0))); + const decrypted = await keyMaterial.decryptMessage(ciphertext, "note-key", associatedData); + return String.fromCharCode(...decrypted); + } + + private async fetchNoteKeyMaterial(note_id: bigint, owner: string): Promise { + const cacheKey = `${note_id}_${owner}`; + + // 1. In-memory cache (fastest, session-scoped) + const memCached = this.keyMaterialCache.get(cacheKey); + if (memCached) return memCached; + + // 2. IndexedDB cache (persisted across sessions via getCryptoKey/fromCryptoKey) + const storedCryptoKey: CryptoKey | undefined = await get([note_id.toString(), owner]); + if (storedCryptoKey) { + const keyMaterial = await vetkd.DerivedKeyMaterial.fromCryptoKey(storedCryptoKey); + this.keyMaterialCache.set(cacheKey, keyMaterial); + return keyMaterial; + } + + // 3. Fetch from canister (first access) + const tsk = vetkd.TransportSecretKey.random(); + const ek_bytes_hex = await this.actor.encryptedSymmetricKeyForNote(note_id, tsk.publicKeyBytes()); + const encryptedVetKey = vetkd.EncryptedVetKey.deserialize(hex_decode(ek_bytes_hex)); + const pk_bytes_hex = await this.actor.symmetricKeyVerificationKeyForNote(); + const dpk = vetkd.DerivedPublicKey.deserialize(hex_decode(pk_bytes_hex)); + const input = buildInput(note_id, owner); + const vetKey = encryptedVetKey.decryptAndVerify(tsk, dpk, input); + const keyMaterial = await vetKey.asDerivedKeyMaterial(); + + // Store the underlying non-extractable CryptoKey in IndexedDB (same pattern as EncryptedMaps) + await set([note_id.toString(), owner], keyMaterial.getCryptoKey()); + this.keyMaterialCache.set(cacheKey, keyMaterial); + return keyMaterial; + } +} + +function buildInput(note_id: bigint, owner: string): Uint8Array { + const note_id_bytes = bigintTo128BitBigEndianUint8Array(note_id); + const owner_utf8 = new TextEncoder().encode(owner); + const input = new Uint8Array(note_id_bytes.length + owner_utf8.length); + input.set(note_id_bytes); + input.set(owner_utf8, note_id_bytes.length); + return input; +} + +const hex_decode = (hexString: string) => + Uint8Array.from(hexString.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16))); + +function bigintTo128BitBigEndianUint8Array(bn: bigint): Uint8Array { + var hex = BigInt(bn).toString(16); + while (hex.length < 32) { hex = '0' + hex; } + var len = hex.length / 2; + var u8 = new Uint8Array(len); + for (var i = 0, j = 0; i < len; i++, j += 2) { + u8[i] = parseInt(hex.slice(j, j + 2), 16); + } + return u8; +} diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/lib/enums.ts b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/lib/enums.ts new file mode 100644 index 0000000000..daf2b66906 --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/lib/enums.ts @@ -0,0 +1,8 @@ +export type KeysOfUnion = T extends T ? keyof T : never; + +export function enumIs( + p: EnumType, + key: KeysOfUnion +): p is T { + return (key as string) in p; +} diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/lib/note.ts b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/lib/note.ts new file mode 100644 index 0000000000..2d1fc21c97 --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/lib/note.ts @@ -0,0 +1,106 @@ +import type { EncryptedNote } from '../bindings/backend'; +import type { CryptoService } from './crypto'; +import type { Principal } from '@icp-sdk/core/principal'; + +export interface NoteModel { + id: bigint; + title: string; + content: string; + createdAt: number; + updatedAt: number; + tags: Array; + owner: string; + users: Array; +} + +type SerializableNoteModel = Omit; + +export function noteFromContent(content: string, tags: string[], self_principal: Principal): NoteModel { + const title = extractTitle(content); + const creationTime = Date.now(); + + return { + id: BigInt(0), + title, + content, + createdAt: creationTime, + updatedAt: creationTime, + tags, + owner: self_principal.toString(), + users: [""], + }; +} + +export async function serialize( + note: NoteModel, + cryptoService: CryptoService +): Promise { + const serializableNote: SerializableNoteModel = { + title: note.title, + content: note.content, + createdAt: note.createdAt, + updatedAt: note.updatedAt, + tags: note.tags, + }; + const encryptedNote = await cryptoService.encryptWithNoteKey( + note.id, + note.owner, + JSON.stringify(serializableNote) + ); + return { + id: note.id, + encryptedText: encryptedNote, + owner: note.owner, + users: note.users, + }; +} + +export async function deserialize( + enote: EncryptedNote, + cryptoService: CryptoService +): Promise { + const serializedNote = await cryptoService.decryptWithNoteKey(enote.id, enote.owner, enote.encryptedText); + const deserializedNote: SerializableNoteModel = JSON.parse(serializedNote); + return { + id: enote.id, + owner: enote.owner, + users: enote.users, + ...deserializedNote, + }; +} + +export function summarize(note: NoteModel, 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) { + const title = el.querySelector('h1'); + if (title) { + return title.innerText; + } + + const blockElements = el.querySelectorAll( + 'h1,h2,p,li' + ) as NodeListOf; + for (const el of blockElements) { + if (el.innerText?.trim().length > 0) { + return el.innerText.trim(); + } + } + return ''; +} + +export function extractTitle(html: string) { + const div = document.createElement('div'); + div.innerHTML = html; + return extractTitleFromDomEl(div); +} diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/lib/sleep.ts b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/lib/sleep.ts new file mode 100644 index 0000000000..0d7f188e17 --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/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/encrypted_notes_app_vetkd/frontend/src/main.ts b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/main.ts new file mode 100644 index 0000000000..75e6d2f702 --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/main.ts @@ -0,0 +1,10 @@ +import "./app.css"; +import App from "./App.svelte"; + +const init = async () => { + const app = new App({ + target: document.body, + }); +}; + +init(); \ No newline at end of file diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/store/auth.ts b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/store/auth.ts new file mode 100644 index 0000000000..88f138897d --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/store/auth.ts @@ -0,0 +1,132 @@ +import { get, writable } from "svelte/store"; +import { type BackendActor, createActor } from "../lib/actor"; +import { AuthClient, LocalStorage } from "@icp-sdk/auth/client"; +import type { Principal } from "@icp-sdk/core/principal"; +import { CryptoService } from "../lib/crypto"; +import { showError } from "./notifications"; +import { push } from "svelte-spa-router"; + +export type AuthState = + | { state: "initializing-auth" } + | { state: "anonymous"; actor: BackendActor; client: AuthClient } + | { state: "initializing-crypto"; actor: BackendActor; client: AuthClient; principal: Principal } + | { state: "synchronizing"; actor: BackendActor; client: AuthClient; principal: Principal } + | { + state: "initialized"; + actor: BackendActor; + client: AuthClient; + principal: Principal; + crypto: CryptoService; + } + | { state: "error"; error: string }; + +export const auth = writable({ state: "initializing-auth" }); + +async function initAuth() { + const isLocal = + 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: isLocal + ? "http://id.ai.localhost:8000/authorize" + : "https://id.ai/authorize", + ...(isLocal + ? { storage: new LocalStorage(), keyType: "Ed25519" as const } + : {}), + }); + if (client.isAuthenticated()) { + authenticate(client); + } else { + const actor = await createActor(); + auth.update(() => ({ + state: "anonymous", + actor, + client, + })); + } +} + +initAuth(); + +export async function login() { + const currentAuth = get(auth); + + if (currentAuth.state === "anonymous") { + await currentAuth.client.signIn(); + authenticate(currentAuth.client); + } +} + +export async function logout() { + const currentAuth = get(auth); + + if (currentAuth.state === "initialized") { + await currentAuth.client.signOut(); + const actor = await createActor(); + auth.update(() => ({ + state: "anonymous", + actor, + client: currentAuth.client, + })); + push("/"); + } +} + +export async function authenticate(client: AuthClient) { + handleSessionTimeout(); + + try { + const identity = await client.getIdentity(); + const principal = identity.getPrincipal(); + const actor = await createActor({ identity }); + + auth.update(() => ({ + state: "initializing-crypto", + actor, + client, + principal, + })); + + const cryptoService = new CryptoService(actor); + + auth.update(() => ({ + state: "initialized", + actor, + client, + principal, + crypto: cryptoService, + })); + } catch (e: any) { + auth.update(() => ({ + state: "error", + error: e.message || "An error occurred", + })); + } +} + +function handleSessionTimeout() { + setTimeout(() => { + try { + const delegation = JSON.parse( + window.localStorage.getItem("ic-delegation") ?? "null", + ) as { + delegations: Array<{ delegation: { expiration: string } }>; + } | null; + if (!delegation) return; + + const expirationTimeMs = + Number.parseInt(delegation.delegations[0].delegation.expiration, 16) / + 1000000; + + setTimeout(() => { + logout(); + }, expirationTimeMs - Date.now()); + } catch { + console.error("Could not handle delegation expiry."); + } + }); +} diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/store/draft.ts b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/store/draft.ts new file mode 100644 index 0000000000..7e89684d68 --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/store/draft.ts @@ -0,0 +1,31 @@ +import { writable } from 'svelte/store'; +import { auth } from './auth'; + +interface DraftModel { + content: string; + tags: string[]; +} + +let initialDraft: DraftModel = { + content: '', + tags: [], +}; + +try { + const savedDraft: DraftModel = JSON.parse(localStorage.getItem('draft')); + if ('content' in savedDraft && 'tags' in savedDraft) { + initialDraft = savedDraft; + } +} catch {} + +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/encrypted_notes_app_vetkd/frontend/src/store/notes.ts b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/store/notes.ts new file mode 100644 index 0000000000..7b33c024da --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/store/notes.ts @@ -0,0 +1,133 @@ +import { writable } from 'svelte/store'; +import type { BackendActor } from '../lib/actor'; +import type { EncryptedNote } from '../bindings/backend'; +import type { CryptoService } from '../lib/crypto'; +import { deserialize, serialize } from '../lib/note'; +import type { NoteModel } from '../lib/note'; +import { auth } from './auth'; +import { showError } from './notifications'; + +export const notesStore = writable< + | { + state: 'uninitialized'; + } + | { + state: 'loading'; + } + | { + state: 'loaded'; + list: NoteModel[]; + } + | { + state: 'error'; + } +>({ state: 'uninitialized' }); + +let notePollerHandle: ReturnType | null; + +async function decryptNotes( + notes: EncryptedNote[], + cryptoService: CryptoService +): Promise { + // When notes are initially created, they do not have (and cannot have) any + // (encrypted) content yet because the note ID, which is needed to retrieve + // the note-specific encryption key, is not known yet before the note is + // created because the note ID is a return value of the call to create a note. + // The (encrypted) note content is stored in the backend only by a second call + // to the backend that updates the note's conent directly after the note is + // created. This means that there is a short period of time where the note + // already exists but doesn't have any (encrypted) content yet. + // To avoid decryption errors for these notes, we skip deserializing (and thus + // decrypting) these notes here. + const notes_with_content = notes.filter((note) => note.encryptedText != ""); + + return await Promise.all( + notes_with_content.map((encryptedNote) => deserialize(encryptedNote, cryptoService)) + ); +} + +function updateNotes(notes: NoteModel[]) { + notesStore.set({ + state: 'loaded', + list: notes, + }); +} + +export async function refreshNotes( + actor: BackendActor, + cryptoService: CryptoService +) { + const encryptedNotes = await actor.getNotes(); + + const notes = await decryptNotes(encryptedNotes, cryptoService); + updateNotes(notes); +} + +export async function addNote( + note: NoteModel, + actor: BackendActor, + crypto: CryptoService +) { + const new_id: bigint = await actor.createNote(); + note.id = new_id; + const encryptedNote = (await serialize(note, crypto)).encryptedText; + await actor.updateNote(new_id, encryptedNote); +} +export async function updateNote( + note: NoteModel, + actor: BackendActor, + crypto: CryptoService +) { + const encryptedNote = await serialize(note, crypto); + await actor.updateNote(note.id, encryptedNote.encryptedText); +} + +export async function addUser( + id: bigint, + user: string, + actor: BackendActor, +) { + await actor.addUser(id, user); +} + +export async function removeUser( + id: bigint, + user: string, + actor: BackendActor, +) { + await actor.removeUser(id, user); +} + +auth.subscribe(async ($auth) => { + if ($auth.state === 'initialized') { + if (notePollerHandle !== null) { + clearInterval(notePollerHandle); + notePollerHandle = null; + } + + notesStore.set({ + state: 'loading', + }); + try { + await refreshNotes($auth.actor, $auth.crypto).catch((e) => + showError(e, 'Could not poll notes.') + ); + + notePollerHandle = setInterval(async () => { + await refreshNotes($auth.actor, $auth.crypto).catch((e) => + showError(e, 'Could not poll notes.') + ); + }, 3000); + } catch { + notesStore.set({ + state: 'error', + }); + } + } else if ($auth.state === 'anonymous' && notePollerHandle !== null) { + clearInterval(notePollerHandle); + notePollerHandle = null; + notesStore.set({ + state: 'uninitialized', + }); + } +}); diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/store/notifications.ts b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/src/store/notifications.ts new file mode 100644 index 0000000000..6b0aa0cad2 --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/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: any, message: string): never { + addNotification({ type: 'error', message }); + console.error(e); + console.error(e.stack); + throw e; +} diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/svelte.config.js b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/svelte.config.js new file mode 100644 index 0000000000..8abe4369b8 --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/svelte.config.js @@ -0,0 +1,5 @@ +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' + +export default { + preprocess: vitePreprocess(), +} diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/tailwind.config.mjs b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/tailwind.config.mjs new file mode 100644 index 0000000000..6d6be576b4 --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/tailwind.config.mjs @@ -0,0 +1,13 @@ +import daisyui from 'daisyui'; + +export default { + content: [ + './index.html', + './src/**/*.svelte', + './src/**/*.ts', + ], + theme: { + extend: {}, + }, + plugins: [daisyui], +}; diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/tsconfig.json b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/tsconfig.json new file mode 100644 index 0000000000..4365ffeb08 --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@tsconfig/svelte/tsconfig.json", + "include": ["src/**/*", "index.html"], + "exclude": ["node_modules/*", "public/*", "src/declarations/**/*"] +} diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/vite.config.ts b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/vite.config.ts new file mode 100644 index 0000000000..f3f735d144 --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/frontend/vite.config.ts @@ -0,0 +1,53 @@ +import { defineConfig } from "vite"; +import { svelte } from "@sveltejs/vite-plugin-svelte"; +import { execSync } from "child_process"; +import { icpBindgen } from "@icp-sdk/bindgen/plugins/vite"; + +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(), + icpBindgen({ + didFile: "../backend/backend.did", + outDir: "./src/bindings", + }), + ], + build: { + sourcemap: true, + rollupOptions: { + output: { + inlineDynamicImports: true, + }, + }, + }, + server: command === "serve" ? getDevServerConfig() : undefined, +})); diff --git a/rust/vetkeys/encrypted_notes_app_vetkd/motoko/icp.yaml b/motoko/vetkeys/encrypted_notes_app_vetkd/icp.yaml similarity index 50% rename from rust/vetkeys/encrypted_notes_app_vetkd/motoko/icp.yaml rename to motoko/vetkeys/encrypted_notes_app_vetkd/icp.yaml index ef936cb217..ba5979ac3f 100644 --- a/rust/vetkeys/encrypted_notes_app_vetkd/motoko/icp.yaml +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/icp.yaml @@ -1,20 +1,19 @@ canisters: - - name: encrypted_notes + - 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: - - npm install --include=dev --legacy-peer-deps --prefix ../frontend - - npm run build --prefix ../frontend - - rm -rf dist && mv ../frontend/dist dist && cp dist/index.html dist/404.html + - npm install --prefix frontend + - npm run build --prefix frontend networks: - name: local diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/mops.toml b/motoko/vetkeys/encrypted_notes_app_vetkd/mops.toml new file mode 100644 index 0000000000..12ae91caaa --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/mops.toml @@ -0,0 +1,15 @@ +[toolchain] +moc = "1.11.0" + +[dependencies] +core = "2.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/encrypted_notes_app_vetkd/package.json b/motoko/vetkeys/encrypted_notes_app_vetkd/package.json new file mode 100644 index 0000000000..caa53e71ff --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/package.json @@ -0,0 +1,13 @@ +{ + "name": "encrypted_notes_app_vetkd", + "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/motoko/vetkeys/encrypted_notes_app_vetkd/security-checklist.md b/motoko/vetkeys/encrypted_notes_app_vetkd/security-checklist.md new file mode 100644 index 0000000000..7f2c2a9a58 --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/security-checklist.md @@ -0,0 +1,165 @@ +This document lists typical steps of a security review needed for production-ready IC dapps. We indicate whether the two backend implementations of Encrypted Notes comply with the corresponding requirements (marked as Done), do not yet comply (Future), or whether a particular requirement is not applicable to this backend (Not applicable). + +While this list might help creating better IC dapps, keep in mind that the list is potentially incomplete. In particular, each real-world dapp may have a different set of security requirements that depend on its target domain and intended use case. + +# 1. Authentication + +### 1.1. Make sure any action that only a specific user should be able to do requires authentication +* Motoko: Done +* Rust: Done + +### 1.2. Disallow the anonymous principal in authenticated calls +* Motoko: Done +* Rust: Done + +# 2. Consensus + +Avoid using uncertified queries in public canister APIs. Instead, either use certified update methods or design an eventual certification approach for performance-critical dapps. +* Motoko: Done (no query methods) +* Rust: Done (no query methods) + +# 3. Input Validation + +Each public API method should sanitize their arguments and gracefully handle exceptional situations. +* Motoko: Done +* Rust: Done + +# 4. Frontend security + +### 4.1. Frontend input validation +* Motoko: Done +* Rust: Done + +### 4.2. Avoid using deterministic encryption. +For example, the initialization vector for AES-GCM encryption should be unique for each message (or chosen at random). +* Motoko: Done +* Rust: Done + +### 4.3. Do not load untrusted assets like CSS or fonts +* Motoko: Done +* Rust: Done + +### 4.4. Avoid logging sensitive data like private keys +When generating the private key using `crypto.subtle.generateKey`, set `extractable=false`. Consider offloading the secret keys to a YubiKey or YubyHSM so that the secret keys never end up in the browser. +* Motoko: Done +* Rust: Done + +### 4.5. Avoid reusing the same public/private key pair for every identity in the same browser +* Motoko: Future +* Rust: Future + +### 4.6. Set reasonable session timeouts +For example, a security-sensitive dapp like Encrypted Notes should set `maxTimeToLive` for Internet Identity delegation to 30 min rather than 24 h. +* Motoko: Future +* Rust: Future + +### 4.7. Regularly refresh symmetric encryption keys +* Motoko: Future +* Rust: Future + +# 5. Asset Certification + +### 5.1. Use HTTP asset certification and avoid serving your dapp through raw.ic0.app +* Motoko: Done +* Rust: Done + +# 6. Canister Storage + +### 6.1. Use thread_local! with Cell/RefCell for state variables and put all your globals in one basket +* Motoko: Not applicable +* Rust: Done + +### 6.2. Limit the amount of data that can be stored in a canister per user +* Motoko: Done +* Rust: Done + +### 6.3. Consider using stable memory, version it, test it +* Motoko: Done (except versioning) +* Rust: Done (except versioning) + +### 6.4. Don’t store sensitive data on canisters (unless it is encrypted) +* Motoko: Done +* Rust: Done + +### 6.5. Create backups +* Motoko: Future +* Rust: Future + +# 7. Inter-Canister Calls and Rollbacks + +### 5.1. Don’t panic after await and don’t lock shared resources across await boundaries +* Motoko: Done (we don't use await) +* Rust: Done (we don't use await) + +### 5.2. Be aware that state may change during inter-canister calls +* Motoko: Done (we have no inter-canister calls) +* Rust: Done (we have no inter-canister calls) + +### 5.3. Only make inter-canister calls to trustworthy canisters +* Motoko: Done (we have no inter-canister calls) +* Rust: Done (we have no inter-canister calls) + +### 5.4. Make sure there are no loops in call graphs +* Motoko: Done +* Rust: Done + +# 8. Canister Upgrades + +### 8.1. Don’t panic/trap during upgrades: +* Motoko: Done, assuming that [`Iter.toArray`](https://github.com/dfinity/motoko-base/blob/master/src/Iter.mo) and [`Map.fromIter`](https://github.com/dfinity/motoko-base/blob/master/src/HashMap.mo) do not trap. +* Rust: Done, assuming that [`borrow_mut`](https://doc.rust-lang.org/std/borrow/trait.BorrowMut.html#tymethod.borrow_mut), [`std::mem::take`](https://doc.rust-lang.org/stable/std/mem/fn.take.html), and [`ic_cdk::storage::stable_save`](https://docs.rs/ic-cdk/latest/ic_cdk/storage/fn.stable_save.html) do not panic. + +### 8.2. Ensure upgradeability +If the canister storage becomes too big, the canister will no longer be upgradable because `pre_upgrade` will time out or the canister will run out of cycles. The recommended remedy is to use stable memory directly rather than serializing data upon upgrade. +* Motoko: Future +* Rust: Future + +# 9. Rust-specific issues + +### 9.1. Don’t use unsafe Rust code: +* Rust: Done + +### 9.2. Avoid integer overflows: +* Rust: Done + +# 10. Miscellaneous + +### 10.1. For expensive calls, consider using captchas or proof of work +* Motoko: Future +* Rust: Future + +### 10.2. Test your canister code even in presence of System API calls +* Motoko: Future +* Rust: Future + +### 10.3. Make canister builds reproducible +* Motoko: Done (via Docker) +* Rust: Done (via Docker) + +### 10.4. Expose metrics from your canister +* Motoko: Future +* Rust: Future + +### 10.5. Don’t rely on time being strictly monotonic +* Motoko: Done +* Rust: Done + +### 10.6. Protect against draining the cycles balance +* Motoko: Future +* Rust: Future + + +# 11. Efficiency considerations + +### 11.1. `submit_ciphertexts` +* Adding submit_ciphertexts is currently O(C*D) where `C = ciphertexts.size()` and `D = store.device_list.size()` + +# 12. Usability + +### 12.1. Confirm user's intention before executing potentially irreversible actions like device removal +* Motoko: Future +* Rust: Future + +### 12.2. Prevent account lockout scenarios +* Motoko: Future +* Rust: Future diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/test.sh b/motoko/vetkeys/encrypted_notes_app_vetkd/test.sh new file mode 100755 index 0000000000..b9a57059cc --- /dev/null +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/test.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -e + +echo "--- Testing encrypted_notes: whoami ---" +result=$(icp canister call backend whoami '()') && \ + echo "$result" && \ + echo "$result" | grep -q '"' && \ + echo "PASS" || (echo "FAIL" && exit 1) + +echo "--- Testing encrypted_notes: getNotes (empty) ---" +result=$(icp canister call backend getNotes '()') && \ + echo "$result" && \ + echo "$result" | grep -q 'vec' && \ + echo "PASS" || (echo "FAIL" && exit 1) diff --git a/rust/vetkeys/encrypted_notes_app_vetkd/rust/Cargo.toml b/rust/vetkeys/encrypted_notes_app_vetkd/Cargo.toml similarity index 100% rename from rust/vetkeys/encrypted_notes_app_vetkd/rust/Cargo.toml rename to rust/vetkeys/encrypted_notes_app_vetkd/Cargo.toml diff --git a/rust/vetkeys/encrypted_notes_app_vetkd/README.md b/rust/vetkeys/encrypted_notes_app_vetkd/README.md index 0effa17d9d..f38cc16e67 100644 --- a/rust/vetkeys/encrypted_notes_app_vetkd/README.md +++ b/rust/vetkeys/encrypted_notes_app_vetkd/README.md @@ -1,11 +1,8 @@ -# Encrypted notes: vetKD +# Encrypted Notes: vetKD (Rust) - +[View this sample's code on GitHub](https://github.com/dfinity/examples/tree/master/rust/vetkeys/encrypted_notes_app_vetkd) + +Also available in: [Motoko](../../../motoko/vetkeys/encrypted_notes_app_vetkd) Encrypted notes is an example dapp for authoring and storing confidential information on the Internet Computer (ICP) in the form of short pieces of text. Users can create and access their notes via any number of automatically synchronized devices authenticated via Internet Identity (II). Notes are stored confidentially using vetKeys. The end-to-end encryption is performed by the dapp's frontend. @@ -13,55 +10,51 @@ In particular, the notes are encrypted with an AES key that is derived (directly The vetKey used to encrypt and decrypt a note is note-ID-specific (and not, for example, principal-specific) to enable the sharing of notes between users. The derived AES keys are stored as non-extractable CryptoKeys in an IndexedDB in the browser for efficiency so that their respective vetKey only has to be fetched from the server once. -## Prerequisites +## Build and deploy from the command line -- [x] Install the [ICP CLI](https://cli.internetcomputer.org). -- [x] Install [npm](https://www.npmjs.com/package/npm). +### Prerequisites -## Folder Structure +- 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 provides both a **Rust** and a **Motoko** backend, sharing a common `frontend/`: +### Install -``` -encrypted_notes_app_vetkd/ -├── 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/encrypted_notes_app_vetkd ``` -## 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: +Open the frontend URL printed by `icp deploy`. + +To run the frontend in development mode with hot reloading (after `icp deploy`): + ```bash -cd rust -icp network start -d && icp deploy +npm run dev ``` -## Running the Frontend in Development Mode +When done, stop the local network to free up the port for other projects: -After deploying, run from the `frontend` folder: ```bash -cd frontend -npm run dev:motoko # if you deployed the Motoko backend -# or -npm run dev:rust # if you deployed the Rust backend +icp network stop ``` -## Example Components +## Example components -### Backend +### Backend (`backend/`) -The backend consists of a canister that stores encrypted notes. It is automatically deployed with `icp deploy`. +A single Rust canister that stores encrypted notes. It is deployed automatically with `icp deploy`. -### Frontend +### Frontend (`frontend/`) -The frontend is a **Svelte** application providing a user-friendly interface for managing encrypted notes. +A **Svelte** application providing a user-friendly interface for managing encrypted notes. Canister bindings are generated from `backend/backend.did` at build time by the `@icp-sdk/bindgen` Vite plugin. ## Limitations @@ -71,12 +64,13 @@ This example dapp does not implement key rotation, which is strongly recommended If you run into issues, clearing all the application-specific IndexedDBs in the browser might help. For example in Chrome, go to Inspect → Application → Local Storage → Clear All, and then reload. -## API Level +## API level -This example intentionally uses the **raw VetKD management canister API** (`encrypted_symmetric_key_for_note`, `symmetric_key_verification_key_for_note`) to demonstrate how VetKD works at the protocol level. +This example intentionally uses the **raw vetKD management canister API** (`encrypted_symmetric_key_for_note`, `symmetric_key_verification_key_for_note`) to demonstrate how vetKD works at the protocol level. -For most applications, the higher-level [`EncryptedMaps`](https://github.com/dfinity/vetkeys/tree/main/frontend/ic_vetkeys/src/encrypted_maps) abstraction from `@icp-sdk/vetkeys` is the recommended approach — it handles key derivation, caching, and access control internally without requiring a custom crypto layer. See the [**VetKD Password Manager**](../password_manager/) and [**VetKD Password Manager with Metadata**](../password_manager_with_metadata/) examples for how `EncryptedMaps` is used in practice. +For most applications, the higher-level [`EncryptedMaps`](https://github.com/dfinity/vetkeys/tree/main/frontend/ic_vetkeys/src/encrypted_maps) abstraction from `@icp-sdk/vetkeys` is the recommended approach — it handles key derivation, caching, and access control internally without requiring a custom crypto layer. See the **VetKD Password Manager** ([`../password_manager`](../password_manager)) and **Password Manager with Metadata** ([`../password_manager_with_metadata`](../password_manager_with_metadata)) examples for how `EncryptedMaps` is used in practice. -## Additional Resources +## Additional resources -- **[What are VetKeys](https://docs.internetcomputer.org/concepts/vetkeys)** - For more information about VetKeys and VetKD. +- **[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/encrypted_notes_app_vetkd/rust/backend/Cargo.toml b/rust/vetkeys/encrypted_notes_app_vetkd/backend/Cargo.toml similarity index 100% rename from rust/vetkeys/encrypted_notes_app_vetkd/rust/backend/Cargo.toml rename to rust/vetkeys/encrypted_notes_app_vetkd/backend/Cargo.toml diff --git a/rust/vetkeys/encrypted_notes_app_vetkd/rust/backend/src/encrypted_notes_rust.did b/rust/vetkeys/encrypted_notes_app_vetkd/backend/backend.did similarity index 100% rename from rust/vetkeys/encrypted_notes_app_vetkd/rust/backend/src/encrypted_notes_rust.did rename to rust/vetkeys/encrypted_notes_app_vetkd/backend/backend.did diff --git a/rust/vetkeys/encrypted_notes_app_vetkd/rust/backend/src/lib.rs b/rust/vetkeys/encrypted_notes_app_vetkd/backend/src/lib.rs similarity index 100% rename from rust/vetkeys/encrypted_notes_app_vetkd/rust/backend/src/lib.rs rename to rust/vetkeys/encrypted_notes_app_vetkd/backend/src/lib.rs diff --git a/rust/vetkeys/encrypted_notes_app_vetkd/frontend/.gitignore b/rust/vetkeys/encrypted_notes_app_vetkd/frontend/.gitignore new file mode 100644 index 0000000000..061e2e66e1 --- /dev/null +++ b/rust/vetkeys/encrypted_notes_app_vetkd/frontend/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +src/bindings/ diff --git a/rust/vetkeys/encrypted_notes_app_vetkd/frontend/package.json b/rust/vetkeys/encrypted_notes_app_vetkd/frontend/package.json index 96ba0925a1..a47e8cdc67 100644 --- a/rust/vetkeys/encrypted_notes_app_vetkd/frontend/package.json +++ b/rust/vetkeys/encrypted_notes_app_vetkd/frontend/package.json @@ -1,39 +1,37 @@ { - "name": "encrypted-notes-frontend", - "private": true, - "version": "0.0.0", - "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": "npm run build:bindings && BACKEND=motoko vite", - "dev:rust": "npm run build:bindings && BACKEND=rust vite", - "build": "npm run build:bindings && vite build", - "build:bindings": "cd scripts && ./gen_bindings.sh", - "preview": "vite preview", - "check": "svelte-check --tsconfig ./tsconfig.json" - }, - "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.2", - "@tsconfig/svelte": "^5.0.4", - "@types/node": "^24.0.10", - "autoprefixer": "^10.4.2", - "postcss": "^8.4.31", - "svelte": "^4.2.19", - "svelte-check": "^3.8.6", - "tailwindcss": "^3.0.17", - "tslib": "^2.8.1", - "typescript": "~5.7.2", - "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", - "daisyui": "^1.25.4", - "idb-keyval": "6.2.1", - "isomorphic-dompurify": "^2.25.0", - "svelte-icons": "^2.1.0", - "svelte-spa-router": "^4.0.1", - "typewriter-editor": "^0.6.45" - } -} \ No newline at end of file + "name": "frontend", + "private": true, + "type": "module", + "scripts": { + "prebuild": "npm i --include=dev", + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-check --tsconfig ./tsconfig.json" + }, + "devDependencies": { + "@icp-sdk/bindgen": "~0.2.2", + "@sveltejs/vite-plugin-svelte": "^3.0.2", + "@tsconfig/svelte": "^5.0.4", + "@types/node": "^24.0.10", + "autoprefixer": "^10.4.2", + "postcss": "^8.4.31", + "svelte": "^4.2.19", + "svelte-check": "^3.8.6", + "tailwindcss": "^3.0.17", + "tslib": "^2.8.1", + "typescript": "~5.7.2", + "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", + "daisyui": "^1.25.4", + "idb-keyval": "6.2.1", + "isomorphic-dompurify": "^2.25.0", + "svelte-icons": "^2.1.0", + "svelte-spa-router": "^4.0.1", + "typewriter-editor": "^0.6.45" + } +} diff --git a/rust/vetkeys/encrypted_notes_app_vetkd/frontend/scripts/gen_bindings.sh b/rust/vetkeys/encrypted_notes_app_vetkd/frontend/scripts/gen_bindings.sh deleted file mode 100755 index 895b957abb..0000000000 --- a/rust/vetkeys/encrypted_notes_app_vetkd/frontend/scripts/gen_bindings.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash -set -eu - -# Resolve the script's physical location so we work correctly even when the -# icp CLI has symlinked `frontend/` into a backend subdirectory for the build. -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" -FRONTEND_DIR="$(dirname "$SCRIPT_DIR")" -EXAMPLE_ROOT="$(dirname "$FRONTEND_DIR")" - -# Bindings are always generated from the Rust backend since both backends -# expose the same Candid interface. - -rm -rf "$FRONTEND_DIR/src/declarations/encrypted_notes" -mkdir -p "$FRONTEND_DIR/src/declarations/encrypted_notes" -npx --yes @icp-sdk/bindgen \ - --did-file "$EXAMPLE_ROOT/rust/backend/src/encrypted_notes_rust.did" \ - --out-dir "$FRONTEND_DIR/src/declarations/encrypted_notes" \ - --declarations-flat --force diff --git a/rust/vetkeys/encrypted_notes_app_vetkd/frontend/src/lib/actor.ts b/rust/vetkeys/encrypted_notes_app_vetkd/frontend/src/lib/actor.ts index b29294c5c6..d953820035 100644 --- a/rust/vetkeys/encrypted_notes_app_vetkd/frontend/src/lib/actor.ts +++ b/rust/vetkeys/encrypted_notes_app_vetkd/frontend/src/lib/actor.ts @@ -1,15 +1,15 @@ import { HttpAgent } from "@icp-sdk/core/agent"; import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env"; -import { createActor as createEncryptedNotesActor, type Backend } from "../declarations/encrypted_notes/encrypted_notes_rust"; +import { createActor as createEncryptedNotesActor, type Backend } from "../bindings/backend"; export type BackendActor = Backend; const canisterEnv = safeGetCanisterEnv<{ - "PUBLIC_CANISTER_ID:encrypted_notes": string; + "PUBLIC_CANISTER_ID:backend": string; }>(); export async function createActor(options?: { identity?: any }): Promise { - const canisterId = canisterEnv?.["PUBLIC_CANISTER_ID:encrypted_notes"]; + const canisterId = canisterEnv?.["PUBLIC_CANISTER_ID:backend"]; const agent = await HttpAgent.create({ identity: options?.identity, host: window.location.origin, diff --git a/rust/vetkeys/encrypted_notes_app_vetkd/frontend/src/lib/note.ts b/rust/vetkeys/encrypted_notes_app_vetkd/frontend/src/lib/note.ts index 10d6598c43..9cb2b21058 100644 --- a/rust/vetkeys/encrypted_notes_app_vetkd/frontend/src/lib/note.ts +++ b/rust/vetkeys/encrypted_notes_app_vetkd/frontend/src/lib/note.ts @@ -1,4 +1,4 @@ -import type { EncryptedNote } from '../declarations/encrypted_notes/backend.did'; +import type { EncryptedNote } from '../bindings/backend'; import type { CryptoService } from './crypto'; import type { Principal } from '@icp-sdk/core/principal'; diff --git a/rust/vetkeys/encrypted_notes_app_vetkd/frontend/src/store/notes.ts b/rust/vetkeys/encrypted_notes_app_vetkd/frontend/src/store/notes.ts index cdf9dd871c..39ed61d2c6 100644 --- a/rust/vetkeys/encrypted_notes_app_vetkd/frontend/src/store/notes.ts +++ b/rust/vetkeys/encrypted_notes_app_vetkd/frontend/src/store/notes.ts @@ -1,6 +1,6 @@ import { writable } from 'svelte/store'; import type { BackendActor } from '../lib/actor'; -import type { EncryptedNote } from '../lib/backend'; +import type { EncryptedNote } from '../bindings/backend'; import type { CryptoService } from '../lib/crypto'; import { deserialize, serialize } from '../lib/note'; import type { NoteModel } from '../lib/note'; diff --git a/rust/vetkeys/encrypted_notes_app_vetkd/frontend/vite.config.ts b/rust/vetkeys/encrypted_notes_app_vetkd/frontend/vite.config.ts index 3d6221a968..f3f735d144 100644 --- a/rust/vetkeys/encrypted_notes_app_vetkd/frontend/vite.config.ts +++ b/rust/vetkeys/encrypted_notes_app_vetkd/frontend/vite.config.ts @@ -1,36 +1,46 @@ import { defineConfig } from "vite"; import { svelte } from "@sveltejs/vite-plugin-svelte"; import { execSync } from "child_process"; - -const environment = process.env.ICP_ENVIRONMENT || "local"; -const CANISTER_NAMES = ["encrypted_notes"]; +import { icpBindgen } from "@icp-sdk/bindgen/plugins/vite"; 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`."); - } - const networkStatus = JSON.parse( - execSync(`icp network status -e ${environment} --json --project-root-override ../${backend}`, { encoding: "utf-8" }), + 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" ); - 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 }) => ({ - plugins: [svelte()], + base: "./", + plugins: [ + svelte(), + icpBindgen({ + didFile: "../backend/backend.did", + outDir: "./src/bindings", + }), + ], build: { sourcemap: true, rollupOptions: { @@ -39,6 +49,5 @@ export default defineConfig(({ command }) => ({ }, }, }, - root: "./", - ...(command === "serve" ? { server: getDevServerConfig() } : {}), + server: command === "serve" ? getDevServerConfig() : undefined, })); diff --git a/rust/vetkeys/encrypted_notes_app_vetkd/rust/icp.yaml b/rust/vetkeys/encrypted_notes_app_vetkd/icp.yaml similarity index 53% rename from rust/vetkeys/encrypted_notes_app_vetkd/rust/icp.yaml rename to rust/vetkeys/encrypted_notes_app_vetkd/icp.yaml index bd77e42f5e..76218e16c2 100644 --- a/rust/vetkeys/encrypted_notes_app_vetkd/rust/icp.yaml +++ b/rust/vetkeys/encrypted_notes_app_vetkd/icp.yaml @@ -1,23 +1,22 @@ canisters: - - name: encrypted_notes + - name: backend recipe: type: "@dfinity/rust@v3.2.0" configuration: package: encrypted_notes_backend - candid: backend/src/encrypted_notes_rust.did + candid: backend/backend.did 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: - - npm install --include=dev --legacy-peer-deps --prefix ../frontend - - npm run build --prefix ../frontend - - rm -rf dist && mv ../frontend/dist dist + - npm install --prefix frontend + - npm run build --prefix frontend networks: - name: local diff --git a/rust/vetkeys/encrypted_notes_app_vetkd/motoko/frontend b/rust/vetkeys/encrypted_notes_app_vetkd/motoko/frontend deleted file mode 120000 index af288785f3..0000000000 --- a/rust/vetkeys/encrypted_notes_app_vetkd/motoko/frontend +++ /dev/null @@ -1 +0,0 @@ -../frontend \ No newline at end of file diff --git a/rust/vetkeys/encrypted_notes_app_vetkd/motoko/mops.toml b/rust/vetkeys/encrypted_notes_app_vetkd/motoko/mops.toml deleted file mode 100644 index 99f601ef11..0000000000 --- a/rust/vetkeys/encrypted_notes_app_vetkd/motoko/mops.toml +++ /dev/null @@ -1,8 +0,0 @@ -[toolchain] -moc = "1.9.0" - -[dependencies] -core = "2.5.0" - -[canisters.encrypted_notes] -main = "backend/main.mo" diff --git a/rust/vetkeys/encrypted_notes_app_vetkd/package.json b/rust/vetkeys/encrypted_notes_app_vetkd/package.json new file mode 100644 index 0000000000..caa53e71ff --- /dev/null +++ b/rust/vetkeys/encrypted_notes_app_vetkd/package.json @@ -0,0 +1,13 @@ +{ + "name": "encrypted_notes_app_vetkd", + "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/encrypted_notes_app_vetkd/rust/rust-toolchain.toml b/rust/vetkeys/encrypted_notes_app_vetkd/rust-toolchain.toml similarity index 100% rename from rust/vetkeys/encrypted_notes_app_vetkd/rust/rust-toolchain.toml rename to rust/vetkeys/encrypted_notes_app_vetkd/rust-toolchain.toml diff --git a/rust/vetkeys/encrypted_notes_app_vetkd/rust/frontend b/rust/vetkeys/encrypted_notes_app_vetkd/rust/frontend deleted file mode 120000 index af288785f3..0000000000 --- a/rust/vetkeys/encrypted_notes_app_vetkd/rust/frontend +++ /dev/null @@ -1 +0,0 @@ -../frontend \ No newline at end of file diff --git a/rust/vetkeys/encrypted_notes_app_vetkd/test.sh b/rust/vetkeys/encrypted_notes_app_vetkd/test.sh new file mode 100755 index 0000000000..cd7eeb24ac --- /dev/null +++ b/rust/vetkeys/encrypted_notes_app_vetkd/test.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -e + +echo "--- Testing encrypted_notes: whoami ---" +result=$(icp canister call backend whoami '()') && \ + echo "$result" && \ + echo "$result" | grep -q '"' && \ + echo "PASS" || (echo "FAIL" && exit 1) + +echo "--- Testing encrypted_notes: get_notes (empty) ---" +result=$(icp canister call backend get_notes '()') && \ + echo "$result" && \ + echo "$result" | grep -q 'vec' && \ + echo "PASS" || (echo "FAIL" && exit 1) From 8b38eea3fe5cebc1ec6b45cbe22d244e5463347b Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 16 Jul 2026 18:13:30 +0200 Subject: [PATCH 2/7] ci(vetkeys/encrypted_notes): drop test.sh, deploy-only CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The backend methods require a non-anonymous caller (e.g. get_notes asserts not isAnonymous), but the CI default identity is anonymous — so the added test.sh failed. This is why the original example shipped without a test.sh. Drop both test.sh files and run deploy-only in CI (matching the original workflow); the deploy itself verifies build + install. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../vetkeys-encrypted-notes-app-vetkd.yml | 6 ++---- motoko/vetkeys/encrypted_notes_app_vetkd/test.sh | 14 -------------- rust/vetkeys/encrypted_notes_app_vetkd/test.sh | 14 -------------- 3 files changed, 2 insertions(+), 32 deletions(-) delete mode 100755 motoko/vetkeys/encrypted_notes_app_vetkd/test.sh delete mode 100755 rust/vetkeys/encrypted_notes_app_vetkd/test.sh diff --git a/.github/workflows/vetkeys-encrypted-notes-app-vetkd.yml b/.github/workflows/vetkeys-encrypted-notes-app-vetkd.yml index 51566fc0f3..15035bc8fd 100644 --- a/.github/workflows/vetkeys-encrypted-notes-app-vetkd.yml +++ b/.github/workflows/vetkeys-encrypted-notes-app-vetkd.yml @@ -22,12 +22,11 @@ jobs: ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - name: Deploy and test + - name: Deploy working-directory: motoko/vetkeys/encrypted_notes_app_vetkd run: | icp network start -d icp deploy - bash test.sh rust: runs-on: ubuntu-24.04 @@ -36,9 +35,8 @@ jobs: ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - name: Deploy and test + - name: Deploy working-directory: rust/vetkeys/encrypted_notes_app_vetkd run: | icp network start -d icp deploy - bash test.sh diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/test.sh b/motoko/vetkeys/encrypted_notes_app_vetkd/test.sh deleted file mode 100755 index b9a57059cc..0000000000 --- a/motoko/vetkeys/encrypted_notes_app_vetkd/test.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -set -e - -echo "--- Testing encrypted_notes: whoami ---" -result=$(icp canister call backend whoami '()') && \ - echo "$result" && \ - echo "$result" | grep -q '"' && \ - echo "PASS" || (echo "FAIL" && exit 1) - -echo "--- Testing encrypted_notes: getNotes (empty) ---" -result=$(icp canister call backend getNotes '()') && \ - echo "$result" && \ - echo "$result" | grep -q 'vec' && \ - echo "PASS" || (echo "FAIL" && exit 1) diff --git a/rust/vetkeys/encrypted_notes_app_vetkd/test.sh b/rust/vetkeys/encrypted_notes_app_vetkd/test.sh deleted file mode 100755 index cd7eeb24ac..0000000000 --- a/rust/vetkeys/encrypted_notes_app_vetkd/test.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -set -e - -echo "--- Testing encrypted_notes: whoami ---" -result=$(icp canister call backend whoami '()') && \ - echo "$result" && \ - echo "$result" | grep -q '"' && \ - echo "PASS" || (echo "FAIL" && exit 1) - -echo "--- Testing encrypted_notes: get_notes (empty) ---" -result=$(icp canister call backend get_notes '()') && \ - echo "$result" && \ - echo "$result" | grep -q 'vec' && \ - echo "PASS" || (echo "FAIL" && exit 1) From 4dbfb9ff31365c28057c72db55fbfa6e1ba1f46d Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Fri, 17 Jul 2026 10:18:36 +0200 Subject: [PATCH 3/7] chore(vetkeys/encrypted_notes): rust v3.3.0, package `backend` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump the Rust recipe to @dfinity/rust@v3.3.0 and rename the Cargo package to `backend`, dropping the custom `package:` from the recipe config (matching the other vetkeys examples). No Makefile / candid-extractor section here — this backend's .did is hand-maintained (no export_candid). Verified: rust deploy succeeds. Co-Authored-By: Claude Opus 4.8 (1M context) --- rust/vetkeys/encrypted_notes_app_vetkd/backend/Cargo.toml | 2 +- rust/vetkeys/encrypted_notes_app_vetkd/icp.yaml | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/rust/vetkeys/encrypted_notes_app_vetkd/backend/Cargo.toml b/rust/vetkeys/encrypted_notes_app_vetkd/backend/Cargo.toml index 48c9a0a7b1..5d071eb1fd 100644 --- a/rust/vetkeys/encrypted_notes_app_vetkd/backend/Cargo.toml +++ b/rust/vetkeys/encrypted_notes_app_vetkd/backend/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "encrypted_notes_backend" +name = "backend" version = "0.1.0" edition = "2018" diff --git a/rust/vetkeys/encrypted_notes_app_vetkd/icp.yaml b/rust/vetkeys/encrypted_notes_app_vetkd/icp.yaml index 76218e16c2..3b7759d854 100644 --- a/rust/vetkeys/encrypted_notes_app_vetkd/icp.yaml +++ b/rust/vetkeys/encrypted_notes_app_vetkd/icp.yaml @@ -1,9 +1,8 @@ canisters: - name: backend recipe: - type: "@dfinity/rust@v3.2.0" + type: "@dfinity/rust@v3.3.0" configuration: - package: encrypted_notes_backend candid: backend/backend.did init_args: type: text From adacdd84c29d63bdb5452a3b3a17654f054fe349 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Fri, 17 Jul 2026 12:03:44 +0200 Subject: [PATCH 4/7] docs(vetkeys/encrypted_notes): fix API links, condense security checklist, drop web3 jargon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Motoko README "API level": link to the Motoko password_manager examples (../password_manager, ../password_manager_with_metadata) instead of the Rust ones (depends on the password_manager split PRs #1446/#1447 being merged). - security-checklist.md: condense to a focused, language-specific version for each of Motoko and Rust (was a 165-line identical copy carried from the older device-based encrypted_notes — it wrongly claimed "no inter-canister calls" (this version calls the vetKD management canister) and referenced submit_ciphertexts / device removal that don't exist here). Link it from each README. - Drop "smart contract" web3 jargon from the backend comments (app.mo, lib.rs): "a single canister smart contract" -> "a single canister". Co-Authored-By: Claude Opus 4.8 (1M context) --- .../encrypted_notes_app_vetkd/README.md | 3 +- .../encrypted_notes_app_vetkd/backend/app.mo | 2 +- .../security-checklist.md | 171 +++-------------- .../encrypted_notes_app_vetkd/README.md | 1 + .../backend/src/lib.rs | 2 +- .../security-checklist.md | 172 +++--------------- 6 files changed, 50 insertions(+), 301 deletions(-) diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/README.md b/motoko/vetkeys/encrypted_notes_app_vetkd/README.md index b05d984a12..8dcea89f28 100644 --- a/motoko/vetkeys/encrypted_notes_app_vetkd/README.md +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/README.md @@ -68,9 +68,10 @@ If you run into issues, clearing all the application-specific IndexedDBs in the This example intentionally uses the **raw vetKD management canister API** (`encryptedSymmetricKeyForNote`, `symmetricKeyVerificationKeyForNote`) to demonstrate how vetKD works at the protocol level. -For most applications, the higher-level [`EncryptedMaps`](https://github.com/dfinity/vetkeys/tree/main/frontend/ic_vetkeys/src/encrypted_maps) abstraction from `@icp-sdk/vetkeys` is the recommended approach — it handles key derivation, caching, and access control internally without requiring a custom crypto layer. See the **VetKD Password Manager** ([`../../../rust/vetkeys/password_manager`](../../../rust/vetkeys/password_manager)) and **Password Manager with Metadata** ([`../../../rust/vetkeys/password_manager_with_metadata`](../../../rust/vetkeys/password_manager_with_metadata)) examples for how `EncryptedMaps` is used in practice. +For most applications, the higher-level [`EncryptedMaps`](https://github.com/dfinity/vetkeys/tree/main/frontend/ic_vetkeys/src/encrypted_maps) abstraction from `@icp-sdk/vetkeys` is the recommended approach — it handles key derivation, caching, and access control internally without requiring a custom crypto layer. See the **VetKD Password Manager** ([`../password_manager`](../password_manager)) and **Password Manager with Metadata** ([`../password_manager_with_metadata`](../password_manager_with_metadata)) examples for how `EncryptedMaps` is used in practice. ## Additional resources - **[What are VetKeys](https://docs.internetcomputer.org/concepts/vetkeys)** — more information about VetKeys and VetKD. +- [Security checklist for this example](security-checklist.md) - [Security best practices](https://docs.internetcomputer.org/guides/security/overview) diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/backend/app.mo b/motoko/vetkeys/encrypted_notes_app_vetkd/backend/app.mo index 47c85787fc..72c31c2404 100644 --- a/motoko/vetkeys/encrypted_notes_app_vetkd/backend/app.mo +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/backend/app.mo @@ -18,7 +18,7 @@ import Hex "./utils/Hex"; // Bind the caller and the initializer shared ({ caller = initializer }) actor class (keyName: Text) { - // Currently, a single canister smart contract is limited to 4 GB of heap size. + // Currently, a single canister is limited to 4 GB of heap size. // For the current limits see https://internetcomputer.org/docs/current/developer-docs/production/resource-limits. // To ensure that our canister does not exceed the limit, we put various restrictions (e.g., max number of users) in place. // This should keep us well below a memory usage of 2 GB because diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/security-checklist.md b/motoko/vetkeys/encrypted_notes_app_vetkd/security-checklist.md index 7f2c2a9a58..3f9727e9d4 100644 --- a/motoko/vetkeys/encrypted_notes_app_vetkd/security-checklist.md +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/security-checklist.md @@ -1,165 +1,36 @@ -This document lists typical steps of a security review needed for production-ready IC dapps. We indicate whether the two backend implementations of Encrypted Notes comply with the corresponding requirements (marked as Done), do not yet comply (Future), or whether a particular requirement is not applicable to this backend (Not applicable). +# Security checklist (Motoko) -While this list might help creating better IC dapps, keep in mind that the list is potentially incomplete. In particular, each real-world dapp may have a different set of security requirements that depend on its target domain and intended use case. +These notes summarize how this example's **Motoko** backend addresses the security considerations most relevant to a vetKeys dapp. They follow the broader [IC security best practices](https://internetcomputer.org/docs/building-apps/security/overview), which are more exhaustive — a production dapp should still perform its own review. -# 1. Authentication +## Authentication -### 1.1. Make sure any action that only a specific user should be able to do requires authentication -* Motoko: Done -* Rust: Done +- Every note operation requires authentication and rejects the anonymous principal (`assert not Principal.isAnonymous(caller)`). -### 1.2. Disallow the anonymous principal in authenticated calls -* Motoko: Done -* Rust: Done +## Consensus -# 2. Consensus +- The public API has no `query` methods; `getNotes` is an update call, so results go through consensus and cannot be forged by a single malicious node. -Avoid using uncertified queries in public canister APIs. Instead, either use certified update methods or design an eventual certification approach for performance-critical dapps. -* Motoko: Done (no query methods) -* Rust: Done (no query methods) +## Input validation -# 3. Input Validation +- Public methods validate their arguments (per-user note limits, note-size limits, authorization checks) and trap on invalid input. -Each public API method should sanitize their arguments and gracefully handle exceptional situations. -* Motoko: Done -* Rust: Done +## End-to-end encryption (frontend) -# 4. Frontend security +- Notes are encrypted in the browser; the canister only ever stores ciphertext. +- Encryption uses a fresh random IV per message (no deterministic encryption). +- Derived keys are stored as non-extractable `CryptoKey`s in IndexedDB. +- *Future:* shorten the Internet Identity delegation lifetime for a security-sensitive dapp, and rotate encryption keys periodically. -### 4.1. Frontend input validation -* Motoko: Done -* Rust: Done +## vetKD and inter-canister calls -### 4.2. Avoid using deterministic encryption. -For example, the initialization vector for AES-GCM encryption should be unique for each message (or chosen at random). -* Motoko: Done -* Rust: Done +- The backend calls only the trusted vetKD **management canister** (`aaaaa-aa`) to derive keys, and does not mutate shared state after an `await`, avoiding reentrancy hazards. -### 4.3. Do not load untrusted assets like CSS or fonts -* Motoko: Done -* Rust: Done +## Canister storage and upgrades -### 4.4. Avoid logging sensitive data like private keys -When generating the private key using `crypto.subtle.generateKey`, set `extractable=false`. Consider offloading the secret keys to a YubiKey or YubyHSM so that the secret keys never end up in the browser. -* Motoko: Done -* Rust: Done +- Only encrypted data is stored on the canister. +- Per-user limits bound how much any caller can store (max notes, note size, shares). +- State is retained across upgrades. *Future:* back large state with stable memory directly and version it for long-term upgradeability. -### 4.5. Avoid reusing the same public/private key pair for every identity in the same browser -* Motoko: Future -* Rust: Future +## Asset certification -### 4.6. Set reasonable session timeouts -For example, a security-sensitive dapp like Encrypted Notes should set `maxTimeToLive` for Internet Identity delegation to 30 min rather than 24 h. -* Motoko: Future -* Rust: Future - -### 4.7. Regularly refresh symmetric encryption keys -* Motoko: Future -* Rust: Future - -# 5. Asset Certification - -### 5.1. Use HTTP asset certification and avoid serving your dapp through raw.ic0.app -* Motoko: Done -* Rust: Done - -# 6. Canister Storage - -### 6.1. Use thread_local! with Cell/RefCell for state variables and put all your globals in one basket -* Motoko: Not applicable -* Rust: Done - -### 6.2. Limit the amount of data that can be stored in a canister per user -* Motoko: Done -* Rust: Done - -### 6.3. Consider using stable memory, version it, test it -* Motoko: Done (except versioning) -* Rust: Done (except versioning) - -### 6.4. Don’t store sensitive data on canisters (unless it is encrypted) -* Motoko: Done -* Rust: Done - -### 6.5. Create backups -* Motoko: Future -* Rust: Future - -# 7. Inter-Canister Calls and Rollbacks - -### 5.1. Don’t panic after await and don’t lock shared resources across await boundaries -* Motoko: Done (we don't use await) -* Rust: Done (we don't use await) - -### 5.2. Be aware that state may change during inter-canister calls -* Motoko: Done (we have no inter-canister calls) -* Rust: Done (we have no inter-canister calls) - -### 5.3. Only make inter-canister calls to trustworthy canisters -* Motoko: Done (we have no inter-canister calls) -* Rust: Done (we have no inter-canister calls) - -### 5.4. Make sure there are no loops in call graphs -* Motoko: Done -* Rust: Done - -# 8. Canister Upgrades - -### 8.1. Don’t panic/trap during upgrades: -* Motoko: Done, assuming that [`Iter.toArray`](https://github.com/dfinity/motoko-base/blob/master/src/Iter.mo) and [`Map.fromIter`](https://github.com/dfinity/motoko-base/blob/master/src/HashMap.mo) do not trap. -* Rust: Done, assuming that [`borrow_mut`](https://doc.rust-lang.org/std/borrow/trait.BorrowMut.html#tymethod.borrow_mut), [`std::mem::take`](https://doc.rust-lang.org/stable/std/mem/fn.take.html), and [`ic_cdk::storage::stable_save`](https://docs.rs/ic-cdk/latest/ic_cdk/storage/fn.stable_save.html) do not panic. - -### 8.2. Ensure upgradeability -If the canister storage becomes too big, the canister will no longer be upgradable because `pre_upgrade` will time out or the canister will run out of cycles. The recommended remedy is to use stable memory directly rather than serializing data upon upgrade. -* Motoko: Future -* Rust: Future - -# 9. Rust-specific issues - -### 9.1. Don’t use unsafe Rust code: -* Rust: Done - -### 9.2. Avoid integer overflows: -* Rust: Done - -# 10. Miscellaneous - -### 10.1. For expensive calls, consider using captchas or proof of work -* Motoko: Future -* Rust: Future - -### 10.2. Test your canister code even in presence of System API calls -* Motoko: Future -* Rust: Future - -### 10.3. Make canister builds reproducible -* Motoko: Done (via Docker) -* Rust: Done (via Docker) - -### 10.4. Expose metrics from your canister -* Motoko: Future -* Rust: Future - -### 10.5. Don’t rely on time being strictly monotonic -* Motoko: Done -* Rust: Done - -### 10.6. Protect against draining the cycles balance -* Motoko: Future -* Rust: Future - - -# 11. Efficiency considerations - -### 11.1. `submit_ciphertexts` -* Adding submit_ciphertexts is currently O(C*D) where `C = ciphertexts.size()` and `D = store.device_list.size()` - -# 12. Usability - -### 12.1. Confirm user's intention before executing potentially irreversible actions like device removal -* Motoko: Future -* Rust: Future - -### 12.2. Prevent account lockout scenarios -* Motoko: Future -* Rust: Future +- The frontend is served by the asset canister with certified HTTP responses. diff --git a/rust/vetkeys/encrypted_notes_app_vetkd/README.md b/rust/vetkeys/encrypted_notes_app_vetkd/README.md index f38cc16e67..9385d2cc06 100644 --- a/rust/vetkeys/encrypted_notes_app_vetkd/README.md +++ b/rust/vetkeys/encrypted_notes_app_vetkd/README.md @@ -73,4 +73,5 @@ For most applications, the higher-level [`EncryptedMaps`](https://github.com/dfi ## Additional resources - **[What are VetKeys](https://docs.internetcomputer.org/concepts/vetkeys)** — more information about VetKeys and VetKD. +- [Security checklist for this example](security-checklist.md) - [Security best practices](https://docs.internetcomputer.org/guides/security/overview) diff --git a/rust/vetkeys/encrypted_notes_app_vetkd/backend/src/lib.rs b/rust/vetkeys/encrypted_notes_app_vetkd/backend/src/lib.rs index 3bd0f2538a..f425f42079 100644 --- a/rust/vetkeys/encrypted_notes_app_vetkd/backend/src/lib.rs +++ b/rust/vetkeys/encrypted_notes_app_vetkd/backend/src/lib.rs @@ -63,7 +63,7 @@ impl Storable for NoteIds { // We use a canister's stable memory as storage. This simplifies the code and makes the appliation // more robust because no (potentially failing) pre_upgrade/post_upgrade hooks are needed. // Note that stable memory is less performant than heap memory, however. -// Currently, a single canister smart contract is limited to 96 GB of stable memory. +// Currently, a single canister is limited to 96 GB of stable memory. // For the current limits see https://internetcomputer.org/docs/current/developer-docs/production/resource-limits. // To ensure that our canister does not exceed the limit, we put various restrictions (e.g., number of users) in place. static MAX_USERS: u64 = 1_000; diff --git a/rust/vetkeys/encrypted_notes_app_vetkd/security-checklist.md b/rust/vetkeys/encrypted_notes_app_vetkd/security-checklist.md index 7f2c2a9a58..c51ba69e32 100644 --- a/rust/vetkeys/encrypted_notes_app_vetkd/security-checklist.md +++ b/rust/vetkeys/encrypted_notes_app_vetkd/security-checklist.md @@ -1,165 +1,41 @@ -This document lists typical steps of a security review needed for production-ready IC dapps. We indicate whether the two backend implementations of Encrypted Notes comply with the corresponding requirements (marked as Done), do not yet comply (Future), or whether a particular requirement is not applicable to this backend (Not applicable). +# Security checklist (Rust) -While this list might help creating better IC dapps, keep in mind that the list is potentially incomplete. In particular, each real-world dapp may have a different set of security requirements that depend on its target domain and intended use case. +These notes summarize how this example's **Rust** backend addresses the security considerations most relevant to a vetKeys dapp. They follow the broader [IC security best practices](https://internetcomputer.org/docs/building-apps/security/overview), which are more exhaustive — a production dapp should still perform its own review. -# 1. Authentication +## Authentication -### 1.1. Make sure any action that only a specific user should be able to do requires authentication -* Motoko: Done -* Rust: Done +- Every note operation requires authentication and rejects the anonymous principal (the `caller()` helper traps for the anonymous principal). -### 1.2. Disallow the anonymous principal in authenticated calls -* Motoko: Done -* Rust: Done +## Consensus -# 2. Consensus +- The public API has no `query` methods; `get_notes` is an update call, so results go through consensus and cannot be forged by a single malicious node. -Avoid using uncertified queries in public canister APIs. Instead, either use certified update methods or design an eventual certification approach for performance-critical dapps. -* Motoko: Done (no query methods) -* Rust: Done (no query methods) +## Input validation -# 3. Input Validation +- Public methods validate their arguments (per-user note limits, note-size limits, authorization checks) and trap on invalid input. -Each public API method should sanitize their arguments and gracefully handle exceptional situations. -* Motoko: Done -* Rust: Done +## End-to-end encryption (frontend) -# 4. Frontend security +- Notes are encrypted in the browser; the canister only ever stores ciphertext. +- Encryption uses a fresh random IV per message (no deterministic encryption). +- Derived keys are stored as non-extractable `CryptoKey`s in IndexedDB. +- *Future:* shorten the Internet Identity delegation lifetime for a security-sensitive dapp, and rotate encryption keys periodically. -### 4.1. Frontend input validation -* Motoko: Done -* Rust: Done +## vetKD and inter-canister calls -### 4.2. Avoid using deterministic encryption. -For example, the initialization vector for AES-GCM encryption should be unique for each message (or chosen at random). -* Motoko: Done -* Rust: Done +- The backend calls only the trusted vetKD **management canister** (`aaaaa-aa`) to derive keys, and does not mutate shared state after an `await`, avoiding reentrancy hazards. -### 4.3. Do not load untrusted assets like CSS or fonts -* Motoko: Done -* Rust: Done +## Canister storage and upgrades -### 4.4. Avoid logging sensitive data like private keys -When generating the private key using `crypto.subtle.generateKey`, set `extractable=false`. Consider offloading the secret keys to a YubiKey or YubyHSM so that the secret keys never end up in the browser. -* Motoko: Done -* Rust: Done +- State is stored directly in stable memory via `ic-stable-structures` (`StableBTreeMap`), so it is upgrade-safe by construction (no serialization step that could time out on large data). +- Only encrypted data is stored on the canister. +- Per-user limits bound how much any caller can store (max notes, note size, shares). -### 4.5. Avoid reusing the same public/private key pair for every identity in the same browser -* Motoko: Future -* Rust: Future +## Rust-specific -### 4.6. Set reasonable session timeouts -For example, a security-sensitive dapp like Encrypted Notes should set `maxTimeToLive` for Internet Identity delegation to 30 min rather than 24 h. -* Motoko: Future -* Rust: Future +- No `unsafe` code. +- Arithmetic is written to avoid integer overflow. -### 4.7. Regularly refresh symmetric encryption keys -* Motoko: Future -* Rust: Future +## Asset certification -# 5. Asset Certification - -### 5.1. Use HTTP asset certification and avoid serving your dapp through raw.ic0.app -* Motoko: Done -* Rust: Done - -# 6. Canister Storage - -### 6.1. Use thread_local! with Cell/RefCell for state variables and put all your globals in one basket -* Motoko: Not applicable -* Rust: Done - -### 6.2. Limit the amount of data that can be stored in a canister per user -* Motoko: Done -* Rust: Done - -### 6.3. Consider using stable memory, version it, test it -* Motoko: Done (except versioning) -* Rust: Done (except versioning) - -### 6.4. Don’t store sensitive data on canisters (unless it is encrypted) -* Motoko: Done -* Rust: Done - -### 6.5. Create backups -* Motoko: Future -* Rust: Future - -# 7. Inter-Canister Calls and Rollbacks - -### 5.1. Don’t panic after await and don’t lock shared resources across await boundaries -* Motoko: Done (we don't use await) -* Rust: Done (we don't use await) - -### 5.2. Be aware that state may change during inter-canister calls -* Motoko: Done (we have no inter-canister calls) -* Rust: Done (we have no inter-canister calls) - -### 5.3. Only make inter-canister calls to trustworthy canisters -* Motoko: Done (we have no inter-canister calls) -* Rust: Done (we have no inter-canister calls) - -### 5.4. Make sure there are no loops in call graphs -* Motoko: Done -* Rust: Done - -# 8. Canister Upgrades - -### 8.1. Don’t panic/trap during upgrades: -* Motoko: Done, assuming that [`Iter.toArray`](https://github.com/dfinity/motoko-base/blob/master/src/Iter.mo) and [`Map.fromIter`](https://github.com/dfinity/motoko-base/blob/master/src/HashMap.mo) do not trap. -* Rust: Done, assuming that [`borrow_mut`](https://doc.rust-lang.org/std/borrow/trait.BorrowMut.html#tymethod.borrow_mut), [`std::mem::take`](https://doc.rust-lang.org/stable/std/mem/fn.take.html), and [`ic_cdk::storage::stable_save`](https://docs.rs/ic-cdk/latest/ic_cdk/storage/fn.stable_save.html) do not panic. - -### 8.2. Ensure upgradeability -If the canister storage becomes too big, the canister will no longer be upgradable because `pre_upgrade` will time out or the canister will run out of cycles. The recommended remedy is to use stable memory directly rather than serializing data upon upgrade. -* Motoko: Future -* Rust: Future - -# 9. Rust-specific issues - -### 9.1. Don’t use unsafe Rust code: -* Rust: Done - -### 9.2. Avoid integer overflows: -* Rust: Done - -# 10. Miscellaneous - -### 10.1. For expensive calls, consider using captchas or proof of work -* Motoko: Future -* Rust: Future - -### 10.2. Test your canister code even in presence of System API calls -* Motoko: Future -* Rust: Future - -### 10.3. Make canister builds reproducible -* Motoko: Done (via Docker) -* Rust: Done (via Docker) - -### 10.4. Expose metrics from your canister -* Motoko: Future -* Rust: Future - -### 10.5. Don’t rely on time being strictly monotonic -* Motoko: Done -* Rust: Done - -### 10.6. Protect against draining the cycles balance -* Motoko: Future -* Rust: Future - - -# 11. Efficiency considerations - -### 11.1. `submit_ciphertexts` -* Adding submit_ciphertexts is currently O(C*D) where `C = ciphertexts.size()` and `D = store.device_list.size()` - -# 12. Usability - -### 12.1. Confirm user's intention before executing potentially irreversible actions like device removal -* Motoko: Future -* Rust: Future - -### 12.2. Prevent account lockout scenarios -* Motoko: Future -* Rust: Future +- The frontend is served by the asset canister with certified HTTP responses. From db7ec952edb7246412f940007016ac751000760b Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Fri, 17 Jul 2026 12:31:31 +0200 Subject: [PATCH 5/7] docs(vetkeys/encrypted_notes): refresh doc links, fix stable terminology, drop "dapp" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update backend doc links to current URLs: resource limits (references/resource-limits), query-vs-update calls, Motoko actors-async, and the upgrade-a-canister guide (canister-management/lifecycle). Drop the dead `motoko/main/upgrades/` links. - Motoko: fix outdated "stable" terminology in comments — actor fields are retained across upgrades unless `transient`; there is no `stable` keyword in use. Rename the `stable_*` upgrade-buffer vars to `*Backup` and describe the transient-maps + backup-buffers + pre/postupgrade pattern accurately. - Replace "dapp" with "app" in READMEs, checklists, and comments; normalize the security best-practices link to https://docs.internetcomputer.org/guides/security/overview/. Verified: Motoko compiles and state survives an upgrade. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../encrypted_notes_app_vetkd/README.md | 8 +-- .../encrypted_notes_app_vetkd/backend/app.mo | 58 +++++++++---------- .../security-checklist.md | 4 +- .../encrypted_notes_app_vetkd/README.md | 8 +-- .../backend/src/lib.rs | 8 +-- .../security-checklist.md | 4 +- 6 files changed, 43 insertions(+), 47 deletions(-) diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/README.md b/motoko/vetkeys/encrypted_notes_app_vetkd/README.md index 8dcea89f28..dc0197c2ab 100644 --- a/motoko/vetkeys/encrypted_notes_app_vetkd/README.md +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/README.md @@ -4,9 +4,9 @@ Also available in: [Rust](../../../rust/vetkeys/encrypted_notes_app_vetkd) -Encrypted notes is an example dapp for authoring and storing confidential information on the Internet Computer (ICP) in the form of short pieces of text. Users can create and access their notes via any number of automatically synchronized devices authenticated via Internet Identity (II). Notes are stored confidentially using vetKeys. The end-to-end encryption is performed by the dapp's frontend. +Encrypted notes is an example app for authoring and storing confidential information on the Internet Computer (ICP) in the form of short pieces of text. Users can create and access their notes via any number of automatically synchronized devices authenticated via Internet Identity (II). Notes are stored confidentially using vetKeys. The end-to-end encryption is performed by the app's frontend. -In particular, the notes are encrypted with an AES key that is derived (directly in the browser) from a note-ID-specific vetKey obtained from the backend canister (in encrypted form, using an ephemeral transport key), which itself obtains it from the vetKD system API. This way, there is no need for any device management in the dapp, plus sharing of notes becomes possible. +In particular, the notes are encrypted with an AES key that is derived (directly in the browser) from a note-ID-specific vetKey obtained from the backend canister (in encrypted form, using an ephemeral transport key), which itself obtains it from the vetKD system API. This way, there is no need for any device management in the app, plus sharing of notes becomes possible. The vetKey used to encrypt and decrypt a note is note-ID-specific (and not, for example, principal-specific) to enable the sharing of notes between users. The derived AES keys are stored as non-extractable CryptoKeys in an IndexedDB in the browser for efficiency so that their respective vetKey only has to be fetched from the server once. @@ -58,7 +58,7 @@ A **Svelte** application providing a user-friendly interface for managing encryp ## Limitations -This example dapp does not implement key rotation, which is strongly recommended in a production environment. +This example app does not implement key rotation, which is strongly recommended in a production environment. ## Troubleshooting @@ -74,4 +74,4 @@ For most applications, the higher-level [`EncryptedMaps`](https://github.com/dfi - **[What are VetKeys](https://docs.internetcomputer.org/concepts/vetkeys)** — more information about VetKeys and VetKD. - [Security checklist for this example](security-checklist.md) -- [Security best practices](https://docs.internetcomputer.org/guides/security/overview) +- [Security best practices](https://docs.internetcomputer.org/guides/security/overview/) diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/backend/app.mo b/motoko/vetkeys/encrypted_notes_app_vetkd/backend/app.mo index 72c31c2404..26603ea1b2 100644 --- a/motoko/vetkeys/encrypted_notes_app_vetkd/backend/app.mo +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/backend/app.mo @@ -19,14 +19,14 @@ import Hex "./utils/Hex"; shared ({ caller = initializer }) actor class (keyName: Text) { // Currently, a single canister is limited to 4 GB of heap size. - // For the current limits see https://internetcomputer.org/docs/current/developer-docs/production/resource-limits. + // For the current limits see https://docs.internetcomputer.org/references/resource-limits. // To ensure that our canister does not exceed the limit, we put various restrictions (e.g., max number of users) in place. // This should keep us well below a memory usage of 2 GB because // up to 2x memory may be needed for data serialization during canister upgrades. // This is sufficient for this proof-of-concept, but in a production environment the actual // memory usage must be calculated or monitored and the various restrictions adapted accordingly. - // Define dapp limits - important for security assurance + // Define app limits - important for security assurance private transient let MAX_USERS = 500; private transient let MAX_NOTES_PER_USER = 200; private transient let MAX_NOTE_CHARS = 1000; @@ -49,15 +49,14 @@ shared ({ caller = initializer }) actor class (keyName: Text) { users : [PrincipalName]; }; - // Define private fields - // Stable actor fields are automatically retained across canister upgrades. - // See https://internetcomputer.org/docs/current/motoko/main/upgrades/ + // Define private fields. + // Non-`transient` actor fields are automatically retained across canister upgrades. // Design choice: Use globally unique note identifiers for all users. // - // The keyword `stable` makes this (scalar) variable keep its value across canister upgrades. + // This variable is retained across upgrades because it is not declared `transient`. // - // See https://internetcomputer.org/docs/current/developer-docs/setup/manage-canisters#upgrade-a-canister + // See https://docs.internetcomputer.org/guides/canister-management/lifecycle/#upgrade-a-canister private var nextNoteId : Nat = 1; // Store notes by their ID, so that note-specific encryption keys can be derived. @@ -67,14 +66,12 @@ shared ({ caller = initializer }) actor class (keyName: Text) { // Store which notes are shared with a particular principal. Does not include the owner, as this is tracked by `noteIdsByOwner`. private transient var noteIdsByUser = Map.empty>(); - // While accessing _heap_ data is more efficient, we use the following _stable memory_ - // as a buffer to preserve data across canister upgrades. - // Stable memory is currently 96GB. For the current limits see - // https://internetcomputer.org/docs/current/developer-docs/production/resource-limits. - // See also: [preupgrade], [postupgrade] - private var stable_notesById : [(NoteId, EncryptedNote)] = []; - private var stable_noteIdsByOwner : [(PrincipalName, PureList.List)] = []; - private var stable_noteIdsByUser : [(PrincipalName, PureList.List)] = []; + // The maps above are `transient` (not retained across upgrades), so we snapshot + // them into the retained buffer arrays below during `preupgrade` and restore + // them in `postupgrade`. + private var notesByIdBackup : [(NoteId, EncryptedNote)] = []; + private var noteIdsByOwnerBackup : [(PrincipalName, PureList.List)] = []; + private var noteIdsByUserBackup : [(PrincipalName, PureList.List)] = []; // Utility function that helps writing assertion-driven code more concisely. private func expect(opt : ?T, violation_msg : Text) : T { @@ -99,7 +96,7 @@ shared ({ caller = initializer }) actor class (keyName: Text) { // Shared functions, i.e., those specified with [shared], are // accessible to remote callers. // The extra parameter [caller] is the caller's principal - // See https://internetcomputer.org/docs/current/motoko/main/actors-async + // See https://docs.internetcomputer.org/languages/motoko/fundamentals/actors/actors-async // Add new empty note for this [caller]. // @@ -142,11 +139,11 @@ shared ({ caller = initializer }) actor class (keyName: Text) { // Note that this method is declared as an *update* call (see `shared`) rather than *query*. // // While queries are significantly faster than updates, they are not certified by the IC. - // Thus, we avoid using queries throughout this dapp, ensuring that the result of our + // Thus, we avoid using queries throughout this app, ensuring that the result of our // functions gets through consensus. Otherwise, this function could e.g. omit some notes - // if it got executed by a malicious node. (To make the dapp more efficient, one could + // if it got executed by a malicious node. (To make the app more efficient, one could // use an approach in which both queries and updates are combined.) - // See https://internetcomputer.org/docs/current/concepts/canisters-code#query-and-update-methods + // See https://docs.internetcomputer.org/guides/canister-calls/calling-from-clients/#query-vs-update-calls // // Returns: // Future of array of EncryptedNote @@ -363,39 +360,38 @@ shared ({ caller = initializer }) actor class (keyName: Text) { }; // Below, we implement the upgrade hooks for our canister. - // See https://internetcomputer.org/docs/current/motoko/main/upgrades/ // The work required before a canister upgrade begins. system func preupgrade() { Debug.print("Starting pre-upgrade hook..."); - stable_notesById := Iter.toArray(Map.entries(notesById)); - stable_noteIdsByOwner := Iter.toArray(Map.entries(noteIdsByOwner)); - stable_noteIdsByUser := Iter.toArray(Map.entries(noteIdsByUser)); + notesByIdBackup := Iter.toArray(Map.entries(notesById)); + noteIdsByOwnerBackup := Iter.toArray(Map.entries(noteIdsByOwner)); + noteIdsByUserBackup := Iter.toArray(Map.entries(noteIdsByUser)); Debug.print("pre-upgrade finished."); }; - // The work required after a canister upgrade ends. - // See [nextNoteId], [stable_notesByUser] + // The work required after a canister upgrade ends: restore the transient maps + // from the retained backup buffers. system func postupgrade() { Debug.print("Starting post-upgrade hook..."); notesById := Map.fromIter( - stable_notesById.values(), + notesByIdBackup.values(), Nat.compare, ); - stable_notesById := []; + notesByIdBackup := []; noteIdsByOwner := Map.fromIter( - stable_noteIdsByOwner.values(), + noteIdsByOwnerBackup.values(), Text.compare, ); - stable_noteIdsByOwner := []; + noteIdsByOwnerBackup := []; noteIdsByUser := Map.fromIter( - stable_noteIdsByUser.values(), + noteIdsByUserBackup.values(), Text.compare, ); - stable_noteIdsByUser := []; + noteIdsByUserBackup := []; Debug.print("post-upgrade finished."); }; diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/security-checklist.md b/motoko/vetkeys/encrypted_notes_app_vetkd/security-checklist.md index 3f9727e9d4..fc6043235d 100644 --- a/motoko/vetkeys/encrypted_notes_app_vetkd/security-checklist.md +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/security-checklist.md @@ -1,6 +1,6 @@ # Security checklist (Motoko) -These notes summarize how this example's **Motoko** backend addresses the security considerations most relevant to a vetKeys dapp. They follow the broader [IC security best practices](https://internetcomputer.org/docs/building-apps/security/overview), which are more exhaustive — a production dapp should still perform its own review. +These notes summarize how this example's **Motoko** backend addresses the security considerations most relevant to a vetKeys app. They follow the broader [IC security best practices](https://docs.internetcomputer.org/guides/security/overview/), which are more exhaustive — a production app should still perform its own review. ## Authentication @@ -19,7 +19,7 @@ These notes summarize how this example's **Motoko** backend addresses the securi - Notes are encrypted in the browser; the canister only ever stores ciphertext. - Encryption uses a fresh random IV per message (no deterministic encryption). - Derived keys are stored as non-extractable `CryptoKey`s in IndexedDB. -- *Future:* shorten the Internet Identity delegation lifetime for a security-sensitive dapp, and rotate encryption keys periodically. +- *Future:* shorten the Internet Identity delegation lifetime for a security-sensitive app, and rotate encryption keys periodically. ## vetKD and inter-canister calls diff --git a/rust/vetkeys/encrypted_notes_app_vetkd/README.md b/rust/vetkeys/encrypted_notes_app_vetkd/README.md index 9385d2cc06..ee481289df 100644 --- a/rust/vetkeys/encrypted_notes_app_vetkd/README.md +++ b/rust/vetkeys/encrypted_notes_app_vetkd/README.md @@ -4,9 +4,9 @@ Also available in: [Motoko](../../../motoko/vetkeys/encrypted_notes_app_vetkd) -Encrypted notes is an example dapp for authoring and storing confidential information on the Internet Computer (ICP) in the form of short pieces of text. Users can create and access their notes via any number of automatically synchronized devices authenticated via Internet Identity (II). Notes are stored confidentially using vetKeys. The end-to-end encryption is performed by the dapp's frontend. +Encrypted notes is an example app for authoring and storing confidential information on the Internet Computer (ICP) in the form of short pieces of text. Users can create and access their notes via any number of automatically synchronized devices authenticated via Internet Identity (II). Notes are stored confidentially using vetKeys. The end-to-end encryption is performed by the app's frontend. -In particular, the notes are encrypted with an AES key that is derived (directly in the browser) from a note-ID-specific vetKey obtained from the backend canister (in encrypted form, using an ephemeral transport key), which itself obtains it from the vetKD system API. This way, there is no need for any device management in the dapp, plus sharing of notes becomes possible. +In particular, the notes are encrypted with an AES key that is derived (directly in the browser) from a note-ID-specific vetKey obtained from the backend canister (in encrypted form, using an ephemeral transport key), which itself obtains it from the vetKD system API. This way, there is no need for any device management in the app, plus sharing of notes becomes possible. The vetKey used to encrypt and decrypt a note is note-ID-specific (and not, for example, principal-specific) to enable the sharing of notes between users. The derived AES keys are stored as non-extractable CryptoKeys in an IndexedDB in the browser for efficiency so that their respective vetKey only has to be fetched from the server once. @@ -58,7 +58,7 @@ A **Svelte** application providing a user-friendly interface for managing encryp ## Limitations -This example dapp does not implement key rotation, which is strongly recommended in a production environment. +This example app does not implement key rotation, which is strongly recommended in a production environment. ## Troubleshooting @@ -74,4 +74,4 @@ For most applications, the higher-level [`EncryptedMaps`](https://github.com/dfi - **[What are VetKeys](https://docs.internetcomputer.org/concepts/vetkeys)** — more information about VetKeys and VetKD. - [Security checklist for this example](security-checklist.md) -- [Security best practices](https://docs.internetcomputer.org/guides/security/overview) +- [Security best practices](https://docs.internetcomputer.org/guides/security/overview/) diff --git a/rust/vetkeys/encrypted_notes_app_vetkd/backend/src/lib.rs b/rust/vetkeys/encrypted_notes_app_vetkd/backend/src/lib.rs index f425f42079..ac73cdca53 100644 --- a/rust/vetkeys/encrypted_notes_app_vetkd/backend/src/lib.rs +++ b/rust/vetkeys/encrypted_notes_app_vetkd/backend/src/lib.rs @@ -64,7 +64,7 @@ impl Storable for NoteIds { // more robust because no (potentially failing) pre_upgrade/post_upgrade hooks are needed. // Note that stable memory is less performant than heap memory, however. // Currently, a single canister is limited to 96 GB of stable memory. -// For the current limits see https://internetcomputer.org/docs/current/developer-docs/production/resource-limits. +// For the current limits see https://docs.internetcomputer.org/references/resource-limits. // To ensure that our canister does not exceed the limit, we put various restrictions (e.g., number of users) in place. static MAX_USERS: u64 = 1_000; static MAX_NOTES_PER_USER: usize = 500; @@ -138,12 +138,12 @@ fn caller() -> Principal { // #[query(name = "notesCnt")] ... // // While queries are significantly faster than updates, they are not certified by the IC. -// Thus, we avoid using queries throughout this dapp, ensuring that the result of our +// Thus, we avoid using queries throughout this app, ensuring that the result of our // methods gets through consensus. Otherwise, this method could e.g. omit some notes -// if it got executed by a malicious node. (To make the dapp more efficient, one could +// if it got executed by a malicious node. (To make the app more efficient, one could // use an approach in which both queries and updates are combined.) // -// See https://internetcomputer.org/docs/current/concepts/canisters-code#query-and-update-methods +// See https://docs.internetcomputer.org/guides/canister-calls/calling-from-clients/#query-vs-update-calls /// Reflects the [caller]'s identity by returning (a future of) its principal. /// Useful for debugging. diff --git a/rust/vetkeys/encrypted_notes_app_vetkd/security-checklist.md b/rust/vetkeys/encrypted_notes_app_vetkd/security-checklist.md index c51ba69e32..fd2fadbdc6 100644 --- a/rust/vetkeys/encrypted_notes_app_vetkd/security-checklist.md +++ b/rust/vetkeys/encrypted_notes_app_vetkd/security-checklist.md @@ -1,6 +1,6 @@ # Security checklist (Rust) -These notes summarize how this example's **Rust** backend addresses the security considerations most relevant to a vetKeys dapp. They follow the broader [IC security best practices](https://internetcomputer.org/docs/building-apps/security/overview), which are more exhaustive — a production dapp should still perform its own review. +These notes summarize how this example's **Rust** backend addresses the security considerations most relevant to a vetKeys app. They follow the broader [IC security best practices](https://docs.internetcomputer.org/guides/security/overview/), which are more exhaustive — a production app should still perform its own review. ## Authentication @@ -19,7 +19,7 @@ These notes summarize how this example's **Rust** backend addresses the security - Notes are encrypted in the browser; the canister only ever stores ciphertext. - Encryption uses a fresh random IV per message (no deterministic encryption). - Derived keys are stored as non-extractable `CryptoKey`s in IndexedDB. -- *Future:* shorten the Internet Identity delegation lifetime for a security-sensitive dapp, and rotate encryption keys periodically. +- *Future:* shorten the Internet Identity delegation lifetime for a security-sensitive app, and rotate encryption keys periodically. ## vetKD and inter-canister calls From 7245fccc122eae59c19bf0ae024075cf688e3de5 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Fri, 17 Jul 2026 15:03:18 +0200 Subject: [PATCH 6/7] docs(vetkeys/encrypted_notes): restore done-vs-future in security checklists Use task-list checkboxes so the distinction is explicit and scannable: [x] marks what the example implements, [ ] marks recommendations left out for simplicity that a production app should still address. All checked items were verified against the backend code. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../security-checklist.md | 28 ++++++++++------- .../security-checklist.md | 31 ++++++++++--------- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/security-checklist.md b/motoko/vetkeys/encrypted_notes_app_vetkd/security-checklist.md index fc6043235d..462d6686fa 100644 --- a/motoko/vetkeys/encrypted_notes_app_vetkd/security-checklist.md +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/security-checklist.md @@ -2,35 +2,39 @@ These notes summarize how this example's **Motoko** backend addresses the security considerations most relevant to a vetKeys app. They follow the broader [IC security best practices](https://docs.internetcomputer.org/guides/security/overview/), which are more exhaustive — a production app should still perform its own review. +Checked items (`[x]`) are implemented by this example. Unchecked items (`[ ]`) are recommendations that this example intentionally leaves out for simplicity and that a production app should still address. + ## Authentication -- Every note operation requires authentication and rejects the anonymous principal (`assert not Principal.isAnonymous(caller)`). +- [x] Every note operation requires authentication and rejects the anonymous principal (`assert not Principal.isAnonymous(caller)`). ## Consensus -- The public API has no `query` methods; `getNotes` is an update call, so results go through consensus and cannot be forged by a single malicious node. +- [x] The public API has no `query` methods; `getNotes` is an update call, so results go through consensus and cannot be forged by a single malicious node. ## Input validation -- Public methods validate their arguments (per-user note limits, note-size limits, authorization checks) and trap on invalid input. +- [x] Public methods validate their arguments (per-user note limits, note-size limits, authorization checks) and trap on invalid input. ## End-to-end encryption (frontend) -- Notes are encrypted in the browser; the canister only ever stores ciphertext. -- Encryption uses a fresh random IV per message (no deterministic encryption). -- Derived keys are stored as non-extractable `CryptoKey`s in IndexedDB. -- *Future:* shorten the Internet Identity delegation lifetime for a security-sensitive app, and rotate encryption keys periodically. +- [x] Notes are encrypted in the browser; the canister only ever stores ciphertext. +- [x] Encryption uses a fresh random IV per message (no deterministic encryption). +- [x] Derived keys are stored as non-extractable `CryptoKey`s in IndexedDB. +- [ ] Shorten the Internet Identity delegation lifetime for a security-sensitive app. +- [ ] Rotate encryption keys periodically. ## vetKD and inter-canister calls -- The backend calls only the trusted vetKD **management canister** (`aaaaa-aa`) to derive keys, and does not mutate shared state after an `await`, avoiding reentrancy hazards. +- [x] The backend calls only the trusted vetKD **management canister** (`aaaaa-aa`) to derive keys, and does not mutate shared state after an `await`, avoiding reentrancy hazards. ## Canister storage and upgrades -- Only encrypted data is stored on the canister. -- Per-user limits bound how much any caller can store (max notes, note size, shares). -- State is retained across upgrades. *Future:* back large state with stable memory directly and version it for long-term upgradeability. +- [x] Only encrypted data is stored on the canister. +- [x] Per-user limits bound how much any caller can store (max notes, note size, shares). +- [x] State is retained across upgrades. +- [ ] For long-term upgradeability, back large state with stable memory directly and version it. ## Asset certification -- The frontend is served by the asset canister with certified HTTP responses. +- [x] The frontend is served by the asset canister with certified HTTP responses. diff --git a/rust/vetkeys/encrypted_notes_app_vetkd/security-checklist.md b/rust/vetkeys/encrypted_notes_app_vetkd/security-checklist.md index fd2fadbdc6..77cf464918 100644 --- a/rust/vetkeys/encrypted_notes_app_vetkd/security-checklist.md +++ b/rust/vetkeys/encrypted_notes_app_vetkd/security-checklist.md @@ -2,40 +2,43 @@ These notes summarize how this example's **Rust** backend addresses the security considerations most relevant to a vetKeys app. They follow the broader [IC security best practices](https://docs.internetcomputer.org/guides/security/overview/), which are more exhaustive — a production app should still perform its own review. +Checked items (`[x]`) are implemented by this example. Unchecked items (`[ ]`) are recommendations that this example intentionally leaves out for simplicity and that a production app should still address. + ## Authentication -- Every note operation requires authentication and rejects the anonymous principal (the `caller()` helper traps for the anonymous principal). +- [x] Every note operation requires authentication and rejects the anonymous principal (the `caller()` helper traps for the anonymous principal). ## Consensus -- The public API has no `query` methods; `get_notes` is an update call, so results go through consensus and cannot be forged by a single malicious node. +- [x] The public API has no `query` methods; `get_notes` is an update call, so results go through consensus and cannot be forged by a single malicious node. ## Input validation -- Public methods validate their arguments (per-user note limits, note-size limits, authorization checks) and trap on invalid input. +- [x] Public methods validate their arguments (per-user note limits, note-size limits, authorization checks) and trap on invalid input. ## End-to-end encryption (frontend) -- Notes are encrypted in the browser; the canister only ever stores ciphertext. -- Encryption uses a fresh random IV per message (no deterministic encryption). -- Derived keys are stored as non-extractable `CryptoKey`s in IndexedDB. -- *Future:* shorten the Internet Identity delegation lifetime for a security-sensitive app, and rotate encryption keys periodically. +- [x] Notes are encrypted in the browser; the canister only ever stores ciphertext. +- [x] Encryption uses a fresh random IV per message (no deterministic encryption). +- [x] Derived keys are stored as non-extractable `CryptoKey`s in IndexedDB. +- [ ] Shorten the Internet Identity delegation lifetime for a security-sensitive app. +- [ ] Rotate encryption keys periodically. ## vetKD and inter-canister calls -- The backend calls only the trusted vetKD **management canister** (`aaaaa-aa`) to derive keys, and does not mutate shared state after an `await`, avoiding reentrancy hazards. +- [x] The backend calls only the trusted vetKD **management canister** (`aaaaa-aa`) to derive keys, and does not mutate shared state after an `await`, avoiding reentrancy hazards. ## Canister storage and upgrades -- State is stored directly in stable memory via `ic-stable-structures` (`StableBTreeMap`), so it is upgrade-safe by construction (no serialization step that could time out on large data). -- Only encrypted data is stored on the canister. -- Per-user limits bound how much any caller can store (max notes, note size, shares). +- [x] State is stored directly in stable memory via `ic-stable-structures` (`StableBTreeMap`), so it is upgrade-safe by construction (no serialization step that could time out on large data). +- [x] Only encrypted data is stored on the canister. +- [x] Per-user limits bound how much any caller can store (max notes, note size, shares). ## Rust-specific -- No `unsafe` code. -- Arithmetic is written to avoid integer overflow. +- [x] No `unsafe` code. +- [x] Arithmetic is written to avoid integer overflow (e.g. the note-ID counter uses `checked_add`). ## Asset certification -- The frontend is served by the asset canister with certified HTTP responses. +- [x] The frontend is served by the asset canister with certified HTTP responses. From 41025a0697c88d66f8de36f24a3be743ae38d40c Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Fri, 17 Jul 2026 15:11:01 +0200 Subject: [PATCH 7/7] refactor(vetkeys/encrypted_notes): drop legacy preupgrade/postupgrade hooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Under enhanced orthogonal persistence, `mo:core` Map fields are retained across upgrades automatically. The maps were declared `transient` and then manually snapshotted into backup arrays in `preupgrade`/`postupgrade` — machinery that existed only to undo the self-imposed `transient`. Drop `transient` from the three maps, delete the backup arrays and both system hooks, and remove the now unused `Iter`/`Debug` imports. Verified: a note created before an upgrade is still present after it. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../encrypted_notes_app_vetkd/backend/app.mo | 62 +++---------------- .../backend/backend.did | 4 +- 2 files changed, 10 insertions(+), 56 deletions(-) diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/backend/app.mo b/motoko/vetkeys/encrypted_notes_app_vetkd/backend/app.mo index 26603ea1b2..1d0dcec0c4 100644 --- a/motoko/vetkeys/encrypted_notes_app_vetkd/backend/app.mo +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/backend/app.mo @@ -3,13 +3,11 @@ import Text "mo:core/Text"; import Array "mo:core/Array"; import List "mo:core/List"; import PureList "mo:core/pure/List"; -import Iter "mo:core/Iter"; import Nat "mo:core/Nat"; import Nat8 "mo:core/Nat8"; import Bool "mo:core/Bool"; import Principal "mo:core/Principal"; import Option "mo:core/Option"; -import Debug "mo:core/Debug"; import Runtime "mo:core/Runtime"; import Blob "mo:core/Blob"; import Hex "./utils/Hex"; @@ -50,28 +48,21 @@ shared ({ caller = initializer }) actor class (keyName: Text) { }; // Define private fields. - // Non-`transient` actor fields are automatically retained across canister upgrades. - - // Design choice: Use globally unique note identifiers for all users. - // - // This variable is retained across upgrades because it is not declared `transient`. + // Actor fields are automatically retained across canister upgrades unless + // declared `transient` (enhanced orthogonal persistence), so no `preupgrade`/ + // `postupgrade` hooks are needed to persist the state below. // // See https://docs.internetcomputer.org/guides/canister-management/lifecycle/#upgrade-a-canister + + // Design choice: Use globally unique note identifiers for all users. private var nextNoteId : Nat = 1; // Store notes by their ID, so that note-specific encryption keys can be derived. - private transient var notesById = Map.empty(); + private var notesById = Map.empty(); // Store which note IDs are owned by a particular principal - private transient var noteIdsByOwner = Map.empty>(); + private var noteIdsByOwner = Map.empty>(); // Store which notes are shared with a particular principal. Does not include the owner, as this is tracked by `noteIdsByOwner`. - private transient var noteIdsByUser = Map.empty>(); - - // The maps above are `transient` (not retained across upgrades), so we snapshot - // them into the retained buffer arrays below during `preupgrade` and restore - // them in `postupgrade`. - private var notesByIdBackup : [(NoteId, EncryptedNote)] = []; - private var noteIdsByOwnerBackup : [(PrincipalName, PureList.List)] = []; - private var noteIdsByUserBackup : [(PrincipalName, PureList.List)] = []; + private var noteIdsByUser = Map.empty>(); // Utility function that helps writing assertion-driven code more concisely. private func expect(opt : ?T, violation_msg : Text) : T { @@ -358,41 +349,4 @@ shared ({ caller = initializer }) actor class (keyName: Text) { }; Array.tabulate(len, ith_byte); }; - - // Below, we implement the upgrade hooks for our canister. - - // The work required before a canister upgrade begins. - system func preupgrade() { - Debug.print("Starting pre-upgrade hook..."); - notesByIdBackup := Iter.toArray(Map.entries(notesById)); - noteIdsByOwnerBackup := Iter.toArray(Map.entries(noteIdsByOwner)); - noteIdsByUserBackup := Iter.toArray(Map.entries(noteIdsByUser)); - Debug.print("pre-upgrade finished."); - }; - - // The work required after a canister upgrade ends: restore the transient maps - // from the retained backup buffers. - system func postupgrade() { - Debug.print("Starting post-upgrade hook..."); - - notesById := Map.fromIter( - notesByIdBackup.values(), - Nat.compare, - ); - notesByIdBackup := []; - - noteIdsByOwner := Map.fromIter( - noteIdsByOwnerBackup.values(), - Text.compare, - ); - noteIdsByOwnerBackup := []; - - noteIdsByUser := Map.fromIter( - noteIdsByUserBackup.values(), - Text.compare, - ); - noteIdsByUserBackup := []; - - Debug.print("post-upgrade finished."); - }; }; diff --git a/motoko/vetkeys/encrypted_notes_app_vetkd/backend/backend.did b/motoko/vetkeys/encrypted_notes_app_vetkd/backend/backend.did index 8c355cf4b4..ce3f9b9f93 100644 --- a/motoko/vetkeys/encrypted_notes_app_vetkd/backend/backend.did +++ b/motoko/vetkeys/encrypted_notes_app_vetkd/backend/backend.did @@ -1,4 +1,4 @@ -type _anon_class_19_1 = +type _anon_class_17_1 = service { addUser: (note_id: NoteId, user: PrincipalName) -> (); createNote: () -> (NoteId); @@ -20,4 +20,4 @@ type EncryptedNote = owner: PrincipalName; users: vec PrincipalName; }; -service : (keyName: text) -> _anon_class_19_1 +service : (keyName: text) -> _anon_class_17_1