diff --git a/.github/workflows/motoko-encrypted-notes-vetkd-example.yml b/.github/workflows/motoko-encrypted-notes-vetkd-example.yml index 2c68ee5ed..4464e8a34 100644 --- a/.github/workflows/motoko-encrypted-notes-vetkd-example.yml +++ b/.github/workflows/motoko-encrypted-notes-vetkd-example.yml @@ -1,4 +1,4 @@ -name: motoko-encrypted-notes +name: motoko-encrypted-notes-vetkd on: push: branches: @@ -6,9 +6,9 @@ on: pull_request: paths: - motoko/encrypted-notes-dapp-vetkd/** - - .github/workflows/provision-darwin.sh + - motoko/encrypted-notes-dapp-vetkd/provision-darwin.sh - .github/workflows/provision-linux.sh - - .github/workflows/motoko-encrypted-notes-example.yml + - .github/workflows/motoko-encrypted-notes-vetkd-example.yml concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/motoko-encrypted-notes-vetkd-skip.yml b/.github/workflows/motoko-encrypted-notes-vetkd-skip.yml index 2e80e402f..21e7784e4 100644 --- a/.github/workflows/motoko-encrypted-notes-vetkd-skip.yml +++ b/.github/workflows/motoko-encrypted-notes-vetkd-skip.yml @@ -1,11 +1,11 @@ -name: motoko-encrypted-notes +name: motoko-encrypted-notes-vetkd on: pull_request: paths-ignore: - motoko/encrypted-notes-dapp-vetkd/** - - .github/workflows/provision-darwin.sh + - motoko/encrypted-notes-dapp-vetkd/provision-darwin.sh - .github/workflows/provision-linux.sh - - .github/workflows/motoko-encrypted-notes-example.yml + - .github/workflows/motoko-encrypted-notes-vetkd-example.yml concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/rust-encrypted-notes-vetkd-example.yml b/.github/workflows/rust-encrypted-notes-vetkd-example.yml index 2a6183557..d1f4f669d 100644 --- a/.github/workflows/rust-encrypted-notes-vetkd-example.yml +++ b/.github/workflows/rust-encrypted-notes-vetkd-example.yml @@ -1,4 +1,4 @@ -name: rust-encrypted-notes +name: rust-encrypted-notes-vetkd on: push: branches: @@ -6,9 +6,9 @@ on: pull_request: paths: - motoko/encrypted-notes-dapp-vetkd/** - - .github/workflows/provision-darwin.sh + - motoko/encrypted-notes-dapp-vetkd/provision-darwin.sh - .github/workflows/provision-linux.sh - - .github/workflows/rust-encrypted-notes-example.yml + - .github/workflows/rust-encrypted-notes-vetkd-example.yml concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/rust-encrypted-notes-vetkd-skip.yml b/.github/workflows/rust-encrypted-notes-vetkd-skip.yml index 148a0798c..0a26c7a05 100644 --- a/.github/workflows/rust-encrypted-notes-vetkd-skip.yml +++ b/.github/workflows/rust-encrypted-notes-vetkd-skip.yml @@ -1,11 +1,11 @@ -name: rust-encrypted-notes +name: rust-encrypted-notes-vetkd on: pull_request: paths-ignore: - - rust/encrypted-notes-dapp-vetkd/** - - .github/workflows/provision-darwin.sh + - motoko/encrypted-notes-dapp-vetkd/** + - motoko/encrypted-notes-dapp-vetkd/provision-darwin.sh - .github/workflows/provision-linux.sh - - .github/workflows/rust-encrypted-notes-example.yml + - .github/workflows/rust-encrypted-notes-vetkd-example.yml concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true diff --git a/motoko/encrypted-notes-dapp-vetkd/Cargo.lock b/motoko/encrypted-notes-dapp-vetkd/Cargo.lock index 1916cdb7e..a14bcafd2 100644 --- a/motoko/encrypted-notes-dapp-vetkd/Cargo.lock +++ b/motoko/encrypted-notes-dapp-vetkd/Cargo.lock @@ -2,20 +2,11 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "aho-corasick" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" -dependencies = [ - "memchr", -] - [[package]] name = "anyhow" -version = "1.0.52" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84450d0b4a8bd1ba4144ce8ce718fbc5d071358b1e5384bace6536b3d1f2d5b3" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "arrayvec" @@ -23,44 +14,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" -[[package]] -name = "ascii-canvas" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" -dependencies = [ - "term", -] - -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] - [[package]] name = "autocfg" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" -[[package]] -name = "base32" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" - -[[package]] -name = "beef" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bed554bd50246729a1ec158d08aa3235d1b69d94ad120ebe187e28894787e736" - [[package]] name = "binread" version = "2.2.0" @@ -81,35 +40,14 @@ dependencies = [ "either", "proc-macro2", "quote", - "syn", -] - -[[package]] -name = "bit-set" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" -dependencies = [ - "bit-vec", + "syn 1.0.85", ] -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "block-buffer" -version = "0.9.0" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] @@ -122,21 +60,19 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "candid" -version = "0.7.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12970d8d0620d2bdb7e81a5b13ed11e41fcdfeba53d61e45b5853afcbf9611fd" +checksum = "465c1ce01d8089ee5b49ba20d3a9da15a28bba64c35cdff2aa256d37e319625d" dependencies = [ "anyhow", "binread", "byteorder", "candid_derive", "codespan-reporting", + "crc32fast", + "data-encoding", "hex", - "ic-types", - "lalrpop", - "lalrpop-util", "leb128", - "logos", "num-bigint", "num-traits", "num_enum", @@ -144,19 +80,30 @@ dependencies = [ "pretty", "serde", "serde_bytes", + "sha2", + "stacker", "thiserror", ] [[package]] name = "candid_derive" -version = "0.4.5" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e02c03c4d547674a3f3f3109538fb49871fbe636216daa019f06a62faca9061" +checksum = "201ea498d901add0822653ac94cb0f8a92f9b1758a5273f4dafbb6673c9a5020" dependencies = [ "lazy_static", "proc-macro2", "quote", - "syn", + "syn 2.0.38", +] + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", ] [[package]] @@ -194,45 +141,29 @@ dependencies = [ ] [[package]] -name = "crunchy" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" - -[[package]] -name = "diff" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" - -[[package]] -name = "digest" -version = "0.9.0" +name = "crypto-common" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "typenum", ] [[package]] -name = "dirs-next" -version = "2.0.0" +name = "data-encoding" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] +checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] -name = "dirs-sys-next" -version = "0.1.2" +name = "digest" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "libc", - "redox_users", - "winapi", + "block-buffer", + "crypto-common", ] [[package]] @@ -241,41 +172,22 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" -[[package]] -name = "ena" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7402b94a93c24e742487327a7cd839dc9d36fec9de9fb25b09f2dae459f36c3" -dependencies = [ - "log", -] - [[package]] name = "encrypted_notes_rust" version = "0.1.0" dependencies = [ "anyhow", + "candid", "hex", "ic-cdk", "ic-cdk-macros", + "ic-stable-structures", "ic-types", "lazy_static", "serde", "serde_json", ] -[[package]] -name = "fixedbitset" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - [[package]] name = "generic-array" version = "0.14.5" @@ -286,32 +198,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "getrandom" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "hashbrown" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" - -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - [[package]] name = "hex" version = "0.4.3" @@ -320,72 +206,48 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "ic-cdk" -version = "0.3.3" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "606276ed1ce363eb9ccaf492e36fb40425417dcd4598f261d47e0ed6a1309faa" +checksum = "c126ac20219abff15c3441282e9da6aa7244319d5a4a42c7260667237e790712" dependencies = [ "candid", - "cfg-if", + "ic-cdk-macros", + "ic0", "serde", + "serde_bytes", ] [[package]] name = "ic-cdk-macros" -version = "0.3.3" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bac2578a4779c3ae6d24c766ec7127872d73f29a9e7d70b6607a2fdedd0dde" +checksum = "8b6295fd7389c198a97dd99b28b846e18487d99303077102d817eebbf6a924cd" dependencies = [ "candid", - "ic-cdk", "proc-macro2", "quote", "serde", "serde_tokenstream", - "syn", + "syn 1.0.85", ] [[package]] -name = "ic-types" -version = "0.3.0" +name = "ic-stable-structures" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e78ec6f58886cdc252d6f912dc794211bd6bbc39ddc9dcda434b2dc16c335b3" -dependencies = [ - "base32", - "crc32fast", - "hex", - "serde", - "serde_bytes", - "sha2", - "thiserror", -] +checksum = "be4867a1d9f232e99ca68682161d1fc67dff9501f4f1bf42d69a9358289ad0f8" [[package]] -name = "indexmap" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" -dependencies = [ - "autocfg", - "hashbrown", -] - -[[package]] -name = "instant" -version = "0.1.12" +name = "ic-types" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] +checksum = "95af86c6e40ea8a850c64914024762a0b91a2a6d438b412d8f4a7fc010e72519" [[package]] -name = "itertools" -version = "0.10.5" +name = "ic0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] +checksum = "a54b5297861c651551676e8c43df805dad175cc33bc97dbd992edbbb85dcbcdf" [[package]] name = "itoa" @@ -393,38 +255,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" -[[package]] -name = "lalrpop" -version = "0.19.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15174f1c529af5bf1283c3bc0058266b483a67156f79589fab2a25e23cf8988" -dependencies = [ - "ascii-canvas", - "atty", - "bit-set", - "diff", - "ena", - "itertools", - "lalrpop-util", - "petgraph", - "pico-args", - "regex", - "regex-syntax", - "string_cache", - "term", - "tiny-keccak", - "unicode-xid", -] - -[[package]] -name = "lalrpop-util" -version = "0.19.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e58cce361efcc90ba8a0a5f982c741ff86b603495bb15a998412e957dcd278" -dependencies = [ - "regex", -] - [[package]] name = "lazy_static" version = "1.4.0" @@ -443,60 +273,6 @@ version = "0.2.112" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" -[[package]] -name = "lock_api" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "logos" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "427e2abca5be13136da9afdbf874e6b34ad9001dd70f2b103b083a85daa7b345" -dependencies = [ - "logos-derive", -] - -[[package]] -name = "logos-derive" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56a7d287fd2ac3f75b11f19a1c8a874a7d55744bd91f7a1b3e7cf87d4343c36d" -dependencies = [ - "beef", - "fnv", - "proc-macro2", - "quote", - "regex-syntax", - "syn", - "utf8-ranges", -] - -[[package]] -name = "memchr" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" - -[[package]] -name = "new_debug_unreachable" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" - [[package]] name = "num-bigint" version = "0.4.3" @@ -506,6 +282,7 @@ dependencies = [ "autocfg", "num-integer", "num-traits", + "serde", ] [[package]] @@ -529,54 +306,23 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.5.6" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "720d3ea1055e4e4574c0c0b0f8c3fd4f24c4cdaf465948206dea090b57b526ad" +checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1" dependencies = [ "num_enum_derive", ] [[package]] name = "num_enum_derive" -version = "0.5.6" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d992b768490d7fe0d8586d9b5745f6c49f557da6d81dc982b1d167ad4edbb21" +checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", -] - -[[package]] -name = "opaque-debug" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" - -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall", - "smallvec", - "winapi", + "syn 2.0.38", ] [[package]] @@ -585,45 +331,15 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" -[[package]] -name = "petgraph" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7" -dependencies = [ - "fixedbitset", - "indexmap", -] - -[[package]] -name = "phf_shared" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pico-args" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468" - -[[package]] -name = "precomputed-hash" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" - [[package]] name = "pretty" -version = "0.10.0" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad9940b913ee56ddd94aec2d3cd179dd47068236f42a1a6415ccf9d880ce2a61" +checksum = "b55c4d17d994b637e2f4daf6e5dc5d660d209d5642377d675d7a1c3ab69fa579" dependencies = [ "arrayvec", "typed-arena", + "unicode-width", ] [[package]] @@ -638,58 +354,31 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.60" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] [[package]] -name = "quote" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "redox_syscall" -version = "0.2.10" +name = "psm" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" dependencies = [ - "bitflags", + "cc", ] [[package]] -name = "redox_users" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" -dependencies = [ - "getrandom", - "redox_syscall", -] - -[[package]] -name = "regex" -version = "1.5.5" +name = "quote" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", + "proc-macro2", ] -[[package]] -name = "regex-syntax" -version = "0.6.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" - [[package]] name = "rustversion" version = "1.0.6" @@ -702,17 +391,11 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" -[[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - [[package]] name = "serde" -version = "1.0.133" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a" +checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" dependencies = [ "serde_derive", ] @@ -728,20 +411,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.133" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed201699328568d8d08208fdd080e3ff594e6c422e438b6705905da01005d537" +checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.38", ] [[package]] name = "serde_json" -version = "1.0.75" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c059c05b48c5c0067d4b4b2b4f0732dd65feb52daf7e0ea09cd87e7dadc1af79" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa", "ryu", @@ -756,45 +439,31 @@ checksum = "d6deb15c3a535e81438110111d90168d91721652f502abb147f31cde129f683d" dependencies = [ "proc-macro2", "serde", - "syn", + "syn 1.0.85", ] [[package]] name = "sha2" -version = "0.9.9" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ - "block-buffer", "cfg-if", "cpufeatures", "digest", - "opaque-debug", ] [[package]] -name = "siphasher" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a86232ab60fa71287d7f2ddae4a7073f6b7aac33631c3015abb556f08c6d0a3e" - -[[package]] -name = "smallvec" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" - -[[package]] -name = "string_cache" -version = "0.8.2" +name = "stacker" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "923f0f39b6267d37d23ce71ae7235602134b250ace715dd2c90421998ddac0c6" +checksum = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce" dependencies = [ - "lazy_static", - "new_debug_unreachable", - "parking_lot", - "phf_shared", - "precomputed-hash", + "cc", + "cfg-if", + "libc", + "psm", + "winapi", ] [[package]] @@ -809,14 +478,14 @@ dependencies = [ ] [[package]] -name = "term" -version = "0.7.0" +name = "syn" +version = "2.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" dependencies = [ - "dirs-next", - "rustversion", - "winapi", + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] @@ -845,16 +514,7 @@ checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" dependencies = [ "proc-macro2", "quote", - "syn", -] - -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", + "syn 1.0.85", ] [[package]] @@ -896,24 +556,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" -[[package]] -name = "utf8-ranges" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ae116fef2b7fea257ed6440d3cfcff7f190865f170cdad00bb6465bf18ecba" - [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" -[[package]] -name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" - [[package]] name = "winapi" version = "0.3.9" diff --git a/motoko/encrypted-notes-dapp-vetkd/Makefile b/motoko/encrypted-notes-dapp-vetkd/Makefile index bb380b326..ad722ed16 100644 --- a/motoko/encrypted-notes-dapp-vetkd/Makefile +++ b/motoko/encrypted-notes-dapp-vetkd/Makefile @@ -1,4 +1,4 @@ -BUILD_ENV := motoko +BUILD_ENV ?= motoko .PHONY: test test-unit test-e2e @@ -30,6 +30,7 @@ test-e2e: echo "ENCRYPTED NOTES E2E TESTS PASSED" clean: + dfx stop rm -rf .dfx rm -rf node_modules rm -rf src/declarations diff --git a/motoko/encrypted-notes-dapp-vetkd/README.md b/motoko/encrypted-notes-dapp-vetkd/README.md index 3e6e372e9..dc14ec646 100644 --- a/motoko/encrypted-notes-dapp-vetkd/README.md +++ b/motoko/encrypted-notes-dapp-vetkd/README.md @@ -1,10 +1,10 @@ # Encrypted notes adapted for using vetKD -This is a copy of the encrypted-notes-dapp example, adapted to use [the proposed vetKD feature](https://github.com/dfinity/interface-spec/pull/158). +This is a copy of the encrypted-notes-dapp example, adapted to (1) use [the proposed vetKD feature](https://github.com/dfinity/interface-spec/pull/158) and (2) add sharing of notes between users. -In particular, instead of creating a principal-specific AES key and syncing it across devices (by means of device-specific RSA keys), the notes are encrypted with an AES key that is derived (directly in the browser) from a principal-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. +In particular, instead of creating a principal-specific AES key and syncing it across devices (by means of device-specific RSA keys), 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 difference between the original encrypted-notes-dapp and the this one here can be seen in https://github.com/dfinity/examples/pull/561. +The vetKey used to encrypt and decrypt a note is note-ID-specific (and not, for example, principal-specific) so as 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 they respective vetKey only has to be fetched from the server once. To improve the security even further, the vetKeys' derivation information could be adapted to include a (numeric) epoch that advances each time the list of users with which the note is shared is changed. Currently, the only way to use this dapp is via manual local deployment (see below). @@ -87,3 +87,7 @@ This example uses an [**insecure** implementation](../../rust/vetkd/src/system_a 2. Open the URL that is printed in the console output. Usually, this is [http://localhost:3000/](http://localhost:3000/). ⚠️ If you have opened this page previously, please remove all local store data for this page from your web browser, and hard-reload the page. For example in Chrome, go to Inspect → Application → Local Storage → `http://localhost:3000/` → Clear All, and then reload. + +## Troubleshooting + +If you run into issues, clearing all the application-specific IndexedDBs in the browser (which are used to store Internet Identity information and the derived non-extractable AES keys) might help fixing the issue. \ No newline at end of file diff --git a/motoko/encrypted-notes-dapp-vetkd/package-lock.json b/motoko/encrypted-notes-dapp-vetkd/package-lock.json index 82e788880..e468532da 100644 --- a/motoko/encrypted-notes-dapp-vetkd/package-lock.json +++ b/motoko/encrypted-notes-dapp-vetkd/package-lock.json @@ -33,6 +33,7 @@ "autoprefixer": "^10.4.2", "babel-jest": "^27.4.6", "daisyui": "^1.25.4", + "idb-keyval": "6.2.1", "jest": "^27.4.7", "postcss": "^8.4.5", "rollup": "^3.0.0", @@ -6468,6 +6469,12 @@ "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==" }, + "node_modules/idb-keyval": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz", + "integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==", + "dev": true + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -16080,6 +16087,12 @@ "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==" }, + "idb-keyval": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz", + "integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==", + "dev": true + }, "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", diff --git a/motoko/encrypted-notes-dapp-vetkd/package.json b/motoko/encrypted-notes-dapp-vetkd/package.json index 98d8b492b..1f561145d 100644 --- a/motoko/encrypted-notes-dapp-vetkd/package.json +++ b/motoko/encrypted-notes-dapp-vetkd/package.json @@ -36,6 +36,7 @@ "autoprefixer": "^10.4.2", "babel-jest": "^27.4.6", "daisyui": "^1.25.4", + "idb-keyval": "6.2.1", "jest": "^27.4.7", "postcss": "^8.4.5", "rollup": "^3.0.0", diff --git a/motoko/encrypted-notes-dapp-vetkd/pre_deploy.sh b/motoko/encrypted-notes-dapp-vetkd/pre_deploy.sh index 9989ebdcb..1608a9651 100755 --- a/motoko/encrypted-notes-dapp-vetkd/pre_deploy.sh +++ b/motoko/encrypted-notes-dapp-vetkd/pre_deploy.sh @@ -20,7 +20,7 @@ EOM else read -r -d '' BACKEND_TS <<- EOM export { idlFactory } from './idlFactory'; -export type { _SERVICE } from '../../../declarations/encrypted_notes_rust/encrypted_notes_rust.did.js'; +export * from '../../../declarations/encrypted_notes_rust/encrypted_notes_rust.did.js'; export const ENCRYPTED_NOTES_CANISTER_ID = process.env.ENCRYPTED_NOTES_RUST_CANISTER_ID; EOM fi diff --git a/motoko/encrypted-notes-dapp-vetkd/src/encrypted_notes_motoko/main.mo b/motoko/encrypted-notes-dapp-vetkd/src/encrypted_notes_motoko/main.mo index 712f8036d..f62b8afef 100644 --- a/motoko/encrypted-notes-dapp-vetkd/src/encrypted_notes_motoko/main.mo +++ b/motoko/encrypted-notes-dapp-vetkd/src/encrypted_notes_motoko/main.mo @@ -6,6 +6,7 @@ import List "mo:base/List"; import Iter "mo:base/Iter"; import Int "mo:base/Int"; import Nat "mo:base/Nat"; +import Nat8 "mo:base/Nat8"; import Bool "mo:base/Bool"; import Principal "mo:base/Principal"; import Result "mo:base/Result"; @@ -13,40 +14,46 @@ import Option "mo:base/Option"; import Debug "mo:base/Debug"; import Order "mo:base/Order"; import Blob "mo:base/Blob"; +import Hash "mo:base/Hash"; import Hex "./utils/Hex"; // Declare a shared actor class // Bind the caller and the initializer -shared({ caller = initializer }) actor class() { +shared ({ caller = initializer }) actor class () { - // Currently, a single canister smart contract is limited to 4 GB of storage due to WebAssembly limitations. - // To ensure that our canister does not exceed this limit, we restrict memory usage to at most 2 GB because - // up to 2x memory may be needed for data serialization during canister upgrades. Therefore, we aim to support - // up to 1,000 users, each storing up to 2 MB of data. - // The data is reserved for storing the notes: - // NOTES_PER_USER = MAX_NOTES_PER_USER x MAX_NOTE_CHARS x (4 bytes per char) - // 2 MB = 500 x 1000 x 4 = 2,000,000 + // 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. + // 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 - private let MAX_USERS = 1_000; - private let MAX_NOTES_PER_USER = 500; + private let MAX_USERS = 500; + private let MAX_NOTES_PER_USER = 200; private let MAX_NOTE_CHARS = 1000; + private let MAX_SHARES_PER_NOTE = 50; - // Define private types private type PrincipalName = Text; + private type NoteId = Nat; // Define public types // Type of an encrypted note - // Attention: This canister does *not* perform any encryption. + // Attention: This canister does *not* perform any encryption. // Here we assume that the notes are encrypted end- - // to-end by the front-end (at client side). + // to-end by the front-end (at client side). public type EncryptedNote = { - encrypted_text: Text; - id: Nat; + encrypted_text : Text; + id : Nat; + owner : PrincipalName; + // Principals with whom this note is shared. Does not include the owner. + // Needed to be able to efficiently show in the UI with whom this note is shared. + users : [PrincipalName]; }; // Define private fields - // Stable actor fields are automatically retained across canister upgrades. + // Stable actor fields are automatically retained across canister upgrades. // See https://internetcomputer.org/docs/current/motoko/main/upgrades/ // Design choice: Use globally unique note identifiers for all users. @@ -54,219 +61,321 @@ shared({ caller = initializer }) actor class() { // The keyword `stable` makes this (scalar) variable keep its value across canister upgrades. // // See https://internetcomputer.org/docs/current/developer-docs/setup/manage-canisters#upgrade-a-canister - private stable var nextNoteId: Nat = 1; - - // Internal representation: store each user's notes in a separate List. - private var notesByUser = Map.HashMap>(0, Text.equal, Text.hash); - - // While accessing data via [notesByUser] is more efficient, we use the following stable array - // as a buffer to preserve user notes across canister upgrades. + private stable var nextNoteId : Nat = 1; + + // Store notes by their ID, so that note-specific encryption keys can be derived. + private var notesById = Map.HashMap(0, Nat.equal, Hash.hash); + // Store which note IDs are owned by a particular principal + private var noteIdsByOwner = Map.HashMap>(0, Text.equal, Text.hash); + // Store which notes are shared with a particular principal. Does not include the owner, as this is tracked by `noteIdsByOwner`. + private var noteIdsByUser = Map.HashMap>(0, Text.equal, Text.hash); + + // 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 stable var stable_notesByUser: [(PrincipalName, List.List)] = []; - - // Returns the current number of users. - // Traps if [users_invariant] is violated - private func user_count(): Nat { - notesByUser.size() - }; - - // Check that a note identifier is sane. This is needed since Motoko integers - // are infinite-precision. - // Note: avoid extraneous usage of async functions, hence [user_count] - private func is_id_sane(id: Nat): Bool { - 0 <= id and id < MAX_NOTES_PER_USER * user_count() - }; + private stable var stable_notesById : [(NoteId, EncryptedNote)] = []; + private stable var stable_noteIdsByOwner : [(PrincipalName, List.List)] = []; + private stable var stable_noteIdsByUser : [(PrincipalName, List.List)] = []; // Utility function that helps writing assertion-driven code more concisely. - private func expect(opt: ?T, violation_msg: Text): T { + private func expect(opt : ?T, violation_msg : Text) : T { switch (opt) { case (null) { Debug.trap(violation_msg); }; case (?x) { - x + x; }; }; }; - // Reflects the [caller]'s identity by returning (a future of) its principal. - // Useful for debugging. - public shared({ caller }) func whoami(): async Text { + private func is_authorized(user : PrincipalName, note : EncryptedNote) : Bool { + user == note.owner or Option.isSome(Array.find(note.users, func(x : PrincipalName) : Bool { x == user })); + }; + + public shared ({ caller }) func whoami() : async Text { return Principal.toText(caller); }; - // Shared functions, i.e., those specified with [shared], are - // accessible to remote callers. + // 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 - // Add new note for this [caller]. Note: this function may be called only by - // those users that have at least one device registered via [register_device]. - // [encrypted_text]: (encrypted) content of this note + // Add new empty note for this [caller]. // - // Returns: - // Future of unit - // Traps: + // Returns: + // Future of ID of new empty note + // Traps: // [caller] is the anonymous identity - // [encrypted_text] exceeds [MAX_NOTE_CHARS] - // User already has [MAX_NOTES_PER_USER] notes - // [encrypted_text] would be for a new user and [MAX_USERS] is exceeded - public shared({ caller }) func add_note(encrypted_text: Text): async () { + // [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 { assert not Principal.isAnonymous(caller); - assert encrypted_text.size() <= MAX_NOTE_CHARS; + let owner = Principal.toText(caller); - Debug.print("Adding note..."); - - let principalName = Principal.toText(caller); - let userNotes : List.List = Option.get(notesByUser.get(principalName), List.nil()); - - if (List.isNil(userNotes)) { - // user didn't have notes yet, so this is a new user: check that user is not going to exceed limits - Debug.print("new user: #" # Nat.toText(user_count())); - assert user_count() < MAX_USERS; + let newNote : EncryptedNote = { + id = nextNoteId; + encrypted_text = ""; + owner = owner; + users = []; }; - // check that user is not going to exceed limits - assert List.size(userNotes) < MAX_NOTES_PER_USER; - - let newNote: EncryptedNote = { - id = nextNoteId; - encrypted_text = encrypted_text + switch (noteIdsByOwner.get(owner)) { + case (?owner_nids) { + assert List.size(owner_nids) < MAX_NOTES_PER_USER; + noteIdsByOwner.put(owner, List.push(newNote.id, owner_nids)); + }; + case null { + assert noteIdsByOwner.size() < MAX_USERS; + noteIdsByOwner.put(owner, List.make(newNote.id)); + }; }; + + notesById.put(newNote.id, newNote); nextNoteId += 1; - notesByUser.put(principalName, List.push(newNote, userNotes)); + newNote.id; }; // Returns (a future of) this [caller]'s notes. - // + // // --- Queries vs. Updates --- // 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 - // 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 + // 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 + // 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 // use an approach in which both queries and updates are combined.) // See https://internetcomputer.org/docs/current/concepts/canisters-code#query-and-update-methods // - // Returns: + // Returns: // Future of array of EncryptedNote - // Traps: + // Traps: // [caller] is the anonymous identity - public shared({ caller }) func get_notes(): async [EncryptedNote] { + public shared ({ caller }) func get_notes() : async [EncryptedNote] { assert not Principal.isAnonymous(caller); - - let principalName = Principal.toText(caller); - let userNotes = Option.get(notesByUser.get(principalName), List.nil()); - return List.toArray(userNotes); + let user = Principal.toText(caller); + + let owned_notes = List.map( + Option.get(noteIdsByOwner.get(user), List.nil()), + func(nid : NoteId) : EncryptedNote { + expect(notesById.get(nid), "missing note with ID " # Nat.toText(nid)); + }, + ); + let shared_notes = List.map( + Option.get(noteIdsByUser.get(user), List.nil()), + func(nid : NoteId) : EncryptedNote { + expect(notesById.get(nid), "missing note with ID " # Nat.toText(nid)); + }, + ); + + let buf = Buffer.Buffer(List.size(owned_notes) + List.size(shared_notes)); + buf.append(Buffer.fromArray(List.toArray(owned_notes))); + buf.append(Buffer.fromArray(List.toArray(shared_notes))); + Buffer.toArray(buf); }; - // Update this [caller]'s note (by replacing an existing with - // the same id). If none of the existing notes have this id, - // do nothing. - // [encrypted_note]: the note to be updated + // Replaces the encrypted text of note with ID [id] with [encrypted_text]. // - // Returns: + // Returns: // Future of unit - // Traps: - // [caller] is the anonymous identity - // [encrypted_note.encrypted_text] exceeds [MAX_NOTE_CHARS] - // [encrypted_note.id] is unreasonable; see [is_id_sane] - public shared({ caller }) func update_note(encrypted_note: EncryptedNote): async () { + // Traps: + // [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 () { assert not Principal.isAnonymous(caller); - assert encrypted_note.encrypted_text.size() <= MAX_NOTE_CHARS; - assert is_id_sane(encrypted_note.id); - - let principalName = Principal.toText(caller); - var existingNotes = expect(notesByUser.get(principalName), - "registered user (principal " # principalName # ") w/o allocated notes"); - - var updatedNotes = List.map(existingNotes, func (note: EncryptedNote): EncryptedNote { - if (note.id == encrypted_note.id) { - encrypted_note - } else { - note - } - }); - notesByUser.put(principalName, updatedNotes); + let caller_text = Principal.toText(caller); + let (?note_to_update) = notesById.get(id) else Debug.trap("note with id " # Nat.toText(id) # "not found"); + if (not is_authorized(caller_text, note_to_update)) { + Debug.trap("unauthorized"); + }; + assert note_to_update.encrypted_text.size() <= MAX_NOTE_CHARS; + notesById.put(id, { note_to_update with encrypted_text }); }; - // Delete this [caller]'s note with given id. If none of the - // existing notes have this id, do nothing. - // [id]: the id of the note to be deleted + // Shares the note with ID [note_id] with the [user]. + // Has no effect if the note is already shared with that user. // - // Returns: + // Returns: // Future of unit - // Traps: - // [caller] is the anonymous identity - // [id] is unreasonable; see [is_id_sane] - public shared({ caller }) func delete_note(id: Nat): async () { + // Traps: + // [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 () { assert not Principal.isAnonymous(caller); - assert is_id_sane(id); + let caller_text = Principal.toText(caller); + let (?note) = notesById.get(note_id) else Debug.trap("note with id " # Nat.toText(note_id) # "not found"); + if (caller_text != note.owner) { + Debug.trap("unauthorized"); + }; + assert note.users.size() < MAX_SHARES_PER_NOTE; + if (not Option.isSome(Array.find(note.users, func(u : PrincipalName) : Bool { u == user }))) { + let users_buf = Buffer.fromArray(note.users); + users_buf.add(user); + let updated_note = { note with users = Buffer.toArray(users_buf) }; + notesById.put(note_id, updated_note); + }; + switch (noteIdsByUser.get(user)) { + case (?user_nids) { + if (not List.some(user_nids, func(nid : NoteId) : Bool { nid == note_id })) { + noteIdsByUser.put(user, List.push(note_id, user_nids)); + }; + }; + case null { + noteIdsByUser.put(user, List.make(note_id)); + }; + }; + }; - let principalName = Principal.toText(caller); - var notesOfUser = Option.get(notesByUser.get(principalName), List.nil()); + // Unshares the note with ID [note_id] with the [user]. + // Has no effect if the note is already shared with that user. + // + // Returns: + // Future of unit + // Traps: + // [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 () { + assert not Principal.isAnonymous(caller); + let caller_text = Principal.toText(caller); + let (?note) = notesById.get(note_id) else Debug.trap("note with id " # Nat.toText(note_id) # "not found"); + if (caller_text != note.owner) { + Debug.trap("unauthorized"); + }; + let users_buf = Buffer.fromArray(note.users); + users_buf.filterEntries(func(i : Nat, u : PrincipalName) : Bool { u != user }); + let updated_note = { note with users = Buffer.toArray(users_buf) }; + notesById.put(note_id, updated_note); + + switch (noteIdsByUser.get(user)) { + case (?user_nids) { + let updated_nids = List.filter(user_nids, func(nid : NoteId) : Bool { nid != note_id }); + if (not List.isNil(updated_nids)) { + noteIdsByUser.put(user, updated_nids); + } else { + let _ = noteIdsByUser.remove(user); + }; + }; + case null {}; + }; + }; - notesByUser.put( - principalName, - List.filter(notesOfUser, func(note: EncryptedNote): Bool { note.id != id }) - ) + // Delete the note with ID [id]. + // + // Returns: + // Future of unit + // Traps: + // [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 () { + assert not Principal.isAnonymous(caller); + let caller_text = Principal.toText(caller); + let (?note_to_delete) = notesById.get(note_id) else Debug.trap("note with id " # Nat.toText(note_id) # "not found"); + let owner = note_to_delete.owner; + if (owner != caller_text) { + Debug.trap("unauthorized"); + }; + switch (noteIdsByOwner.get(owner)) { + case (?owner_nids) { + let updated_nids = List.filter(owner_nids, func(nid : NoteId) : Bool { nid != note_id }); + if (not List.isNil(updated_nids)) { + noteIdsByOwner.put(owner, updated_nids); + } else { + let _ = noteIdsByOwner.remove(owner); + }; + }; + case null {}; + }; + for (user in note_to_delete.users.vals()) { + switch (noteIdsByUser.get(user)) { + case (?user_nids) { + let updated_nids = List.filter(user_nids, func(nid : NoteId) : Bool { nid != note_id }); + if (not List.isNil(updated_nids)) { + noteIdsByUser.put(user, updated_nids); + } else { + let _ = noteIdsByUser.remove(user); + }; + }; + case null {}; + }; + }; + let _ = notesById.remove(note_id); }; - // Only the ecdsa methods in the IC management canister is required here. + // Only the vetKD methods in the IC management canister are required here. type VETKD_SYSTEM_API = actor { vetkd_public_key : ({ canister_id : ?Principal; derivation_path : [Blob]; - key_id : { curve: { #bls12_381; } ; name: Text }; - }) -> async ({ public_key : Blob; }); + key_id : { curve : { #bls12_381 }; name : Text }; + }) -> async ({ public_key : Blob }); vetkd_encrypted_key : ({ public_key_derivation_path : [Blob]; derivation_id : Blob; - key_id : { curve: { #bls12_381; } ; name: Text }; + key_id : { curve : { #bls12_381 }; name : Text }; encryption_public_key : Blob; }) -> async ({ encrypted_key : Blob }); }; - let vetkd_system_api : VETKD_SYSTEM_API = actor("s55qq-oqaaa-aaaaa-aaakq-cai"); + let vetkd_system_api : VETKD_SYSTEM_API = actor ("s55qq-oqaaa-aaaaa-aaakq-cai"); - public shared({ caller }) func app_vetkd_public_key(derivation_path: [Blob]): async Text { + public shared ({ caller }) func symmetric_key_verification_key_for_note() : async Text { let { public_key } = await vetkd_system_api.vetkd_public_key({ canister_id = null; - derivation_path; + derivation_path = Array.make(Text.encodeUtf8("note_symmetric_key")); key_id = { curve = #bls12_381; name = "test_key_1" }; }); - Hex.encode(Blob.toArray(public_key)) + Hex.encode(Blob.toArray(public_key)); }; - public shared({ caller }) func symmetric_key_verification_key(): async Text { - let { public_key } = await vetkd_system_api.vetkd_public_key({ - canister_id = null; - derivation_path = Array.make(Text.encodeUtf8("symmetric_key")); - key_id = { curve = #bls12_381; name = "test_key_1" }; - }); - Hex.encode(Blob.toArray(public_key)) - }; + public shared ({ caller }) func encrypted_symmetric_key_for_note(note_id : NoteId, encryption_public_key : Blob) : async Text { + let caller_text = Principal.toText(caller); + let (?note) = notesById.get(note_id) else Debug.trap("note with id " # Nat.toText(note_id) # "not found"); + if (not is_authorized(caller_text, note)) { + Debug.trap("unauthorized"); + }; + + let buf = Buffer.Buffer(32); + buf.append(Buffer.fromArray(natToBigEndianByteArray(16, note_id))); // fixed-size encoding + buf.append(Buffer.fromArray(Blob.toArray(Text.encodeUtf8(note.owner)))); + let derivation_id = Blob.fromArray(Buffer.toArray(buf)); // prefix-free - public shared ({ caller }) func encrypted_symmetric_key_for_caller(encryption_public_key : Blob) : async Text { - let caller_blob = Principal.toBlob(caller); let { encrypted_key } = await vetkd_system_api.vetkd_encrypted_key({ - derivation_id = Principal.toBlob(caller); - public_key_derivation_path = Array.make(Text.encodeUtf8("symmetric_key")); + derivation_id; + public_key_derivation_path = Array.make(Text.encodeUtf8("note_symmetric_key")); key_id = { curve = #bls12_381; name = "test_key_1" }; encryption_public_key; }); Hex.encode(Blob.toArray(encrypted_key)); }; + // Converts a nat to a fixed-size big-endian byte (Nat8) array + private func natToBigEndianByteArray(len : Nat, n : Nat) : [Nat8] { + let ith_byte = func(i : Nat) : Nat8 { + assert (i < len); + let shift : Nat = 8 * (len - 1 - i); + Nat8.fromIntWrap(n / 2 ** shift); + }; + Array.tabulate(len, ith_byte); + }; + // 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. - // See [nextNoteId], [stable_notesByUser] system func preupgrade() { Debug.print("Starting pre-upgrade hook..."); - stable_notesByUser := Iter.toArray(notesByUser.entries()); + stable_notesById := Iter.toArray(notesById.entries()); + stable_noteIdsByOwner := Iter.toArray(noteIdsByOwner.entries()); + stable_noteIdsByUser := Iter.toArray(noteIdsByUser.entries()); Debug.print("pre-upgrade finished."); }; @@ -274,10 +383,31 @@ shared({ caller = initializer }) actor class() { // See [nextNoteId], [stable_notesByUser] system func postupgrade() { Debug.print("Starting post-upgrade hook..."); - notesByUser := Map.fromIter>( - stable_notesByUser.vals(), stable_notesByUser.size(), Text.equal, Text.hash); - stable_notesByUser := []; + notesById := Map.fromIter( + stable_notesById.vals(), + stable_notesById.size(), + Nat.equal, + Hash.hash, + ); + stable_notesById := []; + + noteIdsByOwner := Map.fromIter>( + stable_noteIdsByOwner.vals(), + stable_noteIdsByOwner.size(), + Text.equal, + Text.hash, + ); + stable_noteIdsByOwner := []; + + noteIdsByUser := Map.fromIter>( + stable_noteIdsByUser.vals(), + stable_noteIdsByUser.size(), + Text.equal, + Text.hash, + ); + stable_noteIdsByUser := []; + Debug.print("post-upgrade finished."); }; }; diff --git a/motoko/encrypted-notes-dapp-vetkd/src/encrypted_notes_rust/Cargo.toml b/motoko/encrypted-notes-dapp-vetkd/src/encrypted_notes_rust/Cargo.toml index d6f9096a8..1d6853c9f 100644 --- a/motoko/encrypted-notes-dapp-vetkd/src/encrypted_notes_rust/Cargo.toml +++ b/motoko/encrypted-notes-dapp-vetkd/src/encrypted_notes_rust/Cargo.toml @@ -9,11 +9,13 @@ crate-type = ["cdylib"] [dependencies] -ic-cdk = "0.3.3" -ic-cdk-macros = "0.3" -lazy_static = "1.4.*" -serde_json = "1.0.74" -anyhow = "1.0.43" -serde = "1.0.133" -ic-types = "0.3.0" -hex = "0.4" +candid = "0.9.11" +ic-cdk = "0.11.3" +ic-cdk-macros = "0.8.1" +ic-stable-structures = "0.6.0" +lazy_static = "1.4.0" +serde_json = "1.0.108" +anyhow = "1.0.75" +serde = "1.0.190" +ic-types = "0.7.0" +hex = "0.4.3" diff --git a/motoko/encrypted-notes-dapp-vetkd/src/encrypted_notes_rust/src/encrypted_notes_rust.did b/motoko/encrypted-notes-dapp-vetkd/src/encrypted_notes_rust/src/encrypted_notes_rust.did index 5e2469f17..1dddd8af5 100644 --- a/motoko/encrypted-notes-dapp-vetkd/src/encrypted_notes_rust/src/encrypted_notes_rust.did +++ b/motoko/encrypted-notes-dapp-vetkd/src/encrypted_notes_rust/src/encrypted_notes_rust.did @@ -1,17 +1,20 @@ type anon_class_15_1 = service { - add_note: (text) -> (); + create_note: () -> (nat); delete_note: (nat) -> (); get_notes: () -> (vec EncryptedNote); - update_note: (EncryptedNote) -> (); + update_note: (nat, text) -> (); + add_user: (nat, text) -> (); + remove_user: (nat, text) -> (); whoami: () -> (text); - app_vetkd_public_key: (vec blob) -> (text); - symmetric_key_verification_key: () -> (text); - encrypted_symmetric_key_for_caller: (blob) -> (text); + symmetric_key_verification_key_for_note: () -> (text); + encrypted_symmetric_key_for_note: (nat, blob) -> (text); }; type EncryptedNote = record { encrypted_text: text; id: nat; + owner: text; + users: vec text; }; service : () -> anon_class_15_1 diff --git a/motoko/encrypted-notes-dapp-vetkd/src/encrypted_notes_rust/src/lib.rs b/motoko/encrypted-notes-dapp-vetkd/src/encrypted_notes_rust/src/lib.rs index 4965eb753..ff4a708c6 100644 --- a/motoko/encrypted-notes-dapp-vetkd/src/encrypted_notes_rust/src/lib.rs +++ b/motoko/encrypted-notes-dapp-vetkd/src/encrypted_notes_rust/src/lib.rs @@ -1,65 +1,110 @@ -use candid::CandidType; -use ic_cdk::api::caller as caller_api; -use ic_cdk::export::{candid, Principal}; -use ic_cdk::storage; +use candid::{CandidType, Decode, Deserialize, Encode, Principal}; use ic_cdk_macros::*; -use serde::{Deserialize, Serialize}; +use ic_stable_structures::memory_manager::{MemoryId, MemoryManager, VirtualMemory}; +use ic_stable_structures::{ + storable::Bound, DefaultMemoryImpl, StableBTreeMap, StableCell, Storable, +}; +use std::borrow::Cow; use std::cell::RefCell; -use std::collections::BTreeMap; -use std::mem; type PrincipalName = String; +type Memory = VirtualMemory; +type NoteId = u128; -/// Deriving CandidType or implementing it is necessary for -/// almost everything IC - if you want your structs to -/// Save in stable storage or serialize in inputs/outputs -/// You should derive CandidType, Serialize, Deserialize. -#[derive(Clone, CandidType, Serialize, Deserialize)] +#[derive(Clone, Debug, CandidType, Deserialize, Eq, PartialEq)] pub struct EncryptedNote { - id: u128, + id: NoteId, encrypted_text: String, + owner: PrincipalName, + /// Principals with whom this note is shared. Does not include the owner. + /// Needed to be able to efficiently show in the UI with whom this note is shared. + users: Vec, } -/// There can only be one Type in stable storage at a time. -/// We use this struct to represent the full CanisterState -/// So we can serialize it to stable storage. -#[derive(Clone, CandidType, Serialize, Deserialize)] -struct CanisterState { - // During canister upgrades, this field contains a stable representation of the value stored in [NEXT_NOTE] - counter: u128, - // We use a BTreeMap vice a HashMap for deterministic ordering. - notes: BTreeMap>, +impl EncryptedNote { + pub fn is_authorized(&self, user: &PrincipalName) -> bool { + user == &self.owner || self.users.contains(user) + } } -// WASM is single-threaded by nature. [RefCell] and [thread_local!] are used despite being not totally safe primitives. -// This is to ensure that the canister state can be used throughout. -// Your other option here is to avoid [thread_local!] and use a [RefCell]. -// Here we use [thread_local!] because it is simpler. -thread_local! { +impl Storable for EncryptedNote { + fn to_bytes(&self) -> Cow<[u8]> { + Cow::Owned(Encode!(self).unwrap()) + } + fn from_bytes(bytes: Cow<[u8]>) -> Self { + Decode!(bytes.as_ref(), Self).unwrap() + } + const BOUND: Bound = Bound::Unbounded; +} - // Currently, a single canister smart contract is limited to 4 GB of storage due to WebAssembly limitations. - // To ensure that our canister does not exceed this limit, we restrict memory usage to at most 2 GB because - // up to 2x memory may be needed for data serialization during canister upgrades. Therefore, we aim to support - // up to 1,000 users, each storing up to 2 MB of data. - // The data is reserved for storing the notes: - // NOTES_PER_USER = MAX_NOTES_PER_USER x MAX_NOTE_CHARS x (4 bytes per char) - // 2 MB = 500 x 1000 x 4 = 2,000,000 - - // Define dapp limits - important for security assurance - static MAX_USERS: usize = 1_000; - static MAX_NOTES_PER_USER: usize = 500; - static MAX_NOTE_CHARS: usize = 1000; - - pub static NEXT_NOTE: RefCell = RefCell::new(1); - pub static NOTES_BY_USER: RefCell>> = RefCell::new(BTreeMap::new()); +#[derive(CandidType, Deserialize, Default)] +pub struct NoteIds { + ids: Vec, +} + +impl NoteIds { + pub fn iter(&self) -> impl std::iter::Iterator { + self.ids.iter() + } +} + +impl Storable for NoteIds { + fn to_bytes(&self) -> Cow<[u8]> { + Cow::Owned(Encode!(self).unwrap()) + } + fn from_bytes(bytes: Cow<[u8]>) -> Self { + Decode!(bytes.as_ref(), Self).unwrap() + } + const BOUND: Bound = Bound::Unbounded; +} + +// 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. +// 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; +static MAX_NOTES_PER_USER: usize = 500; +static MAX_NOTE_CHARS: usize = 1000; +static MAX_SHARES_PER_NOTE: usize = 50; + +thread_local! { + static MEMORY_MANAGER: RefCell> = + RefCell::new(MemoryManager::init(DefaultMemoryImpl::default())); + + static NEXT_NOTE_ID: RefCell> = RefCell::new( + StableCell::init( + MEMORY_MANAGER.with_borrow(|m| m.get(MemoryId::new(0))), + 1 + ).expect("failed to init NEXT_NOTE_ID") + ); + + static NOTES: RefCell> = RefCell::new( + StableBTreeMap::init( + MEMORY_MANAGER.with_borrow(|m| m.get(MemoryId::new(1))), + ) + ); + + static NOTE_OWNERS: RefCell> = RefCell::new( + StableBTreeMap::init( + MEMORY_MANAGER.with_borrow(|m| m.get(MemoryId::new(2))), + ) + ); + + static NOTE_SHARES: RefCell> = RefCell::new( + StableBTreeMap::init( + MEMORY_MANAGER.with_borrow(|m| m.get(MemoryId::new(3))), + ) + ); } /// Unlike Motoko, the caller identity is not built into Rust. -/// Thus, we use the ic_cdk::api::caller() method inside this wrapper function. +/// Thus, we use the ic_cdk::caller() method inside this wrapper function. /// The wrapper prevents the use of the anonymous identity. Forbidding anonymous /// interactions is the recommended default behavior for IC canisters. fn caller() -> Principal { - let caller = caller_api(); + let caller = ic_cdk::caller(); // The anonymous principal is not allowed to interact with the // encrypted notes canister. if caller == Principal::anonymous() { @@ -68,9 +113,6 @@ fn caller() -> Principal { caller } -#[init] -fn init() {} - /// --- Queries vs. Updates --- /// /// Note that our public methods are declared as an *updates* rather than *queries*, e.g.: @@ -88,41 +130,43 @@ fn init() {} /// Reflects the [caller]'s identity by returning (a future of) its principal. /// Useful for debugging. -#[update(name = "whoami")] +#[update] fn whoami() -> String { - caller_api().to_string() + ic_cdk::caller().to_string() } /// General assumptions /// ------------------- /// All the functions of this canister's public API should be available only to -/// registered users, with the exception of [register_device] and [whoami]. - -/// Returns the current number of users. -fn user_count() -> usize { - NOTES_BY_USER.with(|notes_ref| notes_ref.borrow().keys().len()) -} - -/// Check that a note identifier is sane. This is needed since we use finite- -/// precision integers (`u128`). -fn is_id_sane(id: u128) -> bool { - MAX_NOTES_PER_USER.with(|max_notes_per_user| id < (*max_notes_per_user as u128) * (user_count() as u128)) -} +/// registered users, with the exception of [whoami]. /// Returns (a future of) this [caller]'s notes. /// Panics: /// [caller] is the anonymous identity -/// [caller] is not a registered user -#[update(name = "get_notes")] +#[update] fn get_notes() -> Vec { - let user = caller(); - let user_str = user.to_string(); - NOTES_BY_USER.with(|notes_ref| { - notes_ref - .borrow() - .get(&user_str) - .cloned() - .unwrap_or_default() + let user_str = caller().to_string(); + NOTES.with_borrow(|notes| { + let owned = NOTE_OWNERS.with_borrow(|ids| { + ids.get(&user_str) + .unwrap_or_default() + .iter() + .map(|id| notes.get(id).ok_or(format!("missing note with ID {id}"))) + .collect::, _>>() + .unwrap_or_else(|err| ic_cdk::trap(&err)) + }); + let shared = NOTE_SHARES.with_borrow(|ids| { + ids.get(&user_str) + .unwrap_or_default() + .iter() + .map(|id| notes.get(id).ok_or(format!("missing note with ID {id}"))) + .collect::, _>>() + .unwrap_or_else(|err| ic_cdk::trap(&err)) + }); + let mut result = Vec::with_capacity(owned.len() + shared.len()); + result.extend(owned); + result.extend(shared); + result }) } @@ -134,137 +178,178 @@ fn get_notes() -> Vec { /// Future of unit /// Panics: /// [caller] is the anonymous identity -/// [caller] is not a registered user -/// [id] is unreasonable; see [is_id_sane] -#[update(name = "delete_note")] +/// [caller] is not the owner of note with id `note_id` +#[update] fn delete_note(note_id: u128) { - let user = caller(); - assert!(is_id_sane(note_id)); - - let user_str = user.to_string(); - // shared ownership borrowing - NOTES_BY_USER.with(|notes_ref| { - let mut writer = notes_ref.borrow_mut(); - if let Some(v) = writer.get_mut(&user_str) { - v.retain(|item| item.id != note_id); + let user_str = caller().to_string(); + NOTES.with_borrow_mut(|notes| { + if let Some(note_to_delete) = notes.get(¬e_id) { + let owner = ¬e_to_delete.owner; + if owner != &user_str { + ic_cdk::trap("only the owner can delete notes"); + } + NOTE_OWNERS.with_borrow_mut(|owner_to_nids| { + if let Some(mut owner_ids) = owner_to_nids.get(owner) { + owner_ids.ids.retain(|&id| id != note_id); + if !owner_ids.ids.is_empty() { + owner_to_nids.insert(owner.clone(), owner_ids); + } else { + owner_to_nids.remove(owner); + } + } + }); + NOTE_SHARES.with_borrow_mut(|share_to_nids| { + for share in note_to_delete.users { + if let Some(mut share_ids) = share_to_nids.get(&share) { + share_ids.ids.retain(|&id| id != note_id); + if !share_ids.ids.is_empty() { + share_to_nids.insert(share, share_ids); + } else { + share_to_nids.remove(&share); + } + } + } + }); + notes.remove(¬e_id); } }); } -/// Returns (a future of) this [caller]'s notes. +/// Replaces the encrypted text of note with ID [id] with [encrypted_text]. +/// /// Panics: /// [caller] is the anonymous identity -/// [caller] is not a registered user -/// [note.encrypted_text] exceeds [MAX_NOTE_CHARS] -/// [note.id] is unreasonable; see [is_id_sane] -#[update(name = "update_note")] -fn update_note(note: EncryptedNote) { - let user = caller(); - assert!(note.encrypted_text.chars().count() <= MAX_NOTE_CHARS.with(|mnc| *mnc)); - assert!(is_id_sane(note.id)); - - let user_str = user.to_string(); - NOTES_BY_USER.with(|notes_ref| { - let mut writer = notes_ref.borrow_mut(); - if let Some(old_note) = writer - .get_mut(&user_str) - .and_then(|notes| notes.iter_mut().find(|n| n.id == note.id)) - { - old_note.encrypted_text = note.encrypted_text; +/// [caller] is not the note's owner and not a user with whom the note is shared +/// [encrypted_text] exceeds [MAX_NOTE_CHARS] +#[update] +fn update_note(id: NoteId, encrypted_text: String) { + let user_str = caller().to_string(); + + NOTES.with_borrow_mut(|notes| { + if let Some(mut note_to_update) = notes.get(&id) { + if !note_to_update.is_authorized(&user_str) { + ic_cdk::trap("unauthorized update"); + } + assert!(encrypted_text.chars().count() <= MAX_NOTE_CHARS); + note_to_update.encrypted_text = encrypted_text; + notes.insert(id, note_to_update); } }) } -/// Add new note for this [caller]. -/// [note]: (encrypted) content of this note +/// Add new empty note for this [caller]. /// /// Returns: -/// Future of unit +/// Future of ID of new empty note /// Panics: /// [caller] is the anonymous identity -/// [caller] is not a registered user -/// [note] exceeds [MAX_NOTE_CHARS] /// User already has [MAX_NOTES_PER_USER] notes -/// [note] would be for a new user and [MAX_USERS] is exceeded -#[update(name = "add_note")] -fn add_note(note: String) { - let user = caller(); - assert!(note.chars().count() <= MAX_NOTE_CHARS.with(|mnc| *mnc)); - - let user_str = user.to_string(); - let note_id = NEXT_NOTE.with(|counter_ref| { - let mut writer = counter_ref.borrow_mut(); - *writer += 1; - *writer - }); - - let user_count = user_count(); - NOTES_BY_USER.with(|notes_ref| { - let mut writer = notes_ref.borrow_mut(); - let user_notes = writer.entry(user_str).or_insert_with(|| { - // caller unknown ==> check invariants - // A. can we add a new user? - assert!(MAX_USERS.with(|mu| user_count < *mu)); - vec![] - }); - - assert!(user_notes.len() < MAX_NOTES_PER_USER.with(|mnpu| *mnpu)); +/// This is the first note for [caller] and [MAX_USERS] is exceeded +#[update] +fn create_note() -> NoteId { + let owner = caller().to_string(); + + NOTES.with_borrow_mut(|id_to_note| { + NOTE_OWNERS.with_borrow_mut(|owner_to_nids| { + let next_note_id = NEXT_NOTE_ID.with_borrow(|id| *id.get()); + let new_note = EncryptedNote { + id: next_note_id, + owner: owner.clone(), + users: vec![], + encrypted_text: String::new(), + }; - user_notes.push(EncryptedNote { - id: note_id, - encrypted_text: note, - }); - }); + if let Some(mut owner_nids) = owner_to_nids.get(&owner) { + assert!(owner_nids.ids.len() < MAX_NOTES_PER_USER); + owner_nids.ids.push(new_note.id); + owner_to_nids.insert(owner, owner_nids); + } else { + assert!(owner_to_nids.len() < MAX_USERS); + owner_to_nids.insert( + owner, + NoteIds { + ids: vec![new_note.id], + }, + ); + } + assert_eq!(id_to_note.insert(new_note.id, new_note), None); + + NEXT_NOTE_ID.with_borrow_mut(|next_note_id| { + next_note_id + .set(next_note_id.get() + 1) + .unwrap_or_else(|_e| ic_cdk::trap("failed to set NEXT_NOTE_ID")) + }); + next_note_id + }) + }) } -/// Hooks in these macros will produce a `function already defined` error -/// if they share the same name as the underlying function. - -#[pre_upgrade] -/// The pre_upgrade hook determines anything your canister -/// should do before it goes offline for a code upgrade. -fn pre_upgrade() { - let copied_counter: u128 = NEXT_NOTE.with(|counter_ref| { - let reader = counter_ref.borrow(); - *reader - }); - NOTES_BY_USER.with(|notes_ref| { - let old_state = CanisterState { - notes: mem::take(&mut notes_ref.borrow_mut()), - counter: copied_counter, - }; - // storage::stable_save is the API used to write canister state out. - // More explicit error handling *can* be useful, but if we fail to read out/in stable memory on upgrade - // it means the data won't be accessible to the canister in any way. - storage::stable_save((old_state,)).unwrap(); +/// Shares the note with ID `note_id`` with the `user`. +/// Has no effect if the note is already shared with that user. +/// +/// Panics: +/// [caller] is the anonymous identity +/// [caller] is not the owner of note with id `note_id` +#[update] +fn add_user(note_id: NoteId, user: PrincipalName) { + let caller_str = caller().to_string(); + NOTES.with_borrow_mut(|notes| { + NOTE_SHARES.with_borrow_mut(|user_to_nids| { + if let Some(mut note) = notes.get(¬e_id) { + let owner = ¬e.owner; + if owner != &caller_str { + ic_cdk::trap("only the owner can share the note"); + } + assert!(note.users.len() < MAX_SHARES_PER_NOTE); + if !note.users.contains(&user) { + note.users.push(user.clone()); + notes.insert(note_id, note); + } + if let Some(mut user_ids) = user_to_nids.get(&user) { + if !user_ids.ids.contains(¬e_id) { + user_ids.ids.push(note_id); + user_to_nids.insert(user, user_ids); + } + } else { + user_to_nids.insert(user, NoteIds { ids: vec![note_id] }); + } + } + }) }); } -#[post_upgrade] -/// The post_upgrade hook determines anything your canister should do after it restarts -fn post_upgrade() { - // storage::stable_restore is how to read your canister state back in from stable memory - // Same thing with the unwrap here. For this canister there's nothing to do - // in the event of a memory read out/in failure. - let (old_state,): (CanisterState,) = storage::stable_restore().unwrap(); - NOTES_BY_USER.with(|notes_ref| { - NEXT_NOTE.with(|counter_ref| { - *notes_ref.borrow_mut() = old_state.notes; - *counter_ref.borrow_mut() = old_state.counter; +/// Unshares the note with ID `note_id`` with the `user`. +/// Has no effect if the note is not shared with that user. +/// +/// Panics: +/// [caller] is the anonymous identity +/// [caller] is not the owner of note with id `note_id` +#[update] +fn remove_user(note_id: NoteId, user: PrincipalName) { + let caller_str = caller().to_string(); + NOTES.with_borrow_mut(|notes| { + NOTE_SHARES.with_borrow_mut(|user_to_nids| { + if let Some(mut note) = notes.get(¬e_id) { + let owner = ¬e.owner; + if owner != &caller_str { + ic_cdk::trap("only the owner can share the note"); + } + note.users.retain(|u| u != &user); + notes.insert(note_id, note); + + if let Some(mut user_ids) = user_to_nids.get(&user) { + user_ids.ids.retain(|&id| id != note_id); + if !user_ids.ids.is_empty() { + user_to_nids.insert(user, user_ids); + } else { + user_to_nids.remove(&user); + } + } + } }) }); } -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_user_count_succeeds() { - assert_eq!(user_count(), 0); - } -} - mod vetkd_types; const VETKD_SYSTEM_API_CANISTER_ID: &str = "s55qq-oqaaa-aaaaa-aaakq-cai"; @@ -274,16 +359,15 @@ use vetkd_types::{ VetKDPublicKeyReply, VetKDPublicKeyRequest, }; -/// Results can be cached. #[update] -async fn app_vetkd_public_key(derivation_path: Vec>) -> String { +async fn symmetric_key_verification_key_for_note() -> String { let request = VetKDPublicKeyRequest { canister_id: None, - derivation_path, + derivation_path: vec![b"note_symmetric_key".to_vec()], key_id: bls12_381_test_key_1(), }; - let (response,): (VetKDPublicKeyReply,) = ic_cdk::api::call::call( + let (response,): (VetKDPublicKeyReply,) = ic_cdk::call( vetkd_system_api_canister_id(), "vetkd_public_key", (request,), @@ -295,34 +379,33 @@ async fn app_vetkd_public_key(derivation_path: Vec>) -> String { } #[update] -async fn symmetric_key_verification_key() -> String { - let request = VetKDPublicKeyRequest { - canister_id: None, - derivation_path: vec![b"symmetric_key".to_vec()], - key_id: bls12_381_test_key_1(), - }; - - let (response,): (VetKDPublicKeyReply,) = ic_cdk::api::call::call( - vetkd_system_api_canister_id(), - "vetkd_public_key", - (request,), - ) - .await - .expect("call to vetkd_public_key failed"); - - hex::encode(response.public_key) -} - -#[update] -async fn encrypted_symmetric_key_for_caller(encryption_public_key: Vec) -> String { - let request = VetKDEncryptedKeyRequest { - derivation_id: ic_cdk::caller().as_slice().to_vec(), - public_key_derivation_path: vec![b"symmetric_key".to_vec()], - key_id: bls12_381_test_key_1(), - encryption_public_key, - }; +async fn encrypted_symmetric_key_for_note( + note_id: NoteId, + encryption_public_key: Vec, +) -> String { + let user_str = caller().to_string(); + let request = NOTES.with_borrow(|notes| { + if let Some(note) = notes.get(¬e_id) { + if !note.is_authorized(&user_str) { + ic_cdk::trap(&format!("unauthorized key request by user {user_str}")); + } + VetKDEncryptedKeyRequest { + derivation_id: { + let mut buf = vec![]; + buf.extend_from_slice(¬e_id.to_be_bytes()); // fixed-size encoding + buf.extend_from_slice(note.owner.as_bytes()); + buf // prefix-free + }, + public_key_derivation_path: vec![b"note_symmetric_key".to_vec()], + key_id: bls12_381_test_key_1(), + encryption_public_key, + } + } else { + ic_cdk::trap(&format!("note with ID {note_id} does not exist")); + } + }); - let (response,): (VetKDEncryptedKeyReply,) = ic_cdk::api::call::call( + let (response,): (VetKDEncryptedKeyReply,) = ic_cdk::call( vetkd_system_api_canister_id(), "vetkd_encrypted_key", (request,), diff --git a/motoko/encrypted-notes-dapp-vetkd/src/encrypted_notes_rust/src/vetkd_types.rs b/motoko/encrypted-notes-dapp-vetkd/src/encrypted_notes_rust/src/vetkd_types.rs index 03ae4fc89..566761e31 100644 --- a/motoko/encrypted-notes-dapp-vetkd/src/encrypted_notes_rust/src/vetkd_types.rs +++ b/motoko/encrypted-notes-dapp-vetkd/src/encrypted_notes_rust/src/vetkd_types.rs @@ -1,6 +1,6 @@ -use ic_cdk::export::candid::CandidType; -use ic_cdk::export::serde::Deserialize; -use ic_cdk::export::Principal; +use candid::CandidType; +use candid::Deserialize; +use candid::Principal; pub type CanisterId = Principal; diff --git a/motoko/encrypted-notes-dapp-vetkd/src/frontend/src/components/EditNote.svelte b/motoko/encrypted-notes-dapp-vetkd/src/frontend/src/components/EditNote.svelte index 318182404..980cbe725 100644 --- a/motoko/encrypted-notes-dapp-vetkd/src/frontend/src/components/EditNote.svelte +++ b/motoko/encrypted-notes-dapp-vetkd/src/frontend/src/components/EditNote.svelte @@ -3,10 +3,11 @@ import type { CurrentRoute } from 'svelte-router-spa/types/components/route'; import { Editor, placeholder } from 'typewriter-editor'; import { extractTitle, NoteModel } from '../lib/note'; - import { notesStore, refreshNotes, updateNote } from '../store/notes'; + import { notesStore, refreshNotes, updateNote, addUser, removeUser } from '../store/notes'; import Header from './Header.svelte'; import NoteEditor from './NoteEditor.svelte'; import TagEditor from './TagEditor.svelte'; + import SharingEditor from './SharingEditor.svelte'; import Trash from 'svelte-icons/fa/FaTrash.svelte'; import { addNotification, showError } from '../store/notifications'; import { auth } from '../store/auth'; @@ -18,6 +19,7 @@ let editor: Editor; let updating = false; let deleting = false; + let ownedByMe; async function save() { if ($auth.state !== 'initialized') { @@ -78,6 +80,13 @@ editedNote.tags = editedNote.tags.filter((t) => t !== tag); } + function selfPrincipalString(): string { + if ($auth.state !== 'initialized') { + throw new Error('expected the auth.state to be initialized'); + } + return $auth.client.getIdentity().getPrincipal().toString(); + } + $: { if ($notesStore.state === 'loaded' && !editedNote) { const note = $notesStore.list.find( @@ -92,6 +101,7 @@ }, html: editedNote.content, }); + ownedByMe = note.owner == selfPrincipalString(); } } } @@ -102,7 +112,7 @@ Edit note +
+ {:else if $notesStore.state === 'loading'} Loading notes... {/if} diff --git a/motoko/encrypted-notes-dapp-vetkd/src/frontend/src/components/NewNote.svelte b/motoko/encrypted-notes-dapp-vetkd/src/frontend/src/components/NewNote.svelte index 2295c31be..34ac33d1c 100644 --- a/motoko/encrypted-notes-dapp-vetkd/src/frontend/src/components/NewNote.svelte +++ b/motoko/encrypted-notes-dapp-vetkd/src/frontend/src/components/NewNote.svelte @@ -26,7 +26,7 @@ } creating = true; await addNote( - noteFromContent(editor.getHTML(), tags), + noteFromContent(editor.getHTML(), tags, $auth.client.getIdentity().getPrincipal()), $auth.actor, $auth.crypto ) diff --git a/motoko/encrypted-notes-dapp-vetkd/src/frontend/src/components/SharingEditor.svelte b/motoko/encrypted-notes-dapp-vetkd/src/frontend/src/components/SharingEditor.svelte new file mode 100644 index 000000000..15fa4ed2f --- /dev/null +++ b/motoko/encrypted-notes-dapp-vetkd/src/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/encrypted-notes-dapp-vetkd/src/frontend/src/components/SidebarLayout.svelte b/motoko/encrypted-notes-dapp-vetkd/src/frontend/src/components/SidebarLayout.svelte index fc1727879..1dcbd41f7 100644 --- a/motoko/encrypted-notes-dapp-vetkd/src/frontend/src/components/SidebarLayout.svelte +++ b/motoko/encrypted-notes-dapp-vetkd/src/frontend/src/components/SidebarLayout.svelte @@ -1,5 +1,5 @@