Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Security

- `--password-get` no longer prints the stored secret to an interactive terminal (where it would linger in scrollback). On a TTY it now only confirms the key exists; the raw value is emitted **only** when stdout is piped or redirected (e.g. `PW=$(sshx --password-get=key)` or `sshx --password-get=key | pbcopy`), with no decoration for clean capture

### Changed

- `--help` and the no-argument usage screen now print the build version (`Version: <version>`)
Expand Down
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -398,15 +398,16 @@ sshx --password-list
#### Get Password

```bash
# Get stored password (for debugging)
sshx --password-get=master

# Output example:
# ✓ Password retrieved from system keyring
# Service: sshx
# Key: master
#
# Password: yourpassword
# Read a stored password. On a terminal sshx only confirms the key exists; to
# obtain the value, pipe stdout — it is emitted raw, with no decoration.
PW=$(sshx --password-get=master) # capture into a variable
sshx --password-get=master | pbcopy # copy to clipboard (macOS)

# Interactive output example (the secret is NOT printed to the terminal):
# ✓ Password exists for key 'master' (service: sshx)
# Not printing the secret to a terminal. To use it, pipe stdout:
# sshx --password-get=master | pbcopy
# sshx --password-get=master | cat
```

#### Delete Password
Expand Down
19 changes: 10 additions & 9 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,15 +332,16 @@ sshx --password-list
#### 获取密码

```bash
# 获取存储的密码(用于调试)
sshx --password-get=master

# 输出示例:
# ✓ Password retrieved from system keyring
# Service: sshx
# Key: master
#
# Password: yourpassword
# 读取已存储的密码。在终端中 sshx 只确认该 key 是否存在;
# 如需取出明文,请将 stdout 通过管道传出——此时原样输出,无任何修饰。
PW=$(sshx --password-get=master) # 捕获到变量
sshx --password-get=master | pbcopy # 复制到剪贴板(macOS)

# 终端下的输出示例(不会把密码打印到终端):
# ✓ Password exists for key 'master' (service: sshx)
# Not printing the secret to a terminal. To use it, pipe stdout:
# sshx --password-get=master | pbcopy
# sshx --password-get=master | cat
```

#### 删除密码
Expand Down
22 changes: 16 additions & 6 deletions internal/app/password.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,22 @@ func getPassword(serviceName, key string) error {
return fmt.Errorf("failed to get password: %w", err)
}

logger.GetLogger().Success("Password retrieved from system keyring")
logger.GetLogger().Info(" Service: %s", serviceName)
logger.GetLogger().Info(" Key: %s", key)
fmt.Printf("\nPassword: %s\n", password)
logger.GetLogger().Warning("Password printed in plaintext; clear your terminal scrollback if it is sensitive.")
// Never dump a secret onto an interactive terminal, where it would linger in
// scrollback and shoulder-surfing range. sshx already uses the keyring
// internally (it auto-fills sudo over stdin), so the plaintext value is only
// needed when handing it to another program. When stdout is a pipe or file we
// emit just the raw value (no decoration, no trailing newline) so it can be
// captured cleanly, e.g. PW=$(sshx --password-get=key) or `... | pbcopy`.
if term.IsTerminal(int(os.Stdout.Fd())) {
logger.GetLogger().Success("Password exists for key '%s' (service: %s)", key, serviceName)
logger.GetLogger().Info("Not printing the secret to a terminal. To use it, pipe stdout:")
logger.GetLogger().Info(" sshx --password-get=%s | pbcopy # copy to clipboard (macOS)", key)
logger.GetLogger().Info(" sshx --password-get=%s | cat # show on screen if you must", key)
return nil
}

logger.GetLogger().Warning("Emitting the plaintext password for key '%s' on stdout.", key)
fmt.Print(password)
return nil
}

Expand Down Expand Up @@ -173,7 +183,7 @@ func listPasswords() error {
fmt.Println("Custom password keys you've set (like 'test-password') are stored")
fmt.Println("but not listed here due to keyring API limitations.")
fmt.Println("\nTo check a custom key:")
fmt.Println(" sshx --password-get=<your-key-name>")
fmt.Println(" sshx --password-check=<your-key-name>")
fmt.Println("\nPlatform-specific commands to list all:")
if isMacOS() {
fmt.Println(" macOS: security find-generic-password -s sshx")
Expand Down
2 changes: 1 addition & 1 deletion internal/app/usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ SFTP Options:
Password Management (Cross-Platform):
--password-set=<key>[:<password>] Set password in system keyring
If password omitted, will prompt
--password-get=<key> Get password from keyring
--password-get=<key> Output the password (raw value only when piped; on a terminal just confirms it exists)
--password-check=<key> Check if password exists (alias: --password-exists)
--password-delete=<key> Delete password from keyring (alias: --password-del)
--password-list List common password keys (alias: --password-ls)
Expand Down
2 changes: 1 addition & 1 deletion skills/sshx/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ Windows Credential Manager) under service name `sshx`.
```bash
sshx --password-set=master # prompt (no echo) — preferred
sshx --password-set=master:secret # inline (convenient but warned against)
sshx --password-get=master # read back
sshx --password-get=master # confirm exists on a TTY; pipe (e.g. `| pbcopy`) to emit the raw value
sshx --password-check=server-A # exists? (alias: --password-exists)
sshx --password-list # common keys (alias: --password-ls)
sshx --password-delete=server-A # delete (alias: --password-del)
Expand Down
Loading