Skip to content
Open
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
12 changes: 7 additions & 5 deletions cmd/commands/cmd_walletunlocker.go
Original file line number Diff line number Diff line change
Expand Up @@ -543,11 +543,13 @@ func unlockWithDeps(ctx *cli.Context,
// password manager. If the user types the password instead, it will be
// echoed in the console.
case ctx.IsSet("stdin"):
reader := bufio.NewReader(stdin)
pw, err = reader.ReadBytes('\n')

// Remove carriage return and newline characters.
pw = bytes.Trim(pw, "\r\n")
// Read until EOF so passwords containing newline bytes are
// preserved. A single trailing newline (with optional CR) is
// stripped so the common `echo "pw" | lncli unlock --stdin`
// usage keeps working.
pw, err = io.ReadAll(stdin)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The switch to io.ReadAll is a great fix for the truncation issue. However, we should handle the err returned here before proceeding to the trim logic, just in case the stdin stream fails or is interrupted.

Suggested change
pw, err = io.ReadAll(stdin)
pw, err = io.ReadAll(stdin)
if err != nil {
return err
}

pw = bytes.TrimSuffix(pw, []byte{'\n'})
pw = bytes.TrimSuffix(pw, []byte{'\r'})

// Read the password from a terminal by default. This requires the
// terminal to be a real tty and will fail if a string is piped into
Expand Down
79 changes: 79 additions & 0 deletions cmd/commands/cmd_walletunlocker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,85 @@ func TestUnlock(t *testing.T) {
},
},

// Password piped via stdin contains an embedded newline.
// Reading must consume everything up to EOF, preserving the
// embedded newline byte.
{
name: "success_stdin_embedded_newline",
args: []string{"--stdin"},
stdinInput: "first\nsecond\n",
stateStreams: []stateStreamSpec{
{
states: []lnrpc.WalletState{
lnrpc.WalletState_LOCKED,
},
},
{
states: []lnrpc.WalletState{
lnrpc.WalletState_RPC_ACTIVE,
},
},
},
expectReadPasswordCalls: 0,
expectUnlockCalls: 1,
expectSubscribeCalls: 2,
expectReq: &lnrpc.UnlockWalletRequest{
WalletPassword: []byte("first\nsecond"),
},
},

// Password piped via stdin without any trailing newline (e.g.
// `printf %s pw | lncli unlock --stdin`).
{
name: "success_stdin_no_trailing_newline",
args: []string{"--stdin"},
stdinInput: "secret",
stateStreams: []stateStreamSpec{
{
states: []lnrpc.WalletState{
lnrpc.WalletState_LOCKED,
},
},
{
states: []lnrpc.WalletState{
lnrpc.WalletState_RPC_ACTIVE,
},
},
},
expectReadPasswordCalls: 0,
expectUnlockCalls: 1,
expectSubscribeCalls: 2,
expectReq: &lnrpc.UnlockWalletRequest{
WalletPassword: []byte("secret"),
},
},

// Password piped via stdin with a CRLF terminator: only the
// final \r\n pair is stripped.
{
name: "success_stdin_crlf_terminator",
args: []string{"--stdin"},
stdinInput: "secret\r\n",
stateStreams: []stateStreamSpec{
{
states: []lnrpc.WalletState{
lnrpc.WalletState_LOCKED,
},
},
{
states: []lnrpc.WalletState{
lnrpc.WalletState_RPC_ACTIVE,
},
},
},
expectReadPasswordCalls: 0,
expectUnlockCalls: 1,
expectSubscribeCalls: 2,
expectReq: &lnrpc.UnlockWalletRequest{
WalletPassword: []byte("secret"),
},
},

// Uses positional recovery window argument.
{
name: "success_arg_recovery_window",
Expand Down
4 changes: 4 additions & 0 deletions docs/release-notes/release-notes-0.22.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@

# Bug Fixes

* [`lncli unlock --stdin`](https://github.com/lightningnetwork/lnd/pull/10784)
now reads the password until EOF instead of stopping at the first newline,
so passwords containing embedded newline bytes are accepted.

# New Features

## Functional Enhancements
Expand Down
Loading