From 35549c3100d9815903cb5b2ff7dc04b91b215936 Mon Sep 17 00:00:00 2001 From: Ludo Galabru Date: Fri, 27 Mar 2026 11:13:50 -0400 Subject: [PATCH 1/5] feat: implement gnome-keyring keystore --- .github/workflows/release-cli.yml | 25 + rust/Cargo.lock | 903 ++++++++++++++++-- rust/Cargo.toml | 1 + rust/crates/cli/src/commands/destroy.rs | 29 + rust/crates/cli/src/commands/export.rs | 24 +- rust/crates/cli/src/commands/setup.rs | 74 +- rust/crates/core/src/accounts.rs | 3 + rust/crates/core/src/config.rs | 7 + rust/crates/core/src/keystore.rs | 3 + rust/crates/core/src/signer.rs | 23 +- rust/crates/keystore/Cargo.toml | 6 + .../keystore/src/backends/apple_keychain.rs | 6 +- .../keystore/src/backends/gnome_keyring.rs | 369 +++++++ rust/crates/keystore/src/backends/mod.rs | 3 + .../keystore/src/backends/onepassword.rs | 6 +- rust/crates/keystore/src/lib.rs | 4 +- rust/rust-toolchain.toml | 2 + 17 files changed, 1412 insertions(+), 76 deletions(-) create mode 100644 rust/crates/keystore/src/backends/gnome_keyring.rs create mode 100644 rust/rust-toolchain.toml diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 2e90dcf3..d278788d 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -182,6 +182,31 @@ jobs: artifacts/*.zip artifacts/sha256sums.txt + # ── Publish to Snap Store ──────────────────────────────────────────────── + snap: + name: Snap + needs: [preflight, release] + if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.dry-run) }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.preflight.outputs.tag }} + + - name: Set snap version + run: | + sed -i "s/^version:.*/version: \"${{ needs.preflight.outputs.version }}\"/" snap/snapcraft.yaml + + - uses: snapcore/action-build@v1 + id: build + + - uses: snapcore/action-publish@v1 + env: + SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} + with: + snap: ${{ steps.build.outputs.snap }} + release: stable + # ── Update Homebrew tap ────────────────────────────────────────────────── # Disabled: the first Homebrew submission must be manual. # Uncomment after solana-foundation/homebrew-tap has the pay formula. diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 4bb9b37e..a3c3e3c6 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -205,6 +205,28 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" +[[package]] +name = "async-broadcast" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" +dependencies = [ + "event-listener 2.5.3", + "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-compression" version = "0.4.41" @@ -217,6 +239,116 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock 2.8.0", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite 1.13.0", + "log", + "parking", + "polling 2.8.0", + "rustix 0.37.28", + "slab", + "socket2 0.4.10", + "waker-fn", +] + +[[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 2.6.1", + "parking", + "polling 3.11.0", + "rustix 1.1.4", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener 5.4.1", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" +dependencies = [ + "async-io 1.13.0", + "async-lock 2.8.0", + "async-signal", + "blocking", + "cfg-if", + "event-listener 3.1.0", + "futures-lite 1.13.0", + "rustix 0.38.44", + "windows-sys 0.48.0", +] + +[[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.117", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io 2.6.0", + "async-lock 3.4.2", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.4", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[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" @@ -225,7 +357,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -276,6 +408,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.11.0" @@ -306,6 +444,28 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +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", + "async-task", + "futures-io", + "futures-lite 2.6.1", + "piper", +] + [[package]] name = "borsh" version = "1.6.1" @@ -324,10 +484,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" dependencies = [ "once_cell", - "proc-macro-crate", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -393,7 +553,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -423,6 +583,15 @@ dependencies = [ "rustversion", ] +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.2.57" @@ -455,7 +624,7 @@ checksum = "45565fc9416b9896014f5732ac776f810ee53a66730c17e4020c3ec064a8f88f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -513,7 +682,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -573,6 +742,15 @@ version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "console" version = "0.16.3" @@ -641,13 +819,19 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crossterm" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags", + "bitflags 2.11.0", "crossterm_winapi", "mio", "parking_lot", @@ -724,7 +908,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -758,7 +942,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.117", ] [[package]] @@ -771,7 +955,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.117", ] [[package]] @@ -782,7 +966,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core 0.21.3", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -793,7 +977,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core 0.23.0", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -812,6 +996,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e5c37193a1db1d8ed868c03ec7b152175f26160a5b740e5e484143877e0adf0" +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "dialoguer" version = "0.12.0" @@ -865,7 +1060,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -965,6 +1160,27 @@ dependencies = [ "cfg-if", ] +[[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.117", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -981,6 +1197,53 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener 5.4.1", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -1147,6 +1410,31 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.32" @@ -1155,7 +1443,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1287,6 +1575,33 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -1407,7 +1722,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.6.3", "system-configuration", "tokio", "tower-service", @@ -1578,6 +1893,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ + "block-padding", "generic-array", ] @@ -1591,7 +1907,27 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.48.0", ] [[package]] @@ -1729,6 +2065,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -1792,6 +2134,15 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + [[package]] name = "memoffset" version = "0.9.1" @@ -1858,6 +2209,18 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.7.1", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1867,6 +2230,20 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -1877,6 +2254,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-derive" version = "0.4.2" @@ -1885,7 +2271,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1897,6 +2283,28 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1922,10 +2330,10 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1952,7 +2360,7 @@ version = "0.10.76" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" dependencies = [ - "bitflags", + "bitflags 2.11.0", "cfg-if", "foreign-types", "libc", @@ -1969,7 +2377,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2006,12 +2414,28 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[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 = "owo-colors" version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -2122,10 +2546,14 @@ dependencies = [ name = "pay-keystore" version = "0.1.0" dependencies = [ + "secret-service", "serde", "serde_json", "shellexpand", "thiserror 2.0.18", + "tokio", + "zbus", + "zeroize", ] [[package]] @@ -2180,7 +2608,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2201,6 +2629,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand 2.3.0", + "futures-io", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -2217,6 +2656,36 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys 0.48.0", +] + +[[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 0.5.2", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + [[package]] name = "polyval" version = "0.6.2" @@ -2247,6 +2716,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + [[package]] name = "proc-macro-crate" version = "3.5.0" @@ -2273,7 +2752,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "version_check", "yansi", ] @@ -2306,7 +2785,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2", + "socket2 0.6.3", "thiserror 2.0.18", "tokio", "tracing", @@ -2343,7 +2822,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2", + "socket2 0.6.3", "tracing", "windows-sys 0.60.2", ] @@ -2428,7 +2907,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ - "bitflags", + "bitflags 2.11.0", "cassowary", "compact_str", "crossterm", @@ -2449,7 +2928,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.11.0", ] [[package]] @@ -2480,7 +2959,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2629,7 +3108,7 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn", + "syn 2.0.117", ] [[package]] @@ -2653,13 +3132,27 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.37.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "519165d378b97752ca44bbe15047d5d3409e875f39327546b42ac81d7e18c1b6" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + [[package]] name = "rustix" version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -2672,7 +3165,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys 0.12.1", @@ -2770,7 +3263,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn", + "syn 2.0.117", ] [[package]] @@ -2782,7 +3275,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn", + "syn 2.0.117", ] [[package]] @@ -2805,13 +3298,32 @@ dependencies = [ "zeroize", ] +[[package]] +name = "secret-service" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5204d39df37f06d1944935232fd2dfe05008def7ca599bf28c0800366c8a8f9" +dependencies = [ + "aes", + "cbc", + "futures-util", + "generic-array", + "hkdf", + "num", + "once_cell", + "rand 0.8.5", + "serde", + "sha2", + "zbus", +] + [[package]] name = "security-framework" version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags", + "bitflags 2.11.0", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -2880,7 +3392,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2891,7 +3403,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2907,6 +3419,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "serde_spanned" version = "0.6.9" @@ -2947,7 +3470,7 @@ dependencies = [ "darling 0.23.0", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2965,6 +3488,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.9" @@ -3087,6 +3621,16 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "socket2" version = "0.6.3" @@ -3561,7 +4105,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ddf67876c541aa1e21ee1acae35c95c6fbc61119814bfef70579317a5e26955" dependencies = [ - "bitflags", + "bitflags 2.11.0", "solana-account-info", "solana-instruction", "solana-instruction-error", @@ -3758,7 +4302,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6edf2f25743c95229ac0fdc32f8f5893ef738dbf332c669e9861d33ddb0f469d" dependencies = [ - "bitflags", + "bitflags 2.11.0", ] [[package]] @@ -3778,7 +4322,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91b12305dd81045d705f427acd0435a2e46444b65367d7179d7bdcfc3bc5f5eb" dependencies = [ - "memoffset", + "memoffset 0.9.1", "solana-account-info", "solana-big-mod-exp", "solana-blake3-hasher", @@ -4071,7 +4615,7 @@ dependencies = [ "bs58", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4579,7 +5123,7 @@ checksum = "d9e8418ea6269dcfb01c712f0444d2c75542c04448b480e87de59d2865edc750" dependencies = [ "quote", "spl-discriminator-syn", - "syn", + "syn 2.0.117", ] [[package]] @@ -4591,7 +5135,7 @@ dependencies = [ "proc-macro2", "quote", "sha2", - "syn", + "syn 2.0.117", "thiserror 1.0.69", ] @@ -4805,7 +5349,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn", + "syn 2.0.117", ] [[package]] @@ -4814,6 +5358,17 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.117" @@ -4842,7 +5397,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4851,7 +5406,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags", + "bitflags 2.11.0", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -4872,7 +5427,7 @@ version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ - "fastrand", + "fastrand 2.3.0", "getrandom 0.3.4", "once_cell", "rustix 1.1.4", @@ -4905,7 +5460,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4916,7 +5471,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -4965,8 +5520,9 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.6.3", "tokio-macros", + "tracing", "windows-sys 0.61.2", ] @@ -4978,7 +5534,7 @@ checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -5044,6 +5600,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime 0.6.11", + "winnow 0.5.40", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -5107,7 +5674,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "async-compression", - "bitflags", + "bitflags 2.11.0", "bytes", "futures-core", "futures-util", @@ -5154,7 +5721,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -5221,6 +5788,17 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset 0.9.1", + "tempfile", + "windows-sys 0.61.2", +] + [[package]] name = "uncased" version = "0.9.10" @@ -5348,6 +5926,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + [[package]] name = "want" version = "0.3.1" @@ -5418,7 +6002,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -5516,7 +6100,7 @@ dependencies = [ "darling 0.21.3", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -5528,7 +6112,7 @@ dependencies = [ "darling 0.21.3", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -5552,7 +6136,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -5563,7 +6147,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -5601,6 +6185,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -5610,6 +6203,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" @@ -5628,6 +6230,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -5661,6 +6278,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -5673,6 +6296,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -5685,6 +6314,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -5709,6 +6344,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -5721,6 +6362,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -5733,6 +6380,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -5745,6 +6398,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -5757,6 +6416,15 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + [[package]] name = "winnow" version = "0.7.15" @@ -5787,6 +6455,16 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[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 = "yansi" version = "1.0.1" @@ -5812,10 +6490,71 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] +[[package]] +name = "zbus" +version = "3.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "675d170b632a6ad49804c8cf2105d7c31eddd3312555cffd4b740e08e97c25e6" +dependencies = [ + "async-broadcast", + "async-process", + "async-recursion", + "async-trait", + "byteorder", + "derivative", + "enumflags2", + "event-listener 2.5.3", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix", + "once_cell", + "ordered-stream", + "rand 0.8.5", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tokio", + "tracing", + "uds_windows", + "winapi", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "3.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7131497b0f887e8061b430c530240063d33bf9455fa34438f388a245da69e0a5" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "437d738d3750bed6ca9b8d423ccc7a8eb284f6b1d6d4e225a0e4e6258d864c8d" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.8.47" @@ -5833,7 +6572,7 @@ checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -5853,7 +6592,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -5874,7 +6613,7 @@ checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -5907,7 +6646,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -5943,3 +6682,41 @@ dependencies = [ "cc", "pkg-config", ] + +[[package]] +name = "zvariant" +version = "3.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eef2be88ba09b358d3b58aca6e41cd853631d44787f319a1383ca83424fb2db" +dependencies = [ + "byteorder", + "enumflags2", + "libc", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "3.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c24dc0bed72f5f90d1f8bb5b07228cbf63b3c6e9f82d82559d4bae666e7ed9" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 2b594d5d..739ed3a0 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -70,6 +70,7 @@ reqwest = { version = "0.12", features = [ ], default-features = false } # Crypto (already in dep tree via solana crates) +zeroize = "1" ed25519-dalek = { version = "2", default-features = false } rand = "0.8" bs58 = "0.5" diff --git a/rust/crates/cli/src/commands/destroy.rs b/rust/crates/cli/src/commands/destroy.rs index 59e76dff..54f53a4e 100644 --- a/rust/crates/cli/src/commands/destroy.rs +++ b/rust/crates/cli/src/commands/destroy.rs @@ -117,6 +117,19 @@ impl DestroyCommand { "Cannot delete Keychain entries on this platform".to_string(), )); } + #[cfg(target_os = "linux")] + Keystore::GnomeKeyring => { + use pay_core::keystore::GnomeKeyring; + GnomeKeyring + .delete(&self.account) + .map_err(|e| pay_core::Error::Config(format!("GNOME Keyring delete: {e}")))?; + } + #[cfg(not(target_os = "linux"))] + Keystore::GnomeKeyring => { + return Err(pay_core::Error::Config( + "Cannot delete GNOME Keyring entries on this platform".to_string(), + )); + } Keystore::OnePassword => { use pay_core::keystore::OnePassword; let backend = OnePassword::new(); @@ -178,6 +191,22 @@ fn discover_legacy_account(name: &str) -> Option { } } + // Try GNOME Keyring (Linux) + #[cfg(target_os = "linux")] + { + use pay_core::keystore::GnomeKeyring; + let kr = GnomeKeyring; + if kr.exists(name) { + let pubkey = kr.pubkey(name).ok().map(|b| bs58::encode(&b).into_string()); + return Some(Account { + keystore: Keystore::GnomeKeyring, + pubkey, + vault: None, + path: None, + }); + } + } + // Try 1Password { use pay_core::keystore::OnePassword; diff --git a/rust/crates/cli/src/commands/export.rs b/rust/crates/cli/src/commands/export.rs index 99a799d5..a6687a08 100644 --- a/rust/crates/cli/src/commands/export.rs +++ b/rust/crates/cli/src/commands/export.rs @@ -33,8 +33,7 @@ impl ExportCommand { let keypair_bytes = reload_raw_bytes(&source)?; // Solana CLI format: JSON array of 64 u8 values - let bytes_vec: Vec = keypair_bytes.to_vec(); - let json = serde_json::to_string(&bytes_vec) + let json = serde_json::to_string(&*keypair_bytes) .map_err(|e| pay_core::Error::Config(format!("JSON error: {e}")))?; if self.path == "-" { @@ -64,7 +63,7 @@ impl ExportCommand { } } -fn reload_raw_bytes(source: &str) -> pay_core::Result> { +fn reload_raw_bytes(source: &str) -> pay_core::Result>> { use pay_core::keystore::KeystoreBackend; if let Some(account) = source.strip_prefix("keychain:") { @@ -84,6 +83,23 @@ fn reload_raw_bytes(source: &str) -> pay_core::Result> { } } + if let Some(account) = source.strip_prefix("gnome-keyring:") { + #[cfg(target_os = "linux")] + { + use pay_core::keystore::GnomeKeyring; + return GnomeKeyring + .load_keypair(account, "export keypair") + .map_err(|e| pay_core::Error::Config(format!("GNOME Keyring: {e}"))); + } + #[cfg(not(target_os = "linux"))] + { + let _ = account; + return Err(pay_core::Error::Config( + "GNOME Keyring not available on this platform".to_string(), + )); + } + } + if let Some(account) = source.strip_prefix("1password:") { let backend = pay_core::keystore::OnePassword::new(); return backend @@ -103,5 +119,5 @@ fn reload_raw_bytes(source: &str) -> pay_core::Result> { bytes.len() ))); } - Ok(bytes) + Ok(pay_core::keystore::Zeroizing::new(bytes)) } diff --git a/rust/crates/cli/src/commands/setup.rs b/rust/crates/cli/src/commands/setup.rs index 472a606a..13245169 100644 --- a/rust/crates/cli/src/commands/setup.rs +++ b/rust/crates/cli/src/commands/setup.rs @@ -43,9 +43,15 @@ impl SetupCommand { "keychain" => Err(pay_core::Error::Config( "Keychain backend is only available on macOS".to_string(), )), + #[cfg(target_os = "linux")] + "gnome-keyring" => self.run_gnome_keyring(), + #[cfg(not(target_os = "linux"))] + "gnome-keyring" => Err(pay_core::Error::Config( + "GNOME Keyring is only available on Linux".to_string(), + )), "1password" => self.run_1password(), other => Err(pay_core::Error::Config(format!( - "Unknown backend: {other}. Use 'keychain' or '1password'." + "Unknown backend: {other}. Use 'keychain', 'gnome-keyring', or '1password'." ))), } } @@ -61,6 +67,16 @@ impl SetupCommand { } let keychain_available = cfg!(target_os = "macos"); + let gnome_available = { + #[cfg(target_os = "linux")] + { + pay_core::keystore::GnomeKeyring::is_available() + } + #[cfg(not(target_os = "linux"))] + { + false + } + }; let op_available = pay_core::keystore::OnePassword::is_available(); let options = [ @@ -73,6 +89,15 @@ impl SetupCommand { }, available: keychain_available, }, + BackendOption { + id: "gnome-keyring", + label: if gnome_available { + "GNOME Keyring (password prompt)".to_string() + } else { + "GNOME Keyring — not available (desktop session required)".to_string() + }, + available: gnome_available, + }, BackendOption { id: "1password", label: if op_available { @@ -110,6 +135,9 @@ impl SetupCommand { if !chosen.available { let hint = match chosen.id { "keychain" => "Keychain is only available on macOS.", + "gnome-keyring" => { + "GNOME Keyring requires a GNOME or KDE (Plasma 6+) desktop session." + } "1password" => { "Install the 1Password CLI: https://developer.1password.com/docs/cli/get-started" } @@ -162,6 +190,50 @@ impl SetupCommand { self.show_next_steps(&pubkey_b58) } + #[cfg(target_os = "linux")] + fn run_gnome_keyring(&self) -> pay_core::Result<()> { + use pay_core::keystore::GnomeKeyring; + + if !GnomeKeyring::is_available() { + return Err(pay_core::Error::Config( + "GNOME Keyring is not available. A GNOME or KDE (Plasma 6+) desktop session is required.".to_string(), + )); + } + + let backend = GnomeKeyring; + + if backend.exists("default") && !self.force { + return self.show_existing(&backend); + } + + let (keypair_bytes, pubkey_b58) = generate_keypair(); + + backend + .import( + "default", + &keypair_bytes, + pay_core::keystore::SyncMode::ThisDeviceOnly, + ) + .map_err(|e| pay_core::Error::Config(format!("{e}")))?; + + eprintln!(); + eprintln!(" {} {pubkey_b58}", "Your account:".dimmed()); + eprintln!(); + eprintln!( + "{}", + " Stored in GNOME Keyring — password prompt required to pay.".dimmed() + ); + + save_account( + "default", + pay_core::accounts::Keystore::GnomeKeyring, + &pubkey_b58, + None, + None, + )?; + self.show_next_steps(&pubkey_b58) + } + fn run_1password(&self) -> pay_core::Result<()> { use pay_core::keystore::OnePassword; diff --git a/rust/crates/core/src/accounts.rs b/rust/crates/core/src/accounts.rs index 2018c64c..c51e8686 100644 --- a/rust/crates/core/src/accounts.rs +++ b/rust/crates/core/src/accounts.rs @@ -30,6 +30,7 @@ const ACCOUNTS_FILE: &str = "~/.config/pay/accounts.yml"; #[serde(rename_all = "kebab-case")] pub enum Keystore { AppleKeychain, + GnomeKeyring, OnePassword, File, } @@ -38,6 +39,7 @@ impl std::fmt::Display for Keystore { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Keystore::AppleKeychain => write!(f, "apple-keychain"), + Keystore::GnomeKeyring => write!(f, "gnome-keyring"), Keystore::OnePassword => write!(f, "1password"), Keystore::File => write!(f, "file"), } @@ -68,6 +70,7 @@ impl Account { pub fn signer_source(&self, name: &str) -> String { match self.keystore { Keystore::AppleKeychain => format!("keychain:{name}"), + Keystore::GnomeKeyring => format!("gnome-keyring:{name}"), Keystore::OnePassword => format!("1password:{name}"), Keystore::File => self .path diff --git a/rust/crates/core/src/config.rs b/rust/crates/core/src/config.rs index e0fab625..0e49658e 100644 --- a/rust/crates/core/src/config.rs +++ b/rust/crates/core/src/config.rs @@ -103,6 +103,13 @@ impl Config { return Some("keychain:default".to_string()); } } + #[cfg(target_os = "linux")] + { + use crate::keystore::{GnomeKeyring, KeystoreBackend}; + if GnomeKeyring.exists("default") { + return Some("gnome-keyring:default".to_string()); + } + } { use crate::keystore::{KeystoreBackend, OnePassword}; diff --git a/rust/crates/core/src/keystore.rs b/rust/crates/core/src/keystore.rs index a92825c2..9f4f8e46 100644 --- a/rust/crates/core/src/keystore.rs +++ b/rust/crates/core/src/keystore.rs @@ -5,4 +5,7 @@ pub use pay_keystore::*; #[cfg(target_os = "macos")] pub use pay_keystore::backends::apple_keychain::AppleKeychain; +#[cfg(target_os = "linux")] +pub use pay_keystore::backends::gnome_keyring::GnomeKeyring; + pub use pay_keystore::backends::onepassword::OnePassword; diff --git a/rust/crates/core/src/signer.rs b/rust/crates/core/src/signer.rs index e9f18b92..89afb939 100644 --- a/rust/crates/core/src/signer.rs +++ b/rust/crates/core/src/signer.rs @@ -15,11 +15,13 @@ pub fn load_signer(source: &str) -> Result { /// Load a `MemorySigner` with a custom reason string. /// -/// For Keychain sources, the reason is shown in the Touch ID prompt. +/// For Keychain/GNOME Keyring sources, the reason is shown in the auth prompt. /// For 1Password and file-based keypairs, the reason is ignored. pub fn load_signer_with_reason(source: &str, reason: &str) -> Result { if let Some(account) = source.strip_prefix("keychain:") { load_from_keychain(account, reason) + } else if let Some(account) = source.strip_prefix("gnome-keyring:") { + load_from_gnome_keyring(account, reason) } else if let Some(account) = source.strip_prefix("1password:") { load_from_1password(account, reason) } else { @@ -52,6 +54,25 @@ fn load_from_keychain(_account: &str, _reason: &str) -> Result { )) } +#[cfg(target_os = "linux")] +fn load_from_gnome_keyring(account: &str, reason: &str) -> Result { + use crate::keystore::{GnomeKeyring, KeystoreBackend}; + + let bytes = GnomeKeyring + .load_keypair(account, reason) + .map_err(|e| Error::Config(format!("GNOME Keyring: {e}")))?; + + MemorySigner::from_bytes(&bytes) + .map_err(|e| Error::Config(format!("Invalid keypair from GNOME Keyring: {e}"))) +} + +#[cfg(not(target_os = "linux"))] +fn load_from_gnome_keyring(_account: &str, _reason: &str) -> Result { + Err(Error::Config( + "GNOME Keyring not available on this platform".to_string(), + )) +} + fn load_from_1password(account: &str, reason: &str) -> Result { use crate::keystore::{KeystoreBackend, OnePassword}; diff --git a/rust/crates/keystore/Cargo.toml b/rust/crates/keystore/Cargo.toml index cc7136d7..db029d89 100644 --- a/rust/crates/keystore/Cargo.toml +++ b/rust/crates/keystore/Cargo.toml @@ -9,3 +9,9 @@ serde = { workspace = true } serde_json = { workspace = true } shellexpand = { workspace = true } thiserror = { workspace = true } +zeroize = { workspace = true } + +[target.'cfg(target_os = "linux")'.dependencies] +secret-service = { version = "3", features = ["rt-tokio-crypto-rust"] } +tokio = { version = "1", features = ["rt"] } +zbus = { version = "3", default-features = false, features = ["tokio"] } diff --git a/rust/crates/keystore/src/backends/apple_keychain.rs b/rust/crates/keystore/src/backends/apple_keychain.rs index 5edb5c87..599f6652 100644 --- a/rust/crates/keystore/src/backends/apple_keychain.rs +++ b/rust/crates/keystore/src/backends/apple_keychain.rs @@ -14,7 +14,7 @@ use std::path::PathBuf; use std::process::Command; -use crate::{Error, KeystoreBackend, Result, SyncMode}; +use crate::{Error, KeystoreBackend, Result, SyncMode, Zeroizing}; /// macOS Keychain backend with hardware-enforced Touch ID. pub struct AppleKeychain; @@ -56,9 +56,9 @@ impl KeystoreBackend for AppleKeychain { hex_to_bytes(hex.trim()) } - fn load_keypair(&self, account: &str, reason: &str) -> Result> { + fn load_keypair(&self, account: &str, reason: &str) -> Result>> { let hex = helper_run(&["read-protected", account, reason])?; - hex_to_bytes(hex.trim()) + hex_to_bytes(hex.trim()).map(Zeroizing::new) } } diff --git a/rust/crates/keystore/src/backends/gnome_keyring.rs b/rust/crates/keystore/src/backends/gnome_keyring.rs new file mode 100644 index 00000000..0ea496ff --- /dev/null +++ b/rust/crates/keystore/src/backends/gnome_keyring.rs @@ -0,0 +1,369 @@ +//! GNOME Keyring backend — stores keypairs via the Secret Service D-Bus API +//! (org.freedesktop.secrets), available on GNOME and KDE (Plasma 6+) desktops. +//! +//! Storage layout: +//! Collection: "pay" (separate keyring, locked at rest) +//! label: "pay/" +//! attributes: service = "pay.sh", account = "" +//! secret: 64-byte raw keypair +//! +//! Default collection (login keyring, no auth): +//! label: "pay/.pubkey" +//! attributes: service = "pay.sh", account = ".pubkey" +//! secret: 32-byte raw public key +//! +//! Auth gate: polkit action `sh.pay.unlock-keypair` is checked before every +//! `load_keypair` call via `org.freedesktop.PolicyKit1`. Polkit uses PAM +//! internally, so fingerprint (pam_fprintd) and password both work. +//! This is equivalent to Touch ID on macOS — polkit never caches between calls. +//! +//! Requires the polkit action file to be installed: +//! sudo cp linux/polkit/sh.pay.unlock-keypair.policy \ +//! /usr/share/polkit-1/actions/ +//! For snap installs this is handled automatically. + +use std::collections::HashMap; + +use secret_service::{EncryptionType, SecretService}; + +use crate::{Error, KeystoreBackend, Result, SyncMode, Zeroizing}; + +const SERVICE_ATTR: &str = "pay.sh"; +const COLLECTION_LABEL: &str = "pay"; +const POLKIT_ACTION: &str = "sh.pay.unlock-keypair"; + +pub struct GnomeKeyring; + +impl GnomeKeyring { + /// Check if the Secret Service D-Bus interface is reachable. + /// Returns false on headless/server systems where GNOME Keyring is not running. + pub fn is_available() -> bool { + run(async { SecretService::connect(EncryptionType::Plain).await.is_ok() }) + } +} + +impl KeystoreBackend for GnomeKeyring { + fn import(&self, account: &str, keypair_bytes: &[u8], _sync: SyncMode) -> Result<()> { + if keypair_bytes.len() != 64 { + return Err(Error::InvalidKeypair(format!( + "expected 64 bytes, got {}", + keypair_bytes.len() + ))); + } + let account = account.to_owned(); + let keypair_bytes = keypair_bytes.to_owned(); + run(async move { + // Polkit authentication — prompts before writing the keypair. + polkit_authenticate("store keypair").await?; + + let ss = connect().await?; + + // Public key goes into the default (login) collection — readable without auth. + let default = ss.get_default_collection().await.map_err(ss_err)?; + store_item(&default, &pubkey_account(&account), &keypair_bytes[32..64]).await?; + + // Full keypair goes into the locked "pay" collection. + // If the collection is new, GNOME Keyring shows a "set keyring password" dialog. + let col = get_or_create_collection(&ss).await?; + ensure_unlocked(&col).await?; + store_item(&col, &account, &keypair_bytes).await?; + col.lock().await.map_err(ss_err)?; + + Ok(()) + }) + } + + fn exists(&self, account: &str) -> bool { + let account = account.to_owned(); + run(async move { + let Ok(ss) = connect().await else { + return false; + }; + // Both must be present: public key in the default collection AND + // the pay collection itself. Checking only the public key gives a + // false positive when a previous failed setup wrote the public key + // but never created the pay collection. + let Ok(default) = ss.get_default_collection().await else { + return false; + }; + let pubkey_exists = default + .search_items(attrs(&pubkey_account(&account))) + .await + .map(|items| !items.is_empty()) + .unwrap_or(false); + + pubkey_exists && get_collection(&ss).await.is_some() + }) + } + + fn delete(&self, account: &str) -> Result<()> { + let account = account.to_owned(); + run(async move { + polkit_authenticate("delete keypair").await?; + + let ss = connect().await?; + + // Delete keypair from the pay collection (requires unlock). + if let Some(col) = get_collection(&ss).await { + ensure_unlocked(&col).await?; + for item in col.search_items(attrs(&account)).await.map_err(ss_err)? { + item.delete().await.map_err(ss_err)?; + } + col.lock().await.map_err(ss_err)?; + } + + // Delete public key from the default collection (no unlock needed). + let default = ss.get_default_collection().await.map_err(ss_err)?; + for item in default + .search_items(attrs(&pubkey_account(&account))) + .await + .map_err(ss_err)? + { + item.delete().await.map_err(ss_err)?; + } + + Ok(()) + }) + } + + fn pubkey(&self, account: &str) -> Result> { + let account = account.to_owned(); + run(async move { + let ss = connect().await?; + let default = ss.get_default_collection().await.map_err(ss_err)?; + let items = default + .search_items(attrs(&pubkey_account(&account))) + .await + .map_err(ss_err)?; + let item = items + .first() + .ok_or_else(|| Error::Backend("public key not found".to_string()))?; + item.get_secret().await.map_err(ss_err) + }) + } + + fn load_keypair(&self, account: &str, reason: &str) -> Result>> { + let account = account.to_owned(); + let reason = reason.to_owned(); + run(async move { + // Polkit authentication — always prompts (password or fingerprint via PAM). + // This is the auth gate; GNOME Keyring is only used for encrypted storage. + polkit_authenticate(&reason).await?; + + let ss = connect().await?; + let col = get_collection(&ss).await.ok_or_else(|| { + Error::Backend("pay keyring not found — run `pay setup` first".to_string()) + })?; + + ensure_unlocked(&col).await?; + + let items = col.search_items(attrs(&account)).await.map_err(ss_err)?; + let item = items + .first() + .ok_or_else(|| Error::Backend("keypair not found".to_string()))?; + let secret = Zeroizing::new(item.get_secret().await.map_err(ss_err)?); + + // Lock so the keypair is encrypted at rest between calls. + col.lock().await.map_err(ss_err)?; + + Ok(secret) + }) + } +} + +// ── Polkit auth ─────────────────────────────────────────────────────────────── + +/// Authenticate via polkit before reading the keypair. +/// +/// Polkit uses PAM internally, so this supports both password and fingerprint +/// (if pam_fprintd is enabled via `pam-auth-update --enable fprintd`). +/// +/// Requires the action file to be installed: +/// sudo cp linux/polkit/sh.pay.unlock-keypair.policy /usr/share/polkit-1/actions/ +async fn polkit_authenticate(_reason: &str) -> Result<()> { + use zbus::zvariant::{OwnedValue, Value}; + + let conn = zbus::Connection::system() + .await + .map_err(|e| Error::Backend(format!("D-Bus system bus: {e}")))?; + + let pid = std::process::id(); + let start_time = process_start_time()?; + + // Subject: the current process ("unix-process" with pid + start-time). + // start-time prevents PID reuse attacks. + let subject_details: HashMap = [ + ( + "pid".to_owned(), + OwnedValue::try_from(Value::new(pid)) + .map_err(|e| Error::Backend(format!("polkit pid: {e}")))?, + ), + ( + "start-time".to_owned(), + OwnedValue::try_from(Value::new(start_time)) + .map_err(|e| Error::Backend(format!("polkit start-time: {e}")))?, + ), + ] + .into(); + + // details a{ss}: must be empty for unprivileged callers — only uid 0 or + // the action owner may pass custom details to CheckAuthorization. + let details: HashMap = HashMap::new(); + + // flags: 0x1 = AllowUserInteraction (shows the auth dialog). + let flags: u32 = 0x1; + + let reply = conn + .call_method( + Some("org.freedesktop.PolicyKit1"), + "/org/freedesktop/PolicyKit1/Authority", + Some("org.freedesktop.PolicyKit1.Authority"), + "CheckAuthorization", + &( + ("unix-process", subject_details), + POLKIT_ACTION, + details, + flags, + "", // cancellation_id + ), + ) + .await + .map_err(|e| { + let msg = e.to_string(); + if msg.contains("No such action") || msg.contains("not registered") { + Error::Backend(format!( + "polkit action '{POLKIT_ACTION}' is not installed.\n\ + Install it with:\n\ + \x20 sudo cp linux/polkit/sh.pay.unlock-keypair.policy \\\n\ + \x20 /usr/share/polkit-1/actions/" + )) + } else { + Error::Backend(format!("polkit: {msg}")) + } + })?; + + let (authorized, _, _): (bool, bool, HashMap) = reply + .body() + .map_err(|e| Error::Backend(format!("polkit response: {e}")))?; + + if authorized { + Ok(()) + } else { + Err(Error::AuthDenied("authentication cancelled".to_string())) + } +} + +/// Read the process start time from /proc/self/stat (field 22). +/// Used to prevent PID-reuse attacks in the polkit subject. +fn process_start_time() -> Result { + let stat = std::fs::read_to_string("/proc/self/stat") + .map_err(|e| Error::Backend(format!("read /proc/self/stat: {e}")))?; + // Format: pid (comm) state ppid ... starttime + // The comm field can contain spaces and parentheses, so find the last ')'. + let after_comm = stat + .rfind(')') + .ok_or_else(|| Error::Backend("parse /proc/self/stat".to_string()))?; + let fields: Vec<&str> = stat[after_comm + 2..].split_ascii_whitespace().collect(); + // starttime is field 22 overall = index 19 after pid and comm + fields + .get(19) + .and_then(|s| s.parse::().ok()) + .ok_or_else(|| Error::Backend("parse /proc/self/stat: starttime field missing".to_string())) +} + +// ── Secret Service helpers ──────────────────────────────────────────────────── + +async fn connect() -> Result> { + SecretService::connect(EncryptionType::Plain) + .await + .map_err(|e| Error::Backend(format!("Secret Service unavailable: {e}"))) +} + +/// Find the "pay" collection by label, or `None` if it doesn't exist. +/// +/// GNOME Keyring only supports the `"default"` alias; custom aliases return +/// `NotSupported`. We enumerate all collections and match by label instead. +async fn get_collection<'a>(ss: &'a SecretService<'a>) -> Option> { + let collections = ss.get_all_collections().await.ok()?; + for col in collections { + if col + .get_label() + .await + .map(|l| l == COLLECTION_LABEL) + .unwrap_or(false) + { + return Some(col); + } + } + None +} + +/// Get the "pay" collection, creating it if absent. +/// +/// If new, GNOME Keyring shows a "set keyring password" dialog. +/// Empty alias is used since GNOME Keyring doesn't support custom alias names. +async fn get_or_create_collection<'a>( + ss: &'a SecretService<'a>, +) -> Result> { + if let Some(col) = get_collection(ss).await { + return Ok(col); + } + ss.create_collection(COLLECTION_LABEL, "") + .await + .map_err(ss_err) +} + +/// Unlock `col` if locked. Maps cancellation/denial to `Error::AuthDenied`. +async fn ensure_unlocked(col: &secret_service::Collection<'_>) -> Result<()> { + if col.is_locked().await.unwrap_or(true) { + col.unlock().await.map_err(|e| { + let msg = e.to_string().to_lowercase(); + if msg.contains("dismissed") || msg.contains("cancel") || msg.contains("denied") { + Error::AuthDenied("keyring unlock cancelled".to_string()) + } else { + Error::Backend(format!("unlock failed: {e}")) + } + })?; + } + Ok(()) +} + +async fn store_item( + col: &secret_service::Collection<'_>, + account: &str, + secret: &[u8], +) -> Result<()> { + col.create_item( + &format!("pay/{account}"), + attrs(account), + secret, + true, // replace existing item with same attributes + "application/octet-stream", + ) + .await + .map_err(ss_err) + .map(|_| ()) +} + +fn attrs(account: &str) -> HashMap<&str, &str> { + HashMap::from([("service", SERVICE_ATTR), ("account", account)]) +} + +fn pubkey_account(account: &str) -> String { + format!("{account}.pubkey") +} + +fn ss_err(e: secret_service::Error) -> Error { + Error::Backend(e.to_string()) +} + +fn run(future: F) -> T +where + F: std::future::Future, +{ + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("tokio runtime") + .block_on(future) +} diff --git a/rust/crates/keystore/src/backends/mod.rs b/rust/crates/keystore/src/backends/mod.rs index 33a55a08..456fc1f0 100644 --- a/rust/crates/keystore/src/backends/mod.rs +++ b/rust/crates/keystore/src/backends/mod.rs @@ -1,4 +1,7 @@ #[cfg(target_os = "macos")] pub mod apple_keychain; +#[cfg(target_os = "linux")] +pub mod gnome_keyring; + pub mod onepassword; diff --git a/rust/crates/keystore/src/backends/onepassword.rs b/rust/crates/keystore/src/backends/onepassword.rs index 8f5639c6..80da13ce 100644 --- a/rust/crates/keystore/src/backends/onepassword.rs +++ b/rust/crates/keystore/src/backends/onepassword.rs @@ -13,7 +13,7 @@ use std::process::Command; -use crate::{Error, KeystoreBackend, Result, SyncMode}; +use crate::{Error, KeystoreBackend, Result, SyncMode, Zeroizing}; const TAG: &str = "pay"; @@ -156,7 +156,7 @@ impl KeystoreBackend for OnePassword { hex_to_bytes(&hex) } - fn load_keypair(&self, account: &str, _reason: &str) -> Result> { + fn load_keypair(&self, account: &str, _reason: &str) -> Result>> { // 1Password handles its own auth (biometrics/password) via the `op` CLI. // The `reason` parameter is not used — 1Password shows its own prompt. let title = Self::item_title(account); @@ -176,7 +176,7 @@ impl KeystoreBackend for OnePassword { return Err(Error::Backend(format!("op item get failed: {err}"))); } let hex = String::from_utf8_lossy(&output.stdout).trim().to_string(); - hex_to_bytes(&hex) + hex_to_bytes(&hex).map(Zeroizing::new) } } diff --git a/rust/crates/keystore/src/lib.rs b/rust/crates/keystore/src/lib.rs index 475c26d6..36bb5d56 100644 --- a/rust/crates/keystore/src/lib.rs +++ b/rust/crates/keystore/src/lib.rs @@ -8,6 +8,7 @@ pub mod backends; mod error; pub use error::{Error, Result}; +pub use zeroize::Zeroizing; /// Controls whether the key syncs to cloud storage. #[derive(Debug, Clone, Copy, Default)] @@ -34,5 +35,6 @@ pub trait KeystoreBackend { fn pubkey(&self, account: &str) -> Result>; /// Load the full keypair (64 bytes). May trigger auth (Touch ID, password, etc.). - fn load_keypair(&self, account: &str, reason: &str) -> Result>; + /// Returns a `Zeroizing` wrapper that wipes the bytes from memory on drop. + fn load_keypair(&self, account: &str, reason: &str) -> Result>>; } diff --git a/rust/rust-toolchain.toml b/rust/rust-toolchain.toml new file mode 100644 index 00000000..292fe499 --- /dev/null +++ b/rust/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "stable" From 006ba0706234074f9c0ca4c0c2881d4aa1a51dd3 Mon Sep 17 00:00:00 2001 From: Ludo Galabru Date: Fri, 27 Mar 2026 11:18:17 -0400 Subject: [PATCH 2/5] docs: polkit note --- README.md | 19 ++++++++++++++++--- .../polkit/sh.pay.unlock-keypair.policy | 19 +++++++++++++++++++ .../keystore/src/backends/gnome_keyring.rs | 6 +++--- 3 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 rust/config/polkit/sh.pay.unlock-keypair.policy diff --git a/README.md b/README.md index 9aa85830..a837a328 100644 --- a/README.md +++ b/README.md @@ -30,19 +30,26 @@ Supports both live payment standards on Solana: SOL and SPL tokens (USDC, USDT, etc.) are supported out of the box. -### Touch ID & 1Password Key Storage +### Touch ID, GNOME Keyring & 1Password Key Storage Your keys never touch disk in plaintext. `pay` stores keypairs in: - **macOS Keychain** with Touch ID biometric protection +- **GNOME Keyring** with password/fingerprint prompt on every use (Linux) - **1Password** vault integration (cross-platform) - **File-based** fallback for CI and scripting ```sh -pay setup --backend keychain # Touch ID protected -pay setup --backend 1password # Cross-platform vault +pay setup # Touch ID on macOS, GNOME Keyring on Linux, or choose 1Password ``` +> **Linux note:** GNOME Keyring auth uses polkit, which requires a one-time setup step: +> ```sh +> sudo cp rust/config/polkit/sh.pay.unlock-keypair.policy /usr/share/polkit-1/actions/ +> ``` +> This grants `pay` the right to prompt for your password or fingerprint before +> accessing the keypair. Without it, `pay topup` and `pay curl` will error. + ### Session Budgets via TUI Set a spending cap and expiration before making requests. The interactive TUI lets you control exactly how much you're willing to spend per session — no surprise charges. @@ -79,6 +86,12 @@ cd pay/rust cargo install --path crates/cli ``` +**Linux only** — install the polkit action to enable keypair auth: + +```sh +sudo cp rust/config/polkit/sh.pay.unlock-keypair.policy /usr/share/polkit-1/actions/ +``` + ### Verify ```sh diff --git a/rust/config/polkit/sh.pay.unlock-keypair.policy b/rust/config/polkit/sh.pay.unlock-keypair.policy new file mode 100644 index 00000000..05d9cc40 --- /dev/null +++ b/rust/config/polkit/sh.pay.unlock-keypair.policy @@ -0,0 +1,19 @@ + + + + Solana Foundation + https://solana.org + + Authorize payment with your Solana keypair + Authentication is required to authorize a Solana payment + + + auth_self + auth_self + auth_self + + + diff --git a/rust/crates/keystore/src/backends/gnome_keyring.rs b/rust/crates/keystore/src/backends/gnome_keyring.rs index 0ea496ff..a5abead3 100644 --- a/rust/crates/keystore/src/backends/gnome_keyring.rs +++ b/rust/crates/keystore/src/backends/gnome_keyring.rs @@ -18,7 +18,7 @@ //! This is equivalent to Touch ID on macOS — polkit never caches between calls. //! //! Requires the polkit action file to be installed: -//! sudo cp linux/polkit/sh.pay.unlock-keypair.policy \ +//! sudo cp rust/config/polkit/sh.pay.unlock-keypair.policy \ //! /usr/share/polkit-1/actions/ //! For snap installs this is handled automatically. @@ -179,7 +179,7 @@ impl KeystoreBackend for GnomeKeyring { /// (if pam_fprintd is enabled via `pam-auth-update --enable fprintd`). /// /// Requires the action file to be installed: -/// sudo cp linux/polkit/sh.pay.unlock-keypair.policy /usr/share/polkit-1/actions/ +/// sudo cp rust/config/polkit/sh.pay.unlock-keypair.policy /usr/share/polkit-1/actions/ async fn polkit_authenticate(_reason: &str) -> Result<()> { use zbus::zvariant::{OwnedValue, Value}; @@ -234,7 +234,7 @@ async fn polkit_authenticate(_reason: &str) -> Result<()> { Error::Backend(format!( "polkit action '{POLKIT_ACTION}' is not installed.\n\ Install it with:\n\ - \x20 sudo cp linux/polkit/sh.pay.unlock-keypair.policy \\\n\ + \x20 sudo cp rust/config/polkit/sh.pay.unlock-keypair.policy \\\n\ \x20 /usr/share/polkit-1/actions/" )) } else { From a8d51cdb38cd69fd9f7b18a4807794d704c6e5bc Mon Sep 17 00:00:00 2001 From: Ludo Galabru Date: Fri, 27 Mar 2026 13:27:14 -0400 Subject: [PATCH 3/5] fix: use infallible From instead of try_from for OwnedValue conversion Co-Authored-By: Claude Opus 4.6 (1M context) --- rust/crates/keystore/src/backends/gnome_keyring.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/rust/crates/keystore/src/backends/gnome_keyring.rs b/rust/crates/keystore/src/backends/gnome_keyring.rs index a5abead3..d244c88c 100644 --- a/rust/crates/keystore/src/backends/gnome_keyring.rs +++ b/rust/crates/keystore/src/backends/gnome_keyring.rs @@ -193,15 +193,10 @@ async fn polkit_authenticate(_reason: &str) -> Result<()> { // Subject: the current process ("unix-process" with pid + start-time). // start-time prevents PID reuse attacks. let subject_details: HashMap = [ - ( - "pid".to_owned(), - OwnedValue::try_from(Value::new(pid)) - .map_err(|e| Error::Backend(format!("polkit pid: {e}")))?, - ), + ("pid".to_owned(), OwnedValue::from(Value::new(pid))), ( "start-time".to_owned(), - OwnedValue::try_from(Value::new(start_time)) - .map_err(|e| Error::Backend(format!("polkit start-time: {e}")))?, + OwnedValue::from(Value::new(start_time)), ), ] .into(); From 14c1f199b0abaa1cae1908792b8e84971c8ab549 Mon Sep 17 00:00:00 2001 From: Ludo Galabru Date: Fri, 27 Mar 2026 15:54:47 -0400 Subject: [PATCH 4/5] Update rust/crates/keystore/src/backends/gnome_keyring.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- rust/crates/keystore/src/backends/gnome_keyring.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/crates/keystore/src/backends/gnome_keyring.rs b/rust/crates/keystore/src/backends/gnome_keyring.rs index d244c88c..eb440ca1 100644 --- a/rust/crates/keystore/src/backends/gnome_keyring.rs +++ b/rust/crates/keystore/src/backends/gnome_keyring.rs @@ -38,7 +38,7 @@ impl GnomeKeyring { /// Check if the Secret Service D-Bus interface is reachable. /// Returns false on headless/server systems where GNOME Keyring is not running. pub fn is_available() -> bool { - run(async { SecretService::connect(EncryptionType::Plain).await.is_ok() }) + run(async { SecretService::connect(EncryptionType::Dh).await.is_ok() }) } } From e0b6a0ec6dcf323bb629f9df74afa9a8ec6fca92 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:56:58 +0000 Subject: [PATCH 5/5] fix: use EncryptionType::Dh in Secret Service connect() for encrypted D-Bus session Agent-Logs-Url: https://github.com/solana-foundation/pay/sessions/2397098d-b0fb-4da5-a692-695dce568de7 Co-authored-by: lgalabru <87777+lgalabru@users.noreply.github.com> --- rust/crates/keystore/src/backends/gnome_keyring.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/crates/keystore/src/backends/gnome_keyring.rs b/rust/crates/keystore/src/backends/gnome_keyring.rs index eb440ca1..30d7c839 100644 --- a/rust/crates/keystore/src/backends/gnome_keyring.rs +++ b/rust/crates/keystore/src/backends/gnome_keyring.rs @@ -269,7 +269,7 @@ fn process_start_time() -> Result { // ── Secret Service helpers ──────────────────────────────────────────────────── async fn connect() -> Result> { - SecretService::connect(EncryptionType::Plain) + SecretService::connect(EncryptionType::Dh) .await .map_err(|e| Error::Backend(format!("Secret Service unavailable: {e}"))) }