diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88cf73d..d76b4ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,10 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Install Linux keyring build deps + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev pkg-config + - name: Install Rust uses: dtolnay/rust-toolchain@stable @@ -27,13 +31,18 @@ jobs: uses: Swatinem/rust-cache@v2 - name: Run tests - run: cargo test --verbose + run: cargo test --verbose --features secure-storage - name: Check formatting run: cargo fmt --check - name: Run clippy - run: cargo clippy -- -D warnings + run: cargo clippy --features secure-storage -- -D warnings + + # Keep a default-features build green so crates.io installs without + # secure-storage still compile. + - name: Build without secure-storage + run: cargo build --verbose build: name: Build @@ -41,6 +50,9 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Install Linux keyring build deps + run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev pkg-config + - name: Install Rust uses: dtolnay/rust-toolchain@stable @@ -49,4 +61,4 @@ jobs: uses: Swatinem/rust-cache@v2 - name: Build release - run: cargo build --release + run: cargo build --release --features secure-storage diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a724c0d..a773c83 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,6 +35,10 @@ jobs: steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - name: Install Linux keyring build deps + if: runner.os == 'Linux' && !matrix.use_cross + run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev pkg-config + - name: Install Rust uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable @@ -47,11 +51,11 @@ jobs: - name: Build (cross) if: matrix.use_cross - run: cross build --release --target ${{ matrix.target }} + run: cross build --release --features secure-storage --target ${{ matrix.target }} - name: Build (native) if: ${{ !matrix.use_cross }} - run: cargo build --release --target ${{ matrix.target }} + run: cargo build --release --features secure-storage --target ${{ matrix.target }} - name: Package (Unix) if: runner.os != 'Windows' diff --git a/Cargo.lock b/Cargo.lock index 347a922..d32266a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,17 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -76,6 +87,123 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[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-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -109,6 +237,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", + "piper", +] + [[package]] name = "bumpalo" version = "3.19.1" @@ -121,12 +271,27 @@ version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[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.51" @@ -163,6 +328,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.5.53" @@ -228,6 +403,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[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.15.11" @@ -276,6 +460,12 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + [[package]] name = "crypto-common" version = "0.1.7" @@ -307,6 +497,35 @@ dependencies = [ "memchr", ] +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "dbus-secret-service" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6" +dependencies = [ + "aes", + "block-padding", + "cbc", + "dbus", + "fastrand", + "hkdf", + "num", + "once_cell", + "sha2", + "zeroize", +] + [[package]] name = "dialoguer" version = "0.11.0" @@ -378,6 +597,33 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -394,6 +640,27 @@ dependencies = [ "windows-sys 0.61.2", ] +[[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", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -469,6 +736,19 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.31" @@ -578,12 +858,27 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" 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" @@ -833,6 +1128,16 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -896,7 +1201,14 @@ version = "3.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" dependencies = [ + "byteorder", + "dbus-secret-service", + "linux-keyutils", "log", + "secret-service", + "security-framework 2.11.1", + "security-framework 3.6.0", + "windows-sys 0.60.2", "zeroize", ] @@ -912,6 +1224,15 @@ version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + [[package]] name = "libredox" version = "0.1.12" @@ -953,6 +1274,16 @@ dependencies = [ "url", ] +[[package]] +name = "linux-keyutils" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83270a18e9f90d0707c41e9f35efada77b64c0e6f3f1810e71c8368a864d5590" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -992,6 +1323,15 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -1009,6 +1349,82 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "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-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "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" @@ -1053,6 +1469,16 @@ 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 = "papergrid" version = "0.17.0" @@ -1064,6 +1490,12 @@ dependencies = [ "unicode-width", ] +[[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" @@ -1111,6 +1543,37 @@ 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", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -1129,6 +1592,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.6+spec-1.1.0", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -1438,7 +1910,7 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 3.6.0", ] [[package]] @@ -1489,6 +1961,38 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "secret-service" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4d35ad99a181be0a60ffcbe85d680d98f87bdc4d7644ade319b87076b9dbfd4" +dependencies = [ + "aes", + "cbc", + "futures-util", + "generic-array", + "hkdf", + "num", + "once_cell", + "rand 0.8.6", + "serde", + "sha2", + "zbus", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + [[package]] name = "security-framework" version = "3.6.0" @@ -1555,6 +2059,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", +] + [[package]] name = "serde_spanned" version = "0.6.9" @@ -1576,6 +2091,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.9" @@ -1637,6 +2163,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" @@ -1871,8 +2403,8 @@ checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", ] [[package]] @@ -1884,6 +2416,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -1893,9 +2434,30 @@ dependencies = [ "indexmap", "serde", "serde_spanned", - "toml_datetime", + "toml_datetime 0.6.11", "toml_write", - "winnow", + "winnow 0.7.14", +] + +[[package]] +name = "toml_edit" +version = "0.25.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0db3bae107c9522f86d361697dee1d7386a2ddcf659d5aea5159819a21a3c4a7" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.4", ] [[package]] @@ -1956,9 +2518,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing-core" version = "0.1.36" @@ -1980,6 +2554,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", + "tempfile", + "windows-sys 0.60.2", +] + [[package]] name = "unicode-ident" version = "1.0.22" @@ -2453,6 +3038,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.46.0" @@ -2465,6 +3059,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 = "yoke" version = "0.8.1" @@ -2488,6 +3092,62 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +dependencies = [ + "async-broadcast", + "async-process", + "async-recursion", + "async-trait", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix", + "ordered-stream", + "rand 0.8.6", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.8.38" @@ -2534,6 +3194,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "zerotrie" @@ -2573,3 +3247,40 @@ name = "zmij" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aac060176f7020d62c3bcc1cdbcec619d54f48b07ad1963a3f80ce7a0c17755f" + +[[package]] +name = "zvariant" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 2e28078..b84a3bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ chrono = { version = "0.4", features = ["serde"] } regex = "1" rand = "0.8" csv = "1" -keyring = { version = "3", optional = true } +keyring = { version = "3", optional = true, default-features = false, features = ["apple-native", "windows-native", "linux-native-sync-persistent", "crypto-rust"] } sha2 = "0.10" open = "5" url = "2" diff --git a/Cross.toml b/Cross.toml new file mode 100644 index 0000000..428c24f --- /dev/null +++ b/Cross.toml @@ -0,0 +1,5 @@ +[target.aarch64-unknown-linux-gnu] +pre-build = [ + "dpkg --add-architecture $CROSS_DEB_ARCH", + "apt-get update && apt-get --assume-yes install libdbus-1-dev:$CROSS_DEB_ARCH pkg-config", +] diff --git a/README.md b/README.md index ed8c9f4..22aed2b 100644 --- a/README.md +++ b/README.md @@ -439,7 +439,7 @@ linear-cli auth revoke # Revoke OAuth tokens linear-cli auth logout # Remove stored credentials ``` -> On macOS, `--secure` works best with an official signed release binary. Locally built or frequently rebuilt CLI binaries can trigger repeated Keychain prompts and may fail keychain readback verification. If that happens, use plain `linear-cli auth oauth` or `LINEAR_API_KEY` instead. +> `--secure` needs a build with `--features secure-storage` (enables OS backends: Keychain, Credential Manager, Secret Service). On macOS Keychain, unsigned local builds can still prompt repeatedly or fail readback — if that happens, use an official release, plain `linear-cli auth oauth`, or `LINEAR_API_KEY`. **Auth priority:** `LINEAR_API_KEY` env var > OS keyring > OAuth tokens > config file API key. diff --git a/docs/manual-release.md b/docs/manual-release.md index 48da67d..750992d 100644 --- a/docs/manual-release.md +++ b/docs/manual-release.md @@ -86,22 +86,28 @@ Confirm crates.io shows `0.3.16` before moving on. ## Releasing `v0.3.17` and newer -Current `master` uses `reqwest` with `rustls`, so the Linux aarch64 build no longer depends on target OpenSSL packages. +Current `master` uses `reqwest` with `rustls`. Official binaries should build with +`--features secure-storage` so Keychain / Credential Manager / Secret Service work. + +Linux builds need `libdbus-1-dev` (and `pkg-config`). For aarch64 cross builds, use the +repo-root `Cross.toml` which installs `libdbus-1-dev:$CROSS_DEB_ARCH`. ### Local builds ```bash -cargo build --release --target x86_64-unknown-linux-gnu +sudo apt-get install -y libdbus-1-dev pkg-config # Linux hosts only + +cargo build --release --features secure-storage --target x86_64-unknown-linux-gnu tar -C target/x86_64-unknown-linux-gnu/release -czf linear-cli-x86_64-unknown-linux-gnu.tar.gz linear-cli -cross build --release --target aarch64-unknown-linux-gnu +cross build --release --features secure-storage --target aarch64-unknown-linux-gnu tar -C target/aarch64-unknown-linux-gnu/release -czf linear-cli-aarch64-unknown-linux-gnu.tar.gz linear-cli -cargo xwin build --release --target x86_64-pc-windows-msvc +cargo xwin build --release --features secure-storage --target x86_64-pc-windows-msvc cd target/x86_64-pc-windows-msvc/release && 7z a ../../../linear-cli-x86_64-pc-windows-msvc.zip linear-cli.exe ``` -Build both Apple targets on a Mac with the same commands from the previous section. +Build both Apple targets on a Mac with `--features secure-storage` as well. ### Publish first, then release diff --git a/src/commands/auth.rs b/src/commands/auth.rs index 6b47142..6805151 100644 --- a/src/commands/auth.rs +++ b/src/commands/auth.rs @@ -544,9 +544,13 @@ async fn oauth_login( #[cfg(feature = "secure-storage")] if secure { - config::save_oauth_config_secure(&profile, &oauth_config).context( - "Secure OAuth storage failed. On macOS, locally built or unsigned binaries may not be able to read back Keychain items. Try the official signed release or fall back to plain `linear-cli auth oauth`.", - )?; + config::save_oauth_config_secure(&profile, &oauth_config).with_context(|| { + format!( + "Secure OAuth storage failed using {}. \ + On macOS Keychain, unsigned local builds can fail readback — try an official release or plain `linear-cli auth oauth`.", + crate::keyring::backend_name() + ) + })?; if output.is_json() || output.has_template() { print_json_owned( diff --git a/src/commands/setup.rs b/src/commands/setup.rs index f064556..96dbba4 100644 --- a/src/commands/setup.rs +++ b/src/commands/setup.rs @@ -75,10 +75,7 @@ pub async fn handle(output: &OutputOptions) -> Result<()> { if let Some(ref key) = saved_team { config::set_default_team(Some(key))?; println!(" Default team saved: {}", key); - println!( - " Tip: Override with -t {} or LINEAR_CLI_TEAM={}", - key, key - ); + println!(" Tip: Override with -t {} or LINEAR_CLI_TEAM={}", key, key); } else { println!(" Invalid selection, skipping."); } diff --git a/src/config.rs b/src/config.rs index 0745cec..3d42561 100644 --- a/src/config.rs +++ b/src/config.rs @@ -806,9 +806,7 @@ mod tests { token_type: "Bearer".to_string(), scopes: vec!["read".to_string(), "write".to_string()], }); - config - .workspaces - .insert("oauth-test".to_string(), oauth_ws); + config.workspaces.insert("oauth-test".to_string(), oauth_ws); let toml_str = toml::to_string_pretty(&config).unwrap(); let parsed: Config = toml::from_str(&toml_str).unwrap(); @@ -829,9 +827,10 @@ mod tests { current: Some("default".to_string()), ..Default::default() }; - config - .workspaces - .insert("default".to_string(), Workspace::with_api_key("lin_api_key")); + config.workspaces.insert( + "default".to_string(), + Workspace::with_api_key("lin_api_key"), + ); let toml_str = toml::to_string_pretty(&config).unwrap(); assert!( diff --git a/src/keyring.rs b/src/keyring.rs index 40b3413..9408e6a 100644 --- a/src/keyring.rs +++ b/src/keyring.rs @@ -1,123 +1,155 @@ //! Secure API key storage using OS keyring. //! //! This module provides cross-platform credential storage: -//! - macOS: Keychain -//! - Windows: Credential Manager -//! - Linux: Secret Service (requires D-Bus and a keyring daemon) +//! - macOS: Keychain (`apple-native`) +//! - Windows: Credential Manager (`windows-native`) +//! - Linux: keyutils + Secret Service (`linux-native-sync-persistent`) +//! +//! keyring 3.x has no default platform backends. Without those features it +//! silently uses an in-memory mock store — see issue #33. -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; const SERVICE_NAME: &str = "linear-cli"; +const OAUTH_SERVICE_NAME: &str = "linear-cli-oauth"; -/// Get an API key from the keyring for a profile. -/// Returns Ok(None) if no key is stored, Ok(Some(key)) if found. -pub fn get_key(profile: &str) -> Result> { - let entry = - keyring::Entry::new(SERVICE_NAME, profile).context("Failed to create keyring entry")?; +fn entry(service: &str, profile: &str) -> Result { + keyring::Entry::new(service, profile).context("Failed to create keyring entry") +} + +fn is_mock_entry(entry: &keyring::Entry) -> bool { + entry + .get_credential() + .downcast_ref::() + .is_some() +} + +/// Human-readable name of the expected OS backend on this target. +pub fn backend_name() -> &'static str { + if cfg!(target_os = "macos") { + "macOS Keychain" + } else if cfg!(target_os = "windows") { + "Windows Credential Manager" + } else if cfg!(target_os = "linux") { + "Linux keyutils + Secret Service" + } else { + "platform credential store" + } +} + +/// Reject keyring's in-memory mock backend (non-persistent). +fn require_persistent_backend() -> Result<()> { + let probe = entry(SERVICE_NAME, "__linear_cli_backend_probe__")?; + if is_mock_entry(&probe) { + bail!( + "secure-storage is using keyring's in-memory mock backend instead of {backend}. \ + Credentials will not persist. Rebuild with `--features secure-storage` after \ + enabling keyring platform features (apple-native, windows-native, \ + linux-native-sync-persistent).", + backend = backend_name() + ); + } + Ok(()) +} + +fn get_secret(service: &str, profile: &str, fallback_label: &str) -> Result> { + let entry = entry(service, profile)?; + if is_mock_entry(&entry) { + // Treat mock as missing so auth falls through; writes must call require_persistent_backend. + return Ok(None); + } match entry.get_password() { Ok(password) => Ok(Some(password)), Err(keyring::Error::NoEntry) => Ok(None), Err(keyring::Error::NoStorageAccess(_)) => { if !crate::output::is_quiet() { - eprintln!("Warning: Keyring not available, falling back to config file"); + eprintln!("Warning: Keyring not available, falling back to {fallback_label}"); } Ok(None) } Err(e) => { if !crate::output::is_quiet() { - eprintln!( - "Warning: Keyring error ({}), falling back to config file", - e - ); + eprintln!("Warning: Keyring error ({e}), falling back to {fallback_label}"); } Ok(None) } } } -/// Store an API key in the keyring for a profile. -pub fn set_key(profile: &str, api_key: &str) -> Result<()> { - let entry = - keyring::Entry::new(SERVICE_NAME, profile).context("Failed to create keyring entry")?; +fn set_secret(service: &str, profile: &str, secret: &str) -> Result<()> { + require_persistent_backend()?; + entry(service, profile)? + .set_password(secret) + .with_context(|| format!("Failed to store secret in {}", backend_name()))?; + Ok(()) +} - entry - .set_password(api_key) - .context("Failed to store API key in keyring")?; +fn delete_secret(service: &str, profile: &str) -> Result<()> { + let entry = entry(service, profile)?; + if is_mock_entry(&entry) { + return Ok(()); + } + match entry.delete_credential() { + Ok(()) | Err(keyring::Error::NoEntry) => Ok(()), + Err(e) => { + // Some backends (notably Linux keyutils) error with a platform code + // instead of NoEntry when nothing is stored. Treat "already gone" as success. + match entry.get_password() { + Err(keyring::Error::NoEntry) => Ok(()), + _ => Err(e) + .with_context(|| format!("Failed to delete secret from {}", backend_name())), + } + } + } +} - Ok(()) +/// Get an API key from the keyring for a profile. +/// Returns Ok(None) if no key is stored, Ok(Some(key)) if found. +pub fn get_key(profile: &str) -> Result> { + get_secret(SERVICE_NAME, profile, "config file") +} + +/// Store an API key in the keyring for a profile. +pub fn set_key(profile: &str, api_key: &str) -> Result<()> { + set_secret(SERVICE_NAME, profile, api_key) } /// Delete an API key from the keyring for a profile. /// Returns Ok(()) even if no key was stored. pub fn delete_key(profile: &str) -> Result<()> { - let entry = - keyring::Entry::new(SERVICE_NAME, profile).context("Failed to create keyring entry")?; - - match entry.delete_credential() { - Ok(()) => Ok(()), - Err(keyring::Error::NoEntry) => Ok(()), // Already gone, that's fine - Err(e) => Err(e).context("Failed to delete API key from keyring"), - } + delete_secret(SERVICE_NAME, profile) } -/// Check if keyring is available on this system. +/// Check if a durable OS keyring backend is available. pub fn is_available() -> bool { - // Try to create an entry and check if we can access it - match keyring::Entry::new(SERVICE_NAME, "__test__") { + match entry(SERVICE_NAME, "__test__") { Ok(entry) => { - // Try a non-destructive operation - match entry.get_password() { - Err(keyring::Error::NoStorageAccess(_)) => false, - _ => true, // NoEntry or Ok means storage is accessible + if is_mock_entry(&entry) { + return false; } + !matches!( + entry.get_password(), + Err(keyring::Error::NoStorageAccess(_)) + ) } Err(_) => false, } } -const OAUTH_SERVICE_NAME: &str = "linear-cli-oauth"; - /// Get OAuth tokens JSON from keyring for a profile pub fn get_oauth_tokens(profile: &str) -> Result> { - let entry = keyring::Entry::new(OAUTH_SERVICE_NAME, profile) - .context("Failed to create keyring entry")?; - - match entry.get_password() { - Ok(json) => Ok(Some(json)), - Err(keyring::Error::NoEntry) => Ok(None), - Err(keyring::Error::NoStorageAccess(_)) => Ok(None), - Err(e) => { - if !crate::output::is_quiet() { - eprintln!( - "Warning: Keyring OAuth error ({}), falling back to config", - e - ); - } - Ok(None) - } - } + get_secret(OAUTH_SERVICE_NAME, profile, "config") } /// Store OAuth tokens JSON in keyring for a profile pub fn set_oauth_tokens(profile: &str, json: &str) -> Result<()> { - let entry = keyring::Entry::new(OAUTH_SERVICE_NAME, profile) - .context("Failed to create keyring entry")?; - entry - .set_password(json) - .context("Failed to store OAuth tokens in keyring")?; - Ok(()) + set_secret(OAUTH_SERVICE_NAME, profile, json) } /// Delete OAuth tokens from keyring for a profile pub fn delete_oauth_tokens(profile: &str) -> Result<()> { - let entry = keyring::Entry::new(OAUTH_SERVICE_NAME, profile) - .context("Failed to create keyring entry")?; - match entry.delete_credential() { - Ok(()) => Ok(()), - Err(keyring::Error::NoEntry) => Ok(()), - Err(e) => Err(e).context("Failed to delete OAuth tokens from keyring"), - } + delete_secret(OAUTH_SERVICE_NAME, profile) } #[cfg(test)] @@ -127,11 +159,22 @@ mod tests { const TEST_PROFILE: &str = "linear-cli-test-profile"; const TEST_KEY: &str = "lin_api_test_key_12345"; + #[test] + fn default_backend_is_not_mock() { + let probe = entry(SERVICE_NAME, "__backend_test__").expect("create probe entry"); + assert!( + !is_mock_entry(&probe), + "secure-storage must not use keyring's mock backend; enable apple-native, windows-native, and linux-native-sync-persistent on the keyring dependency" + ); + } + #[test] fn test_is_available() { - // Just check it doesn't panic - availability depends on system - let available = is_available(); - println!("Keyring available: {}", available); + // Availability depends on OS services; mock must never count as available. + if is_mock_entry(&entry(SERVICE_NAME, "__test__").expect("probe")) { + assert!(!is_available()); + } + let _ = is_available(); } #[test] diff --git a/src/main.rs b/src/main.rs index d0a29bc..900be8e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1162,9 +1162,7 @@ async fn run_command( Commands::Templates { action } => templates::handle(action, output).await?, Commands::Time { action } => time::handle(action, output).await?, Commands::Uploads { action } => uploads::handle(action).await?, - Commands::Interactive { team } => { - interactive::run(config::resolve_team_arg(team)).await? - } + Commands::Interactive { team } => interactive::run(config::resolve_team_arg(team)).await?, Commands::Context => handle_context(output, agent_opts, retry).await?, Commands::Favorites { action } => favorites::handle(action, output).await?, Commands::Roadmaps { action } => { @@ -1592,31 +1590,11 @@ mod tests { #[test] fn test_should_use_pager_disabled_for_interactive_commands() { - assert!(!should_use_pager( - false, - &OutputFormat::Table, - false, - true - )); + assert!(!should_use_pager(false, &OutputFormat::Table, false, true)); // Non-interactive table on TTY would still depend on is_terminal; quiet/json off. - assert!(!should_use_pager( - true, - &OutputFormat::Table, - false, - false - )); - assert!(!should_use_pager( - false, - &OutputFormat::Json, - false, - false - )); - assert!(!should_use_pager( - false, - &OutputFormat::Table, - true, - false - )); + assert!(!should_use_pager(true, &OutputFormat::Table, false, false)); + assert!(!should_use_pager(false, &OutputFormat::Json, false, false)); + assert!(!should_use_pager(false, &OutputFormat::Table, true, false)); } #[test]