diff --git a/.github/scripts/provision-linux-build.sh b/.github/scripts/provision-linux-build.sh new file mode 100755 index 00000000..c593aadb --- /dev/null +++ b/.github/scripts/provision-linux-build.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +sudo apt-get update +sudo apt-get install -y libdbus-1-dev dbus diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 1e91bc14..05c35cd0 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -19,7 +19,7 @@ env: # Use the local .curlrc CURL_HOME: . # Disable DFX telemetry - DFX_TELEMETRY: 'off' + DFX_TELEMETRY: "off" # Use the stable toolchain for the audit RUSTUP_TOOLCHAIN: stable @@ -33,4 +33,6 @@ jobs: steps: - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - name: Setup image (Linux) + run: ./.github/scripts/provision-linux-build.sh - uses: actions-rust-lang/audit@410bbe6de17ca06c0a60070cca18c88b485ca5a1 # v1.2.6 diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 4499038f..e1e9bf3c 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -14,6 +14,9 @@ jobs: steps: - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - name: Setup image (Linux) + run: ./.github/scripts/provision-linux-build.sh + - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: | @@ -38,6 +41,9 @@ jobs: steps: - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - name: Setup image (Linux) + run: ./.github/scripts/provision-linux-build.sh + - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: | @@ -62,6 +68,9 @@ jobs: steps: - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - name: Setup image (Linux) + run: ./.github/scripts/provision-linux-build.sh + - name: Install Rust toolchain from rust-toolchain.toml run: rustup show @@ -96,6 +105,9 @@ jobs: steps: - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - name: Setup image (Linux) + run: ./.github/scripts/provision-linux-build.sh + - name: Show Rust toolchain run: rustup show @@ -137,6 +149,9 @@ jobs: steps: - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - name: Setup image (Linux) + run: ./.github/scripts/provision-linux-build.sh + - name: Show Rust toolchain run: rustup show diff --git a/.github/workflows/deny.yml b/.github/workflows/deny.yml index 00f6ffac..41d44ee5 100644 --- a/.github/workflows/deny.yml +++ b/.github/workflows/deny.yml @@ -16,7 +16,7 @@ env: # Use the local .curlrc CURL_HOME: . # Disable DFX telemetry - DFX_TELEMETRY: 'off' + DFX_TELEMETRY: "off" jobs: cargo-deny: @@ -24,6 +24,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - name: Setup image (Linux) + run: ./.github/scripts/provision-linux-build.sh - run: rm rust-toolchain.toml - uses: EmbarkStudios/cargo-deny-action@f2ba7abc2abebaf185c833c3961145a3c275caad # v2.0.13 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1f071f3d..35d61087 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,6 +53,10 @@ jobs: run: | echo "RUSTFLAGS=--remap-path-prefix=${GITHUB_WORKSPACE}=/builds/dfinity" >> $GITHUB_ENV + - name: Setup image (Linux) + if: ${{ contains(matrix.os, 'ubuntu') }} + run: ./.github/scripts/provision-linux-build.sh + - name: Set names (tag only) if: github.ref_type == 'tag' run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 164f26e6..266fd784 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,6 +35,10 @@ jobs: steps: - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - name: Setup image (Linux) + if: ${{ contains(matrix.os, 'ubuntu') }} + run: ./.github/scripts/provision-linux-build.sh + - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: | @@ -46,6 +50,9 @@ jobs: target/ key: ${{ matrix.os }}-cargo-${{ hashFiles('rust-toolchain.toml') }}-${{ hashFiles('**/Cargo.lock') }} + - uses: t1m0thyj/unlock-keyring@e481cdc8833d4417a58f40734e8f197183317047 + if: ${{ contains(matrix.os, 'ubuntu') }} + - name: Run unit tests run: cargo test --workspace --lib --bins @@ -61,6 +68,10 @@ jobs: steps: - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + - name: Setup image (Linux) + if: ${{ contains(matrix.os, 'ubuntu') }} + run: ./.github/scripts/provision-linux-build.sh + - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: | @@ -72,6 +83,9 @@ jobs: target/ key: ${{ matrix.os }}-cargo-${{ hashFiles('rust-toolchain.toml') }}-${{ hashFiles('**/Cargo.lock') }} + - uses: t1m0thyj/unlock-keyring@e481cdc8833d4417a58f40734e8f197183317047 + if: ${{ contains(matrix.os, 'ubuntu') }} + - name: install network launcher env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 676a3fea..24af53a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased +* feat: Support keyring storage for identity keys (and make it the default) * fix: Use EOP when upgrading motoko canisters * feat: Network startup verbose output now requires `--debug` flag * feat: Add `icp network status` command to display network information diff --git a/Cargo.lock b/Cargo.lock index 61178b2d..e213d504 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -222,6 +222,18 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener 5.4.1", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-channel" version = "1.9.0" @@ -233,6 +245,18 @@ dependencies = [ "futures-core", ] +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-dropper" version = "0.3.1" @@ -272,6 +296,24 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.1", +] + [[package]] name = "async-lock" version = "3.4.1" @@ -283,6 +325,35 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel 2.5.0", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener 5.4.1", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "async-scoped" version = "0.9.0" @@ -294,6 +365,30 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.1", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.89" @@ -538,6 +633,19 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel 2.5.0", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "bollard" version = "0.19.4" @@ -1046,6 +1154,26 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -1224,6 +1352,35 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +[[package]] +name = "dbus" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b3aa68d7e7abee336255bd7248ea965cc393f3e70411135a6f6a4b651345d4" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "dbus-secret-service" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6" +dependencies = [ + "aes", + "block-padding", + "cbc", + "dbus", + "fastrand", + "hkdf", + "num", + "once_cell", + "sha2 0.10.9", + "zeroize", +] + [[package]] name = "der" version = "0.7.10" @@ -1525,6 +1682,33 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "env_filter" version = "0.1.4" @@ -1845,6 +2029,19 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.31" @@ -2312,6 +2509,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -2549,7 +2752,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5337598ec943bab711e0288319761abb5a7a7087ac226b03472441a90f88e0c" dependencies = [ "arc-swap", - "async-channel", + "async-channel 1.9.0", "async-lock", "async-trait", "async-watch", @@ -2888,6 +3091,7 @@ dependencies = [ "itertools 0.14.0", "jsonschema", "k256", + "keyring", "notify", "p256", "pem 3.0.5", @@ -3429,6 +3633,22 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "keyring" +version = "3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" +dependencies = [ + "byteorder", + "dbus-secret-service", + "log", + "secret-service", + "security-framework 2.11.1", + "security-framework 3.5.1", + "windows-sys 0.60.2", + "zeroize", +] + [[package]] name = "kqueue" version = "1.1.1" @@ -3508,6 +3728,15 @@ version = "0.2.176" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + [[package]] name = "libgit2-sys" version = "0.18.2+1.9.1" @@ -3699,6 +3928,15 @@ dependencies = [ "libc", ] +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "merlin" version = "3.0.0" @@ -3793,6 +4031,7 @@ dependencies = [ "cfg-if", "cfg_aliases", "libc", + "memoffset", ] [[package]] @@ -4084,6 +4323,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "outref" version = "0.5.2" @@ -4370,6 +4619,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkcs5" version = "0.7.1" @@ -4403,6 +4663,20 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.1", +] + [[package]] name = "portable-atomic" version = "1.11.1" @@ -5207,6 +5481,61 @@ dependencies = [ "cc", ] +[[package]] +name = "secret-service" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4d35ad99a181be0a60ffcbe85d680d98f87bdc4d7644ade319b87076b9dbfd4" +dependencies = [ + "aes", + "cbc", + "futures-util", + "generic-array", + "hkdf", + "num", + "once_cell", + "rand 0.8.5", + "serde", + "sha2 0.10.9", + "zbus", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.4", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags 2.9.4", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.27" @@ -5619,7 +5948,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af91f480ee899ab2d9f8435bfdfc14d08a5754bd9d3fef1f1a1c23336aad6c8b" dependencies = [ - "async-channel", + "async-channel 1.9.0", "cfg-if", "futures-core", "pin-project-lite", @@ -6204,6 +6533,17 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + [[package]] name = "unicase" version = "2.8.1" @@ -6859,6 +7199,16 @@ dependencies = [ "tap", ] +[[package]] +name = "xdg-home" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "yoke" version = "0.8.0" @@ -6883,6 +7233,62 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +dependencies = [ + "async-broadcast", + "async-process", + "async-recursion", + "async-trait", + "enumflags2", + "event-listener 5.4.1", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix 0.29.0", + "ordered-stream", + "rand 0.8.5", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.106", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.8.27" @@ -6976,3 +7382,40 @@ dependencies = [ "quote", "syn 2.0.106", ] + +[[package]] +name = "zvariant" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.106", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] diff --git a/Cargo.toml b/Cargo.toml index f1654c34..add27b77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ indoc = "2.0.6" itertools = "0.14.0" jsonschema = "0.33.0" k256 = { version = "0.13.4", features = ["pem", "pkcs8", "std"] } +keyring = { version = "3.6.3", features = ["apple-native", "windows-native", "sync-secret-service", "crypto-rust"] } lazy_static = "1.5.0" mockall = "0.13.1" notify = "8.2.0" diff --git a/crates/icp-cli/src/commands/identity/default.rs b/crates/icp-cli/src/commands/identity/default.rs index bdeb4826..0977bca3 100644 --- a/crates/icp-cli/src/commands/identity/default.rs +++ b/crates/icp-cli/src/commands/identity/default.rs @@ -4,6 +4,7 @@ use icp::identity::manifest::{IdentityDefaults, IdentityList, change_default_ide #[derive(Debug, Args)] pub(crate) struct DefaultArgs { + /// Identity to set as default. If omitted, prints the current default. name: Option, } diff --git a/crates/icp-cli/src/commands/identity/import.rs b/crates/icp-cli/src/commands/identity/import.rs index 924e13e6..d573a501 100644 --- a/crates/icp-cli/src/commands/identity/import.rs +++ b/crates/icp-cli/src/commands/identity/import.rs @@ -20,28 +20,44 @@ use snafu::{OptionExt, ResultExt, Snafu, ensure}; use icp::context::Context; +use crate::commands::identity::StorageMode; + #[derive(Debug, Args)] #[command(group(ArgGroup::new("import-from").required(true)))] pub(crate) struct ImportArgs { + /// Name for the imported identity name: String, + /// Where to store the private key + #[arg(long, value_enum, default_value_t)] + storage: StorageMode, + + /// Import from a PEM file #[arg(long, value_name = "FILE", group = "import-from")] from_pem: Option, + /// Read seed phrase interactively from the terminal #[arg(long, group = "import-from")] read_seed_phrase: bool, + /// Read seed phrase from a file #[arg(long, value_name = "FILE", group = "import-from")] from_seed_file: Option, + /// Read the PEM decryption password from a file instead of prompting #[arg(long, value_name = "FILE", requires = "from_pem")] decryption_password_from_file: Option, + /// Specify the key type when it cannot be detected from the PEM file (danger!) #[arg(long, value_enum)] assert_key_type: Option, } pub(crate) async fn exec(ctx: &Context, args: &ImportArgs) -> Result<(), anyhow::Error> { + let format = match args.storage { + StorageMode::Plaintext => CreateFormat::Plaintext, + StorageMode::Keyring => CreateFormat::Keyring, + }; if let Some(from_pem) = &args.from_pem { import_from_pem( ctx, @@ -49,18 +65,19 @@ pub(crate) async fn exec(ctx: &Context, args: &ImportArgs) -> Result<(), anyhow: from_pem, args.decryption_password_from_file.as_deref(), args.assert_key_type.clone(), + format, ) .await?; } else if let Some(path) = &args.from_seed_file { let phrase = read_to_string(path).context(ReadSeedFileSnafu)?; - import_from_seed_phrase(ctx, &args.name, &phrase).await?; + import_from_seed_phrase(ctx, &args.name, &phrase, format).await?; } else if args.read_seed_phrase { let phrase = Password::new() .with_prompt("Enter seed phrase") .with_confirmation("Re-enter seed phrase", "Seed phrases do not match") .interact() .context(ReadSeedPhraseFromTerminalSnafu)?; - import_from_seed_phrase(ctx, &args.name, &phrase).await?; + import_from_seed_phrase(ctx, &args.name, &phrase, format).await?; } else { unreachable!(); } @@ -76,6 +93,7 @@ async fn import_from_pem( path: &Path, decryption_password_file: Option<&Path>, known_key_type: Option, + format: CreateFormat, ) -> Result<(), LoadKeyError> { // the pem file may be in SEC1 format or PKCS#8 format // - if SEC1, the key algorithm can be embedded, separate, or missing @@ -128,7 +146,7 @@ async fn import_from_pem( ctx.dirs .identity()? - .with_write(async |dirs| create_identity(dirs, name, key, CreateFormat::Plaintext)) + .with_write(async move |dirs| create_identity(dirs, name, key, format)) .await??; Ok(()) @@ -303,19 +321,13 @@ async fn import_from_seed_phrase( ctx: &Context, name: &str, phrase: &str, + format: CreateFormat, ) -> Result<(), DeriveKeyError> { let mnemonic = Mnemonic::from_phrase(phrase, Language::English).context(ParseMnemonicSnafu)?; let key = derive_default_key_from_seed(&mnemonic); ctx.dirs .identity()? - .with_write(async |dirs| { - create_identity( - dirs, - name, - IdentityKey::Secp256k1(key), - CreateFormat::Plaintext, - ) - }) + .with_write(async |dirs| create_identity(dirs, name, IdentityKey::Secp256k1(key), format)) .await??; Ok(()) } diff --git a/crates/icp-cli/src/commands/identity/mod.rs b/crates/icp-cli/src/commands/identity/mod.rs index 7af89e46..de3fd9c2 100644 --- a/crates/icp-cli/src/commands/identity/mod.rs +++ b/crates/icp-cli/src/commands/identity/mod.rs @@ -1,4 +1,4 @@ -use clap::Subcommand; +use clap::{Subcommand, ValueEnum}; pub(crate) mod default; pub(crate) mod import; @@ -23,3 +23,10 @@ pub(crate) enum Command { /// Display the principal for the current identity Principal(principal::PrincipalArgs), } + +#[derive(Debug, Clone, ValueEnum, Default)] +enum StorageMode { + Plaintext, + #[default] + Keyring, +} diff --git a/crates/icp-cli/src/commands/identity/new.rs b/crates/icp-cli/src/commands/identity/new.rs index bae19b42..e3b21214 100644 --- a/crates/icp-cli/src/commands/identity/new.rs +++ b/crates/icp-cli/src/commands/identity/new.rs @@ -12,9 +12,18 @@ use icp::{ use icp::context::Context; +use crate::commands::identity::StorageMode; + #[derive(Debug, Args)] pub(crate) struct NewArgs { + /// Name for the new identity name: String, + + /// Where to store the private key + #[arg(long, value_enum, default_value_t)] + storage: StorageMode, + + /// Write the seed phrase to a file instead of printing to stdout #[arg(long, value_name = "FILE")] output_seed: Option, } @@ -24,6 +33,10 @@ pub(crate) async fn exec(ctx: &Context, args: &NewArgs) -> Result<(), anyhow::Er MnemonicType::for_key_size(256).context("failed to get mnemonic type")?, Language::English, ); + let format = match args.storage { + StorageMode::Plaintext => CreateFormat::Plaintext, + StorageMode::Keyring => CreateFormat::Keyring, + }; ctx.dirs .identity()? @@ -32,7 +45,7 @@ pub(crate) async fn exec(ctx: &Context, args: &NewArgs) -> Result<(), anyhow::Er dirs, &args.name, IdentityKey::Secp256k1(derive_default_key_from_seed(&mnemonic)), - CreateFormat::Plaintext, + format, ) }) .await??; diff --git a/crates/icp-cli/tests/common/context.rs b/crates/icp-cli/tests/common/context.rs index 54edc8d0..1f051c76 100644 --- a/crates/icp-cli/tests/common/context.rs +++ b/crates/icp-cli/tests/common/context.rs @@ -23,6 +23,7 @@ pub(crate) struct TestContext { home_dir: TempDir, bin_dir: PathBuf, asset_dir: PathBuf, + mock_cred_dir: PathBuf, os_path: OsString, gateway_url: OnceCell, root_key: OnceCell>, @@ -41,6 +42,10 @@ impl TestContext { let asset_dir = home_dir.path().join("share"); fs::create_dir(&asset_dir).expect("failed to create asset dir"); + // Credentials + let mock_cred_dir = home_dir.path().join("mock-keyring"); + fs::create_dir(&mock_cred_dir).expect("failed to create mock keyring dir"); + eprintln!("Test environment home directory: {}", home_dir.path()); // OS Path @@ -50,6 +55,7 @@ impl TestContext { home_dir, bin_dir, asset_dir, + mock_cred_dir, os_path, gateway_url: OnceCell::new(), root_key: OnceCell::new(), @@ -68,6 +74,7 @@ impl TestContext { cmd.env("HOME", self.home_path()); cmd.env("PATH", self.os_path.clone()); cmd.env_remove("ICP_HOME"); + cmd.env("ICP_CLI_KEYRING_MOCK_DIR", self.mock_cred_dir.clone()); cmd } diff --git a/crates/icp/Cargo.toml b/crates/icp/Cargo.toml index ceaadffe..2faf5e22 100644 --- a/crates/icp/Cargo.toml +++ b/crates/icp/Cargo.toml @@ -34,6 +34,7 @@ icrc-ledger-types = { workspace = true } indoc = { workspace = true } itertools = { workspace = true } k256 = { workspace = true } +keyring = { workspace = true } notify = { workspace = true } p256 = { workspace = true } pem = { workspace = true } diff --git a/crates/icp/src/context/init.rs b/crates/icp/src/context/init.rs index 656b0041..ab7fb9ce 100644 --- a/crates/icp/src/context/init.rs +++ b/crates/icp/src/context/init.rs @@ -77,6 +77,13 @@ pub fn initialize( let idload = Arc::new(identity::Loader { dir: dirs.identity().context(IdentityDirectorySnafu)?, }); + if let Ok(mockdir) = std::env::var("ICP_CLI_KEYRING_MOCK_DIR") { + keyring::set_default_credential_builder(Box::new( + crate::identity::keyring_mock::MockKeyring { + dir: PathBuf::from(mockdir), + }, + )); + } // Network accessor let netaccess = Arc::new(network::Accessor { diff --git a/crates/icp/src/identity/key.rs b/crates/icp/src/identity/key.rs index 00d9652f..c2039901 100644 --- a/crates/icp/src/identity/key.rs +++ b/crates/icp/src/identity/key.rs @@ -1,10 +1,14 @@ -use std::sync::Arc; +use std::{ + fmt::{self, Display, Formatter}, + sync::Arc, +}; use ic_agent::{ Identity, identity::{AnonymousIdentity, BasicIdentity, Prime256v1Identity, Secp256k1Identity}, }; use ic_ed25519::PrivateKeyFormat; +use keyring::Entry; use pem::Pem; use pkcs8::{ DecodePrivateKey, EncodePrivateKey, EncryptedPrivateKeyInfo, PrivateKeyInfo, SecretDocument, @@ -42,7 +46,7 @@ pub enum IdentityKey { pub enum CreateFormat { Plaintext, Pbes2 { password: Zeroizing }, - // Keyring, + Keyring, } #[derive(Debug, Snafu)] @@ -50,27 +54,27 @@ pub enum LoadIdentityError { #[snafu(transparent)] ReadFileError { source: crate::fs::IoError }, - #[snafu(display("failed to load PEM file `{path}`: failed to parse"))] + #[snafu(display("failed to load PEM from `{origin}`: failed to parse"))] ParsePemError { - path: PathBuf, + origin: PemOrigin, #[snafu(source(from(pem::PemError, Box::new)))] source: Box, }, - #[snafu(display("failed to load PEM file `{path}`: failed to decipher key"))] + #[snafu(display("failed to load PEM from `{origin}`: failed to decipher key"))] ParsePkcs8Error { - path: PathBuf, + origin: PemOrigin, #[snafu(source(from(pkcs8::Error, Box::new)))] source: Box, }, - #[snafu(display("failed to load PEM file `{path}`: failed to decipher key"))] + #[snafu(display("failed to load PEM from `{origin}`: failed to decipher key"))] ParseDerError { - path: PathBuf, + origin: PemOrigin, source: pkcs8::der::Error, }, - #[snafu(display("failed to load PEM file `{path}`: failed to decipher key"))] + #[snafu(display("failed to load PEM from `{origin}`: failed to decipher key"))] ParseEd25519KeyError { - path: PathBuf, + origin: PemOrigin, source: ic_ed25519::PrivateKeyDecodingError, }, @@ -82,9 +86,14 @@ pub enum LoadIdentityError { #[snafu(transparent)] LockError { source: crate::fs::lock::LockError }, + + #[snafu(display("failed to load keyring entry"))] + LoadEntryError { source: keyring::Error }, + + #[snafu(display("failed to load password from keyring entry"))] + LoadPasswordFromEntryError { source: keyring::Error }, } -// TODO(adam.spofford): Support p256, ed25519 pub fn load_identity( dirs: LRead<&IdentityPaths>, list: &IdentityList, @@ -100,6 +109,7 @@ pub fn load_identity( IdentitySpec::Pem { format, algorithm, .. } => load_pem_identity(dirs, name, format, algorithm, password_func), + IdentitySpec::Keyring { algorithm, .. } => load_keyring_identity(name, algorithm), IdentitySpec::Anonymous => Ok(Arc::new(AnonymousIdentity)), } @@ -113,15 +123,18 @@ fn load_pem_identity( password_func: impl FnOnce() -> Result, ) -> Result, LoadIdentityError> { let pem_path = dirs.key_pem_path(name); + let origin = PemOrigin::File { + path: pem_path.clone(), + }; let doc = fs::read_to_string(&pem_path)? .parse::() - .context(ParsePemSnafu { path: &pem_path })?; + .context(ParsePemSnafu { origin: &origin })?; match format { - PemFormat::Pbes2 => load_pbes2_identity(&doc, algorithm, password_func, &pem_path), + PemFormat::Pbes2 => load_pbes2_identity(&doc, algorithm, password_func, &origin), - PemFormat::Plaintext => load_plaintext_identity(&doc, algorithm, &pem_path), + PemFormat::Plaintext => load_plaintext_identity(&doc, algorithm, &origin), } } @@ -129,7 +142,7 @@ fn load_pbes2_identity( doc: &Pem, algorithm: &IdentityKeyAlgorithm, password_func: impl FnOnce() -> Result, - path: &Path, + origin: &PemOrigin, ) -> Result, LoadIdentityError> { assert!( doc.tag() == pkcs8::EncryptedPrivateKeyInfo::PEM_LABEL, @@ -141,23 +154,23 @@ fn load_pbes2_identity( match algorithm { IdentityKeyAlgorithm::Secp256k1 => { let key = k256::SecretKey::from_pkcs8_encrypted_der(doc.contents(), &pw) - .context(ParsePkcs8Snafu { path })?; + .context(ParsePkcs8Snafu { origin })?; Ok(Arc::new(Secp256k1Identity::from_private_key(key))) } IdentityKeyAlgorithm::Prime256v1 => { let key = p256::SecretKey::from_pkcs8_encrypted_der(doc.contents(), &pw) - .context(ParsePkcs8Snafu { path })?; + .context(ParsePkcs8Snafu { origin })?; Ok(Arc::new(Prime256v1Identity::from_private_key(key))) } IdentityKeyAlgorithm::Ed25519 => { let encrypted = EncryptedPrivateKeyInfo::from_der(doc.contents()) - .context(ParseDerSnafu { path })?; + .context(ParseDerSnafu { origin })?; let decrypted: SecretDocument = - encrypted.decrypt(&pw).context(ParsePkcs8Snafu { path })?; + encrypted.decrypt(&pw).context(ParsePkcs8Snafu { origin })?; let key = ic_ed25519::PrivateKey::deserialize_pkcs8(decrypted.as_bytes()) - .context(ParseEd25519KeySnafu { path })?; + .context(ParseEd25519KeySnafu { origin })?; Ok(Arc::new(BasicIdentity::from_raw_key(&key.serialize_raw()))) } } @@ -166,7 +179,7 @@ fn load_pbes2_identity( fn load_plaintext_identity( doc: &Pem, algorithm: &IdentityKeyAlgorithm, - path: &Path, + origin: &PemOrigin, ) -> Result, LoadIdentityError> { assert!( doc.tag() == PrivateKeyInfo::PEM_LABEL, @@ -176,24 +189,75 @@ fn load_plaintext_identity( match algorithm { IdentityKeyAlgorithm::Secp256k1 => { let key = k256::SecretKey::from_pkcs8_der(doc.contents()) - .context(ParsePkcs8Snafu { path })?; + .context(ParsePkcs8Snafu { origin })?; Ok(Arc::new(Secp256k1Identity::from_private_key(key))) } IdentityKeyAlgorithm::Prime256v1 => { let key = p256::SecretKey::from_pkcs8_der(doc.contents()) - .context(ParsePkcs8Snafu { path })?; + .context(ParsePkcs8Snafu { origin })?; Ok(Arc::new(Prime256v1Identity::from_private_key(key))) } IdentityKeyAlgorithm::Ed25519 => { let key = ic_ed25519::PrivateKey::deserialize_pkcs8(doc.contents()) - .context(ParseEd25519KeySnafu { path })?; + .context(ParseEd25519KeySnafu { origin })?; Ok(Arc::new(BasicIdentity::from_raw_key(&key.serialize_raw()))) } } } +const SERVICE_NAME: &str = "icp-cli"; + +fn load_keyring_identity( + name: &str, + algorithm: &IdentityKeyAlgorithm, +) -> Result, LoadIdentityError> { + let entry = Entry::new(SERVICE_NAME, name).context(LoadEntrySnafu)?; + let password = entry.get_password().context(LoadPasswordFromEntrySnafu)?; + let origin = PemOrigin::Keyring { + service: SERVICE_NAME.to_string(), + username: name.to_string(), + }; + let pem = password + .parse::() + .context(ParsePemSnafu { origin: &origin })?; + load_plaintext_identity(&pem, algorithm, &origin) +} + +#[derive(Debug, Clone)] +pub enum PemOrigin { + File { path: PathBuf }, + Keyring { service: String, username: String }, +} + +impl Display for PemOrigin { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + PemOrigin::File { path } => write!(f, "file `{path}`"), + PemOrigin::Keyring { service, username } => { + let store = if cfg!(target_os = "windows") { + "Windows Credential Manager" + } else if cfg!(target_os = "macos") { + "Keychain" + } else { + "secret-service" + }; + write!( + f, + "{store} entry (service=`{service}`, username=`{username}`)" + ) + } + } + } +} + +impl From<&PemOrigin> for PemOrigin { + fn from(value: &PemOrigin) -> Self { + value.clone() + } +} + #[derive(Debug, Snafu)] pub enum LoadIdentityInContextError { #[snafu(transparent)] @@ -241,43 +305,33 @@ pub fn create_identity( key: IdentityKey, format: CreateFormat, ) -> Result<(), CreateIdentityError> { - let spec = IdentitySpec::Pem { - format: match format { - CreateFormat::Plaintext => PemFormat::Plaintext, - CreateFormat::Pbes2 { .. } => PemFormat::Pbes2, - }, - - algorithm: match key { - IdentityKey::Secp256k1(_) => IdentityKeyAlgorithm::Secp256k1, - IdentityKey::Prime256v1(_) => IdentityKeyAlgorithm::Prime256v1, - IdentityKey::Ed25519(_) => IdentityKeyAlgorithm::Ed25519, - }, - - principal: match &key { - IdentityKey::Secp256k1(secret_key) => { - Secp256k1Identity::from_private_key(secret_key.clone()) - .sender() - .expect("infallible method") - } - IdentityKey::Prime256v1(secret_key) => { - Prime256v1Identity::from_private_key(secret_key.clone()) - .sender() - .expect("infallible method") - } - IdentityKey::Ed25519(secret_key) => { - BasicIdentity::from_raw_key(&secret_key.serialize_raw()) - .sender() - .expect("infallible method") - } - }, - }; - let mut identity_list = IdentityList::load_from(dirs.read())?; ensure!( !identity_list.identities.contains_key(name), IdentityAlreadyExistsSnafu { name } ); - + let principal = match &key { + IdentityKey::Secp256k1(secret_key) => { + Secp256k1Identity::from_private_key(secret_key.clone()) + .sender() + .expect("infallible method") + } + IdentityKey::Prime256v1(secret_key) => { + Prime256v1Identity::from_private_key(secret_key.clone()) + .sender() + .expect("infallible method") + } + IdentityKey::Ed25519(secret_key) => { + BasicIdentity::from_raw_key(&secret_key.serialize_raw()) + .sender() + .expect("infallible method") + } + }; + let algorithm = match key { + IdentityKey::Secp256k1(_) => IdentityKeyAlgorithm::Secp256k1, + IdentityKey::Prime256v1(_) => IdentityKeyAlgorithm::Prime256v1, + IdentityKey::Ed25519(_) => IdentityKeyAlgorithm::Ed25519, + }; let doc = match key { IdentityKey::Secp256k1(key) => key.to_pkcs8_der().expect("infallible PKI encoding"), IdentityKey::Prime256v1(key) => key.to_pkcs8_der().expect("infallible PKI encoding"), @@ -286,16 +340,49 @@ pub fn create_identity( .try_into() .expect("infallible PKI encoding"), }; - - let pem = match format { - CreateFormat::Plaintext => doc - .to_pem(PrivateKeyInfo::PEM_LABEL, Default::default()) - .expect("infallible PKI encoding"), - - CreateFormat::Pbes2 { password } => make_pkcs5_encrypted_pem(&doc, &password), + // store key material + match &format { + CreateFormat::Plaintext => { + let pem = doc + .to_pem(PrivateKeyInfo::PEM_LABEL, Default::default()) + .expect("infallible PKI encoding"); + write_identity(dirs, name, &pem)?; + } + CreateFormat::Pbes2 { password } => { + let pem = make_pkcs5_encrypted_pem(&doc, password); + write_identity(dirs, name, &pem)?; + } + CreateFormat::Keyring => { + let pem = doc + .to_pem(PrivateKeyInfo::PEM_LABEL, Default::default()) + .expect("infallible PKI encoding"); + let entry = Entry::new(SERVICE_NAME, name).context(CreateEntrySnafu)?; + let res = entry.set_password(&pem); + #[cfg(target_os = "linux")] + if let Err(keyring::Error::NoStorageAccess(err)) = &res + && err.to_string().contains("no result found") + { + return NoKeyringSnafu.fail()?; + } + res.context(SetEntryPasswordSnafu)?; + } + } + let spec = match format { + CreateFormat::Plaintext => IdentitySpec::Pem { + format: PemFormat::Plaintext, + algorithm, + principal, + }, + CreateFormat::Pbes2 { .. } => IdentitySpec::Pem { + format: PemFormat::Pbes2, + algorithm, + principal, + }, + CreateFormat::Keyring => IdentitySpec::Keyring { + principal, + algorithm, + }, }; - - write_identity(dirs, name, &pem)?; identity_list.identities.insert(name.to_string(), spec); identity_list.write_to(dirs)?; @@ -312,6 +399,16 @@ pub enum WriteIdentityError { #[snafu(transparent)] LockError { source: crate::fs::lock::LockError }, + + #[snafu(display("failed to create keyring entry"))] + CreateEntryError { source: keyring::Error }, + #[snafu(display("failed to set keyring entry password"))] + SetEntryPasswordError { source: keyring::Error }, + #[cfg(target_os = "linux")] + #[snafu(display( + "no keyring available - have you set it up? gnome-keyring must be installed and configured with a default keyring." + ))] + NoKeyring, } fn write_identity( diff --git a/crates/icp/src/identity/keyring_mock.rs b/crates/icp/src/identity/keyring_mock.rs new file mode 100644 index 00000000..ed20de94 --- /dev/null +++ b/crates/icp/src/identity/keyring_mock.rs @@ -0,0 +1,74 @@ +use std::fmt::Debug; + +use keyring::{ + Credential, + credential::{CredentialApi, CredentialBuilderApi, CredentialPersistence}, +}; + +use crate::prelude::*; + +pub struct MockKeyring { + pub dir: PathBuf, +} + +impl CredentialBuilderApi for MockKeyring { + fn as_any(&self) -> &dyn std::any::Any { + self + } + fn build( + &self, + target: Option<&str>, + service: &str, + user: &str, + ) -> keyring::Result> { + let filename = format!( + "{}_{}_{}.mockcred", + target.unwrap_or("default"), + service, + user.replace(|c: char| !c.is_alphanumeric(), "_"), + ); + let filepath = self.dir.join(filename); + Ok(Box::new(MockCredential { file: filepath })) + } + fn persistence(&self) -> CredentialPersistence { + CredentialPersistence::UntilDelete + } +} + +#[derive(Debug)] +pub struct MockCredential { + pub file: PathBuf, +} + +impl CredentialApi for MockCredential { + fn as_any(&self) -> &dyn std::any::Any { + self + } + fn get_password(&self) -> keyring::Result { + std::fs::read_to_string(&self.file) + .map_err(|e| keyring::Error::PlatformFailure(Box::new(e))) + } + fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Debug::fmt(self, f) + } + fn delete_credential(&self) -> keyring::Result<()> { + std::fs::remove_file(&self.file).map_err(|e| keyring::Error::PlatformFailure(Box::new(e))) + } + fn get_attributes(&self) -> keyring::Result> { + Err(keyring::Error::PlatformFailure("unsupported".into())) + } + fn get_secret(&self) -> keyring::Result> { + std::fs::read(&self.file).map_err(|e| keyring::Error::PlatformFailure(Box::new(e))) + } + fn set_password(&self, password: &str) -> keyring::Result<()> { + std::fs::write(&self.file, password.as_bytes()) + .map_err(|e| keyring::Error::PlatformFailure(Box::new(e))) + } + fn set_secret(&self, password: &[u8]) -> keyring::Result<()> { + std::fs::write(&self.file, password) + .map_err(|e| keyring::Error::PlatformFailure(Box::new(e))) + } + fn update_attributes(&self, _: &std::collections::HashMap<&str, &str>) -> keyring::Result<()> { + Err(keyring::Error::PlatformFailure("unsupported".into())) + } +} diff --git a/crates/icp/src/identity/manifest.rs b/crates/icp/src/identity/manifest.rs index 7c394dae..e3febb31 100644 --- a/crates/icp/src/identity/manifest.rs +++ b/crates/icp/src/identity/manifest.rs @@ -118,7 +118,10 @@ pub enum IdentitySpec { principal: Principal, }, Anonymous, - // Keyring, + Keyring { + principal: Principal, + algorithm: IdentityKeyAlgorithm, + }, } impl IdentitySpec { @@ -126,6 +129,7 @@ impl IdentitySpec { match self { IdentitySpec::Pem { principal, .. } => *principal, IdentitySpec::Anonymous => Principal::anonymous(), + IdentitySpec::Keyring { principal, .. } => *principal, } } } diff --git a/crates/icp/src/identity/mod.rs b/crates/icp/src/identity/mod.rs index a28c3e0b..1ef60e43 100644 --- a/crates/icp/src/identity/mod.rs +++ b/crates/icp/src/identity/mod.rs @@ -16,6 +16,7 @@ use crate::{ }; pub mod key; +pub mod keyring_mock; pub mod manifest; pub mod seed; diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 4c9f5a0f..576569c6 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -523,7 +523,7 @@ Display the currently selected identity ###### **Arguments:** -* `` +* `` — Identity to set as default. If omitted, prints the current default @@ -535,15 +535,21 @@ Import a new identity ###### **Arguments:** -* `` +* `` — Name for the imported identity ###### **Options:** -* `--from-pem ` -* `--read-seed-phrase` -* `--from-seed-file ` -* `--decryption-password-from-file ` -* `--assert-key-type ` +* `--storage ` — Where to store the private key + + Default value: `keyring` + + Possible values: `plaintext`, `keyring` + +* `--from-pem ` — Import from a PEM file +* `--read-seed-phrase` — Read seed phrase interactively from the terminal +* `--from-seed-file ` — Read seed phrase from a file +* `--decryption-password-from-file ` — Read the PEM decryption password from a file instead of prompting +* `--assert-key-type ` — Specify the key type when it cannot be detected from the PEM file (danger!) @@ -563,11 +569,17 @@ Create a new identity ###### **Arguments:** -* `` +* `` — Name for the new identity ###### **Options:** -* `--output-seed ` +* `--storage ` — Where to store the private key + + Default value: `keyring` + + Possible values: `plaintext`, `keyring` + +* `--output-seed ` — Write the seed phrase to a file instead of printing to stdout