diff --git a/cmd/commands/cmd_walletunlocker.go b/cmd/commands/cmd_walletunlocker.go index 0c04ce25c0d..7cde52c74d7 100644 --- a/cmd/commands/cmd_walletunlocker.go +++ b/cmd/commands/cmd_walletunlocker.go @@ -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) + 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 diff --git a/cmd/commands/cmd_walletunlocker_test.go b/cmd/commands/cmd_walletunlocker_test.go index 91e4be91368..63d00fea708 100644 --- a/cmd/commands/cmd_walletunlocker_test.go +++ b/cmd/commands/cmd_walletunlocker_test.go @@ -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", diff --git a/docs/release-notes/release-notes-0.22.0.md b/docs/release-notes/release-notes-0.22.0.md index c3367af9cc7..e5539ba5e59 100644 --- a/docs/release-notes/release-notes-0.22.0.md +++ b/docs/release-notes/release-notes-0.22.0.md @@ -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