Skip to content

Commit dd942fd

Browse files
committed
cmd/litcli: use macaroon instead of ui password
Remove the use of the UI password from litcl. Use the litd macaroon instead. Note, this means that in stateless mode, litcli calls wont work and all calls will need to go through tht UI.
1 parent 3b690f8 commit dd942fd

File tree

2 files changed

+100
-85
lines changed

2 files changed

+100
-85
lines changed

cmd/litcli/main.go

Lines changed: 93 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,29 @@
11
package main
22

33
import (
4-
"context"
5-
"encoding/base64"
64
"fmt"
7-
"os"
8-
"path/filepath"
9-
"strings"
10-
"syscall"
11-
125
terminal "github.com/lightninglabs/lightning-terminal"
136
"github.com/lightninglabs/lightning-terminal/litrpc"
147
"github.com/lightninglabs/lndclient"
158
"github.com/lightninglabs/protobuf-hex-display/jsonpb"
169
"github.com/lightninglabs/protobuf-hex-display/proto"
1710
"github.com/lightningnetwork/lnd"
1811
"github.com/lightningnetwork/lnd/lncfg"
12+
"github.com/lightningnetwork/lnd/macaroons"
1913
"github.com/urfave/cli"
20-
"golang.org/x/term"
2114
"google.golang.org/grpc"
2215
"google.golang.org/grpc/credentials"
23-
"google.golang.org/grpc/metadata"
16+
"gopkg.in/macaroon.v2"
17+
"io/ioutil"
18+
"os"
19+
"path/filepath"
20+
"strings"
2421
)
2522

2623
const (
27-
// uiPasswordEnvName is the name of the environment variable under which
28-
// we look for the UI password for litcli.
29-
uiPasswordEnvName = "UI_PASSWORD"
24+
// defaultMacaroonTimeout is the default macaroon timeout in seconds
25+
// that we set when sending it over the line.
26+
defaultMacaroonTimeout int64 = 60
3027
)
3128

