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/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/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/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/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..30d7c839
--- /dev/null
+++ b/rust/crates/keystore/src/backends/gnome_keyring.rs
@@ -0,0 +1,364 @@
+//! 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 rust/config/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::Dh).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 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};
+
+ 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::from(Value::new(pid))),
+ (
+ "start-time".to_owned(),
+ OwnedValue::from(Value::new(start_time)),
+ ),
+ ]
+ .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 rust/config/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::Dh)
+ .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