# \[H1\] ghost-eye stores sr25519 seeds in plaintext on disk — wallet-keys, address-book, and node stash keys world-readable

**URL:** https://ghostforum.org/t/h1-ghost-eye-stores-sr25519-seeds-in-plaintext-on-disk-wallet-keys-address-book-and-node-stash-keys-world-readable/124
**Category:** Submitted
**Tags:** ghost-eye
**Created:** [October 3, 2026, 12:27am UTC](https://ghostforum.org/t/h1-ghost-eye-stores-sr25519-seeds-in-plaintext-on-disk-wallet-keys-address-book-and-node-stash-keys-world-readable/124 "2026-10-03T00:27:39Z")
**Posts on this page:** 1
**Showing post:** 1

<div class="post-metadata">

### Author: ![Chaplin](https://ghostforum.org/letter_avatar_proxy/v4/letter/c/74df32/32.png) [@Chaplin](https://ghostforum.org/u/Chaplin)
#### Post date: [October 3, 2026, 12:27am UTC](https://ghostforum.org/t/h1-ghost-eye-stores-sr25519-seeds-in-plaintext-on-disk-wallet-keys-address-book-and-node-stash-keys-world-readable/124/1 "2026-10-03T00:27:40Z")

</div>

## Summary

`ghost-eye` (the GHOST operator TUI) persists every secret it manages as plaintext  
`name:0x<64-hex-seed>` lines, with no encryption, no password, and no restrictive file  
permissions:

| Secret | File | Writer |
| --- | --- | --- |
| Wallet seeds (all accounts) | `~/.local/share/ghost-eye/wallet-keys` | `accounts.rs:230` `save_to_file()` |
| Address-book entries (same `name:0xseed` format) | `~/.local/share/ghost-eye/address-book` | `address_book.rs:68` `save_to_file()` |
| Freshly generated stash key | `/etc/ghost/stash-key` | `stash_info.rs:134-135` `generate_and_save_new_key()` |

All writes use `File::create` with default permissions (0666 & !umask — verified empirically;  
under the standard default umask 022 this is **0644 world-readable** ). The data directory  
itself is created at startup by `logging::init()` (`src/logging.rs:13-14`) via  
`create_dir_all` (0755 default), so the whole tree is world-readable/traversable on typical  
systems. The codebase has no `mode(0o600)`/`set_permissions` call, no password prompt, and  
no encryption crate in `Cargo.toml`.

Worse, the TUI actively funnels the node’s most sensitive keys into the plaintext file:  
pressing `L` (`load_initial_keys`, `accounts.rs:399`) — and the automatic first-run import  
(`read_or_create`, `accounts.rs:246`) — copies `/etc/ghost/wallet-key` and  
`/etc/ghost/stash-key` (the **validator’s session/stash keys** , controlling staked funds)  
into `wallet-keys` through the same `save_to_file()`. Seeds also live in process memory as  
`String`s (`AccountInfo.seed`), cloned through mpsc channels with no zeroization. Any local  
user, backup/disk-image reader, or co-tenant process recovers full sr25519 seeds — complete  
signing control over operator, stash and wallet funds.

## Severity

**High — CVSS 3.1: 7.8** — `CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H`

Local attack vector (world-readable file under default umask), no privileges beyond any  
local account, no user interaction. Full confidentiality/integrity/availability impact on  
key material: seed compromise = total fund control.

## Affected code

```rust
// src/components/wallet/accounts.rs:228-233
fn save_to_file(&mut self) {
    let mut file = File::create(&self.wallet_keys_file).unwrap(); // 0666 & !umask
    for wallet in self.wallet_keys.iter() {
        writeln!(file, "{}:0x{}", wallet.name, &wallet.seed).unwrap(); // plaintext seed
    }
}

```

```rust
// src/components/wallet/accounts.rs:399-402 — 'L' key pulls node keys into the file
fn load_initial_keys(&mut self) {
    self.try_load_and_store_keys("/etc/ghost/wallet-key", "ghostie");
    self.try_load_and_store_keys("/etc/ghost/stash-key", "stash");
}

```

```rust
// src/components/validator/stash_info.rs:126-135
fn generate_and_save_new_key(&mut self) -> Result<()> {
    let (pair, seed) = Pair::generate();
    let secret_seed = hex::encode(seed);
    ...
    let mut new_file = File::create(&self.stash_filepath)?; // /etc/ghost/stash-key
    writeln!(new_file, "0x{}", &secret_seed)?; // plaintext

```

```rust
// src/components/wallet/address_book.rs:67-72 — same pattern, second file
fn save_to_file(&mut self) {
    let mut file = File::create(&self.address_book_file).unwrap();
    for wallet in self.address_book.iter() {
        writeln!(file, "{}:0x{}", wallet.name, &wallet.seed).unwrap();
    }
}

```

Data dir: `ProjectDirs::from("com", "ghost", "ghost-eye")` (`src/config.rs:124-126`) →  
`~/.local/share/ghost-eye/` on Linux.

## Steps to reproduce

1. `git clone https://git.ghostchain.io/ghostchain/ghost-eye && cd ghost-eye`
2. `git rev-parse HEAD` → `ee23e6ba643148edd11944fae2d294daf017a18a`
3. Build and run (`cargo run`), open the wallet tab, create an account (or press `L` with  
`/etc/ghost/wallet-key` present).
4. `cat ~/.local/share/ghost-eye/wallet-keys` → `mywallet:0x9d61b19d…` (64 hex chars = raw  
sr25519 seed); `ls -l` → `-rw-r--r--`.
5. `Pair::from_seed(&hex::decode(seed))` reconstructs the full signing key — the app itself  
does exactly this to sign (`accounts.rs:254`, `predefined_txs.rs:347`).

The write path is unconditional: create/rename/delete/import all call `save_to_file()`.

## Impact

- **Direct key exposure:** any local user or process with filesystem read access obtains  
complete sr25519 seeds for operator wallets and validator stash/session keys — full  
authority over funds and staking operations. Backups, disk images, container layers and  
monitoring agents that can read the file are all key-compromise vectors.
- **Privilege boundary crossed:** the `L`-key/first-run import moves keys from root-owned  
`/etc/ghost/*` into a user-readable `$HOME` file, widening exposure beyond the node  
operator’s own hardening.
- **No defense in depth:** no password, no OS keyring, no file-mode hardening, no  
in-memory zeroization.

## Remediation

- Encrypt at rest: password prompt on first run, Argon2id + ChaCha20-Poly1305; never store  
plaintext seeds.
- Short term: create key files with `0o600`, verify parent dir permissions, and stop  
importing `/etc/ghost/*` node keys into the wallet file (or warn loudly).
- Zeroize seed material in memory (`zeroize` crate); load seeds on demand for signing  
instead of keeping them resident as `String`s.
- Parse the wallet-keys file defensively — currently a blank/malformed line panics the TUI  
on startup (`&""[3..]`, `accounts.rs:254`).

---

_[View the full topic](https://ghostforum.org/t/h1-ghost-eye-stores-sr25519-seeds-in-plaintext-on-disk-wallet-keys-address-book-and-node-stash-keys-world-readable/124)._