3229
var (
@@ -60,12 +57,10 @@ var (
6057
Usage: "path to lnd's TLS certificate",
6158
Value: lnd.DefaultConfig().TLSCertPath,
6259
}
63-
uiPasswordFlag = cli.StringFlag{
64-
Name: "uipassword",
65-
Usage: "the UI password for authenticating against LiT; if " +
66-
"not specified will read from environment variable " +
67-
uiPasswordEnvName + " or prompt on terminal if both " +
68-
"values are empty",
60+
macaroonPathFlag = cli.StringFlag{
61+
Name: "macaroonpath",
62+
Usage: "path to lit's macaroon file",
63+
Value: terminal.DefaultMacaroonPath,
6964
}
7065
)
7166

@@ -85,7 +80,7 @@ func main() {
8580
lndMode,
8681
tlsCertFlag,
8782
lndTlsCertFlag,
88-
uiPasswordFlag,
83+
macaroonPathFlag,
8984
}
9085
app.Commands = append(app.Commands, sessionCommands...)
9186

@@ -102,11 +97,11 @@ func fatal(err error) {
10297

10398
func getClient(ctx *cli.Context) (litrpc.SessionsClient, func(), error) {
10499
rpcServer := ctx.GlobalString("rpcserver")
105-
tlsCertPath, err := extractPathArgs(ctx)
100+
tlsCertPath, macPath, err := extractPathArgs(ctx)
106101
if err != nil {
107102
return nil, nil, err
108103
}
109-
conn, err := getClientConn(rpcServer, tlsCertPath)
104+
conn, err := getClientConn(rpcServer, tlsCertPath, macPath)
110105
if err != nil {
111106
return nil, nil, err
112107
}
@@ -116,9 +111,18 @@ func getClient(ctx *cli.Context) (litrpc.SessionsClient, func(), error) {
116111
return sessionsClient, cleanup, nil
117112
}
118113

119-
func getClientConn(address, tlsCertPath string) (*grpc.ClientConn, error) {
114+
func getClientConn(address, tlsCertPath, macaroonPath string) (*grpc.ClientConn,
115+
error) {
116+
117+
// We always need to send a macaroon.
118+
macOption, err := readMacaroon(macaroonPath)
119+
if err != nil {
120+
return nil, err
121+
}
122+
120123
opts := []grpc.DialOption{
121124
grpc.WithDefaultCallOptions(maxMsgRecvSize),
125+
macOption,
122126
}
123127

124128
// TLS cannot be disabled, we'll always have a cert file to read.
@@ -138,15 +142,33 @@ func getClientConn(address, tlsCertPath string) (*grpc.ClientConn, error) {
138142
return conn, nil
139143
}
140144

141-
// extractPathArgs parses the TLS certificate from the command.
142-
func extractPathArgs(ctx *cli.Context) (string, error) {
145+
// extractPathArgs parses the TLS certificate and macaroon paths from the
146+
// command.
147+
func extractPathArgs(ctx *cli.Context) (string, string, error) {
143148
// We'll start off by parsing the network. This is needed to determine
144149
// the correct path to the TLS certificate and macaroon when not
145150
// specified.
146151
networkStr := strings.ToLower(ctx.GlobalString("network"))
147152
_, err := lndclient.Network(networkStr).ChainParams()
148153
if err != nil {
149-
return "", err
154+
return "", "", err
155+
}
156+
157+
// Get the base dir so that we can reconstruct the default tls and
158+
// macaroon paths if needed.
159+
baseDir := lncfg.CleanAndExpandPath(ctx.GlobalString(baseDirFlag.Name))
160+
161+
macaroonPath := lncfg.CleanAndExpandPath(ctx.GlobalString(
162+
macaroonPathFlag.Name,
163+
))
164+
165+
// If the macaroon path flag has not been set to a custom value,
166+
// then reconstruct it with the possibly new base dir and network
167+
// values.
168+
if macaroonPath == terminal.DefaultMacaroonPath {
169+
macaroonPath = filepath.Join(
170+
baseDir, networkStr, terminal.DefaultMacaroonFilename,
171+
)
150172
}
151173

152174
// Get the LND mode. If Lit is in integrated LND mode, then LND's tls
@@ -157,7 +179,7 @@ func extractPathArgs(ctx *cli.Context) (string, error) {
157179
lndTlsCertFlag.Name,
158180
))
159181

160-
return tlsCertPath, nil
182+
return tlsCertPath, macaroonPath, nil
161183
}
162184

163185
// Lit is in remote LND mode. So we need Lit's tls cert.
@@ -167,89 +189,78 @@ func extractPathArgs(ctx *cli.Context) (string, error) {
167189

168190
// If a custom TLS path was set, use it as is.
169191
if tlsCertPath != terminal.DefaultTLSCertPath {
170-
return tlsCertPath, nil
192+
return tlsCertPath, macaroonPath, nil
171193
}
172194

173195
// If a custom base directory was set, we'll also check if custom paths
174196
// for the TLS cert file was set as well. If not, we'll override the
175197
// paths so they can be found within the custom base directory set.
176198
// This allows us to set a custom base directory, along with custom
177199
// paths to the TLS cert file.
178-
baseDir := lncfg.CleanAndExpandPath(ctx.GlobalString(baseDirFlag.Name))
179200
if baseDir != terminal.DefaultLitDir {
180201
tlsCertPath = filepath.Join(
181202
baseDir, terminal.DefaultTLSCertFilename,
182203
)
183204
}
184205

185-
return tlsCertPath, nil
206+
return tlsCertPath, macaroonPath, nil
186207
}
187208

188-
func printRespJSON(resp proto.Message) { // nolint
189-
jsonMarshaler := &jsonpb.Marshaler{
190-
EmitDefaults: true,
191-
OrigName: true,
192-
Indent: "\t", // Matches indentation of printJSON.
209+
// readMacaroon tries to read the macaroon file at the specified path and create
210+
// gRPC dial options from it.
211+
func readMacaroon(macPath string) (grpc.DialOption, error) {
212+
// Load the specified macaroon file.
213+
macBytes, err := ioutil.ReadFile(macPath)
214+
if err != nil {
215+
return nil, fmt.Errorf("unable to read macaroon path : %v", err)
193216
}
194217

195-
jsonStr, err := jsonMarshaler.MarshalToString(resp)
196-
if err != nil {
197-
fmt.Println("unable to decode response: ", err)
198-
return
218+
mac := &macaroon.Macaroon{}
219+
if err = mac.UnmarshalBinary(macBytes); err != nil {
220+
return nil, fmt.Errorf("unable to decode macaroon: %v", err)
199221
}
200222

201-
fmt.Println(jsonStr)
202-
}
223+
macConstraints := []macaroons.Constraint{
224+
// We add a time-based constraint to prevent replay of the
225+
// macaroon. It's good for 60 seconds by default to make up for
226+
// any discrepancy between client and server clocks, but leaking
227+
// the macaroon before it becomes invalid makes it possible for
228+
// an attacker to reuse the macaroon. In addition, the validity
229+
// time of the macaroon is extended by the time the server clock
230+
// is behind the client clock, or shortened by the time the
231+
// server clock is ahead of the client clock (or invalid
232+
// altogether if, in the latter case, this time is more than 60
233+
// seconds).
234+
macaroons.TimeoutConstraint(defaultMacaroonTimeout),
235+
}
203236

204-
func getAuthContext(cliCtx *cli.Context) context.Context {
205-
uiPassword, err := getUIPassword(cliCtx)
237+
// Apply constraints to the macaroon.
238+
constrainedMac, err := macaroons.AddConstraints(mac, macConstraints...)
206239
if err != nil {
207-
fatal(err)
240+
return nil, err
208241
}
209242

210-
basicAuth := base64.StdEncoding.EncodeToString(
211-
[]byte(fmt.Sprintf("%s:%s", uiPassword, uiPassword)),
212-
)
213-
214-
ctxb := context.Background()
215-
md := metadata.MD{}
216-
217-
md.Set("macaroon", "no-macaroons-for-litcli")
218-
md.Set("authorization", fmt.Sprintf("Basic %s", basicAuth))
219-
220-
return metadata.NewOutgoingContext(ctxb, md)
221-
}
222-
223-
func getUIPassword(ctx *cli.Context) (string, error) {
224-
// The command line flag has precedence.
225-
uiPassword := strings.TrimSpace(ctx.GlobalString(uiPasswordFlag.Name))
226-
227-
// To automate things with litcli, we also offer reading the password
228-
// from environment variables if the flag wasn't specified.
229-
if uiPassword == "" {
230-
uiPassword = strings.TrimSpace(os.Getenv(uiPasswordEnvName))
243+
// Now we append the macaroon credentials to the dial options.
244+
cred, err := macaroons.NewMacaroonCredential(constrainedMac)
245+
if err != nil {
246+
return nil, fmt.Errorf("error creating macaroon credential: %v",
247+
err)
231248
}
249+
return grpc.WithPerRPCCredentials(cred), nil
250+
}
232251

233-
if uiPassword == "" {
234-
// If there's no value in the environment, we'll now prompt the
235-
// user to enter their password on the terminal.
236-
fmt.Printf("Input your LiT UI password: ")
237-
238-
// The variable syscall.Stdin is of a different type in the
239-
// Windows API that's why we need the explicit cast. And of
240-
// course the linter doesn't like it either.
241-
pw, err := term.ReadPassword(int(syscall.Stdin)) // nolint:unconvert
242-
fmt.Println()
243-
244-
if err != nil {
245-
return "", err
246-
}
247-
uiPassword = strings.TrimSpace(string(pw))
252+
func printRespJSON(resp proto.Message) { // nolint
253+
jsonMarshaler := &jsonpb.Marshaler{
254+
EmitDefaults: true,
255+
OrigName: true,
256+
Indent: "\t", // Matches indentation of printJSON.
248257
}
249258

250-
if uiPassword == "" {
251-
return "", fmt.Errorf("no UI password provided")
259+
jsonStr, err := jsonMarshaler.MarshalToString(resp)
260+
if err != nil {
261+
fmt.Println("unable to decode response: ", err)
262+
return
252263
}
253264

254-
return uiPassword, nil
265+
fmt.Println(jsonStr)
255266
}

cmd/litcli/sessions.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"context"
45
"encoding/hex"
56
"fmt"
67
"time"
@@ -68,8 +69,9 @@ func addSession(ctx *cli.Context) error {
6869
sessionLength := time.Second * time.Duration(ctx.Uint64("expiry"))
6970
sessionExpiry := time.Now().Add(sessionLength).Unix()
7071

72+
ctxb := context.Background()
7173
resp, err := client.AddSession(
72-
getAuthContext(ctx), &litrpc.AddSessionRequest{
74+
ctxb, &litrpc.AddSessionRequest{
7375
Label: label,
7476
SessionType: litrpc.SessionType_TYPE_UI_PASSWORD,
7577
ExpiryTimestampSeconds: uint64(sessionExpiry),
@@ -165,8 +167,9 @@ func listSessions(filter sessionFilter) func(ctx *cli.Context) error {
165167
}
166168
defer cleanup()
167169

170+
ctxb := context.Background()
168171
resp, err := client.ListSessions(
169-
getAuthContext(ctx), &litrpc.ListSessionsRequest{},
172+
ctxb, &litrpc.ListSessionsRequest{},
170173
)
171174
if err != nil {
172175
return err
@@ -217,8 +220,9 @@ func revokeSession(ctx *cli.Context) error {
217220
return err
218221
}
219222

223+
ctxb := context.Background()
220224
resp, err := client.RevokeSession(
221-
getAuthContext(ctx), &litrpc.RevokeSessionRequest{
225+
ctxb, &litrpc.RevokeSessionRequest{
222226
LocalPublicKey: pubkey,
223227
},
224228
)

0 commit comments

Comments
 (0)