Skip to content

Commit 993ff58

Browse files
committed
cmd/commands: read full stdin for lncli unlock --stdin
The --stdin branch of `lncli unlock` used bufio.ReadBytes('\n'), which stops at the first newline byte and silently truncates passwords that contain embedded newlines. A wallet password generated from random bytes can legitimately contain a newline, in which case the unlock attempt fails even though the same password works over REST/gRPC. Switch to io.ReadAll so the password is consumed up to EOF, and only trim a single trailing newline (with optional CR) so the common `echo "pw" | lncli unlock --stdin` invocation keeps working without leaking the trailing byte added by the shell. New table-driven test cases cover an embedded newline, no trailing newline, and a CRLF terminator. Fixes #5584
1 parent 8df972d commit 993ff58

2 files changed

Lines changed: 86 additions & 5 deletions

File tree

cmd/commands/cmd_walletunlocker.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -543,11 +543,13 @@ func unlockWithDeps(ctx *cli.Context,
543543
// password manager. If the user types the password instead, it will be
544544
// echoed in the console.
545545
case ctx.IsSet("stdin"):
546-
reader := bufio.NewReader(stdin)
547-
pw, err = reader.ReadBytes('\n')
548-
549-
// Remove carriage return and newline characters.
550-
pw = bytes.Trim(pw, "\r\n")
546+
// Read until EOF so passwords containing newline bytes are
547+
// preserved. A single trailing newline (with optional CR) is
548+
// stripped so the common `echo "pw" | lncli unlock --stdin`
549+
// usage keeps working.
550+
pw, err = io.ReadAll(stdin)
551+
pw = bytes.TrimSuffix(pw, []byte{'\n'})
552+
pw = bytes.TrimSuffix(pw, []byte{'\r'})
551553

552554
// Read the password from a terminal by default. This requires the
553555
// terminal to be a real tty and will fail if a string is piped into

cmd/commands/cmd_walletunlocker_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,85 @@ func TestUnlock(t *testing.T) {
266266
},
267267
},
268268

269+
// Password piped via stdin contains an embedded newline.
270+
// Reading must consume everything up to EOF, preserving the
271+
// embedded newline byte.
272+
{
273+
name: "success_stdin_embedded_newline",
274+
args: []string{"--stdin"},
275+
stdinInput: "first\nsecond\n",
276+
stateStreams: []stateStreamSpec{
277+
{
278+
states: []lnrpc.WalletState{
279+
lnrpc.WalletState_LOCKED,
280+
},
281+
},
282+
{
283+
states: []lnrpc.WalletState{
284+
lnrpc.WalletState_RPC_ACTIVE,
285+
},
286+
},
287+
},
288+
expectReadPasswordCalls: 0,
289+
expectUnlockCalls: 1,
290+
expectSubscribeCalls: 2,
291+
expectReq: &lnrpc.UnlockWalletRequest{
292+
WalletPassword: []byte("first\nsecond"),
293+
},
294+
},
295+
296+
// Password piped via stdin without any trailing newline (e.g.
297+
// `printf %s pw | lncli unlock --stdin`).
298+
{
299+
name: "success_stdin_no_trailing_newline",
300+
args: []string{"--stdin"},
301+
stdinInput: "secret",
302+
stateStreams: []stateStreamSpec{
303+
{
304+
states: []lnrpc.WalletState{
305+
lnrpc.WalletState_LOCKED,
306+
},
307+
},
308+
{
309+
states: []lnrpc.WalletState{
310+
lnrpc.WalletState_RPC_ACTIVE,
311+
},
312+
},
313+
},
314+
expectReadPasswordCalls: 0,
315+
expectUnlockCalls: 1,
316+
expectSubscribeCalls: 2,
317+
expectReq: &lnrpc.UnlockWalletRequest{
318+
WalletPassword: []byte("secret"),
319+
},
320+
},
321+
322+
// Password piped via stdin with a CRLF terminator: only the
323+
// final \r\n pair is stripped.
324+
{
325+
name: "success_stdin_crlf_terminator",
326+
args: []string{"--stdin"},
327+
stdinInput: "secret\r\n",
328+
stateStreams: []stateStreamSpec{
329+
{
330+
states: []lnrpc.WalletState{
331+
lnrpc.WalletState_LOCKED,
332+
},
333+
},
334+
{
335+
states: []lnrpc.WalletState{
336+
lnrpc.WalletState_RPC_ACTIVE,
337+
},
338+
},
339+
},
340+
expectReadPasswordCalls: 0,
341+
expectUnlockCalls: 1,
342+
expectSubscribeCalls: 2,
343+
expectReq: &lnrpc.UnlockWalletRequest{
344+
WalletPassword: []byte("secret"),
345+
},
346+
},
347+
269348
// Uses positional recovery window argument.
270349
{
271350
name: "success_arg_recovery_window",

0 commit comments

Comments
 (0)