Skip to content

Commit 8f3f1be

Browse files
authored
Merge pull request #111 from msladek/pin
Quick unlock: PIN
2 parents 6a0861d + c99d0bb commit 8f3f1be

File tree

8 files changed

+468
-138
lines changed

8 files changed

+468
-138
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ Commands
3434
| `show FILTER` | List vault entries matching FILTER with password |
3535
| `copy FILTER` | Copy the password of a vault entry matching FILTER to the clipboard |
3636
| `pass FILTER` | Print the password of a vaulty entry matching FILTER to stdout |
37+
| `dryrun` | Opens the vault without reading anything from it |
3738
| `version` | Print the version |
39+
| `help` | Print the help text |
3840

3941
Flags
4042
-----
@@ -45,6 +47,7 @@ Flags
4547
| `-type=TYPE` | The type of your card (password, ...) |
4648
| `-log=LEVEL` | The log level from debug (5) to error (1) |
4749
| `-nonInteractive` | Disable prompts and fail instead |
50+
| `-pin` | Enable Quick Unlock using a PIN |
4851
| `-sort` | Sort the output by title and username of the `list` and `show` command |
4952
| `-trashed` | Show trashed items in the `list` and `show` command |
5053
| `-clipboardPrimary` | Use primary X selection instead of clipboard for the `copy` command |

cmd/enpasscli/main.go

+179-71
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,73 @@ import (
66
"os"
77
"path/filepath"
88
"runtime"
9-
s "sort"
9+
"sort"
10+
"strconv"
1011
"strings"
1112

1213
"github.com/hazcod/enpass-cli/pkg/clipboard"
1314
"github.com/hazcod/enpass-cli/pkg/enpass"
15+
"github.com/hazcod/enpass-cli/pkg/unlock"
1416
"github.com/miquella/ask"
1517
"github.com/sirupsen/logrus"
1618
)
1719

1820
const (
19-
defaultLogLevel = logrus.InfoLevel
21+
// commands
22+
cmdVersion = "version"
23+
cmdHelp = "help"
24+
cmdDryRun = "dryrun"
25+
cmdList = "list"
26+
cmdShow = "show"
27+
cmdCopy = "copy"
28+
cmdPass = "pass"
29+
// defaults
30+
defaultLogLevel = logrus.InfoLevel
31+
pinMinLength = 8
32+
pinDefaultKdfIterCount = 100000
2033
)
2134

2235
var (
2336
// overwritten by go build
2437
version = "dev"
25-
// enables prompts
26-
interactive = true
38+
// set of all commands
39+
commands = map[string]struct{}{cmdVersion: {}, cmdHelp: {}, cmdDryRun: {}, cmdList: {},
40+
cmdShow: {}, cmdCopy: {}, cmdPass: {}}
2741
)
2842

29-
func prompt(logger *logrus.Logger, msg string) string {
30-
if interactive {
43+
type Args struct {
44+
command string
45+
// params
46+
filters []string
47+
// flags
48+
vaultPath *string
49+
cardType *string
50+
keyFilePath *string
51+
logLevelStr *string
52+
nonInteractive *bool
53+
pinEnable *bool
54+
sort *bool
55+
trashed *bool
56+
clipboardPrimary *bool
57+
}
58+
59+
func (args *Args) parse() {
60+
args.vaultPath = flag.String("vault", "", "Path to your Enpass vault.")
61+
args.cardType = flag.String("type", "password", "The type of your card. (password, ...)")
62+
args.keyFilePath = flag.String("keyfile", "", "Path to your Enpass vault keyfile.")
63+
args.logLevelStr = flag.String("log", defaultLogLevel.String(), "The log level from debug (5) to error (1).")
64+
args.nonInteractive = flag.Bool("nonInteractive", false, "Disable prompts and fail instead.")
65+
args.pinEnable = flag.Bool("pin", false, "Enable PIN.")
66+
args.sort = flag.Bool("sort", false, "Sort the output by title and username of the 'list' and 'show' command.")
67+
args.trashed = flag.Bool("trashed", false, "Show trashed items in the 'list' and 'show' command.")
68+
args.clipboardPrimary = flag.Bool("clipboardPrimary", false, "Use primary X selection instead of clipboard for the 'copy' command.")
69+
flag.Parse()
70+
args.command = strings.ToLower(flag.Arg(0))
71+
args.filters = flag.Args()[1:]
72+
}
73+
74+
func prompt(logger *logrus.Logger, args *Args, msg string) string {
75+
if !*args.nonInteractive {
3176
if response, err := ask.HiddenAsk("Enter " + msg + ": "); err != nil {
3277
logger.WithError(err).Fatal("could not prompt for " + msg)
3378
} else {
@@ -37,27 +82,37 @@ func prompt(logger *logrus.Logger, msg string) string {
3782
return ""
3883
}
3984

85+
func printHelp() {
86+
fmt.Print("Valid commands: ")
87+
for cmd := range commands {
88+
fmt.Printf("%s, ", cmd)
89+
}
90+
fmt.Println()
91+
flag.Usage()
92+
os.Exit(1)
93+
}
94+
4095
func sortEntries(cards []enpass.Card) {
4196
// Sort by username preserving original order
42-
s.SliceStable(cards, func(i, j int) bool {
97+
sort.SliceStable(cards, func(i, j int) bool {
4398
return strings.ToLower(cards[i].Subtitle) < strings.ToLower(cards[j].Subtitle)
4499
})
45100
// Sort by title, preserving username order
46-
s.SliceStable(cards, func(i, j int) bool {
101+
sort.SliceStable(cards, func(i, j int) bool {
47102
return strings.ToLower(cards[i].Title) < strings.ToLower(cards[j].Title)
48103
})
49104
}
50105

51-
func listEntries(logger *logrus.Logger, vault *enpass.Vault, cardType string, sort bool, trashed bool, filters []string) {
52-
cards, err := vault.GetEntries(cardType, filters)
106+
func listEntries(logger *logrus.Logger, vault *enpass.Vault, args *Args) {
107+
cards, err := vault.GetEntries(*args.cardType, args.filters)
53108
if err != nil {
54109
logger.WithError(err).Fatal("could not retrieve cards")
55110
}
56-
if sort {
111+
if *args.sort {
57112
sortEntries(cards)
58113
}
59114
for _, card := range cards {
60-
if card.IsTrashed() && !trashed {
115+
if card.IsTrashed() && !*args.trashed {
61116
continue
62117
}
63118
logger.Printf(
@@ -71,19 +126,19 @@ func listEntries(logger *logrus.Logger, vault *enpass.Vault, cardType string, so
71126
}
72127
}
73128

74-
func showEntries(logger *logrus.Logger, vault *enpass.Vault, cardType string, sort bool, trashed bool, filters []string) {
75-
cards, err := vault.GetEntries(cardType, filters)
129+
func showEntries(logger *logrus.Logger, vault *enpass.Vault, args *Args) {
130+
cards, err := vault.GetEntries(*args.cardType, args.filters)
76131
if err != nil {
77132
logger.WithError(err).Fatal("could not retrieve cards")
78133
}
79-
if sort {
134+
if *args.sort {
80135
sortEntries(cards)
81136
}
82137
for _, card := range cards {
83-
if card.IsTrashed() && !trashed {
138+
if card.IsTrashed() && !*args.trashed {
84139
continue
85140
}
86-
password, err := card.Decrypt()
141+
decrypted, err := card.Decrypt()
87142
if err != nil {
88143
logger.WithError(err).Error("could not decrypt " + card.Title)
89144
continue
@@ -97,113 +152,166 @@ func showEntries(logger *logrus.Logger, vault *enpass.Vault, cardType string, so
97152
card.Title,
98153
card.Subtitle,
99154
card.Category,
100-
password,
155+
decrypted,
101156
)
102157
}
103158
}
104159

105-
func copyEntry(logger *logrus.Logger, vault *enpass.Vault, cardType string, filters []string) {
106-
card, err := vault.GetUniqueEntry(cardType, filters)
160+
func copyEntry(logger *logrus.Logger, vault *enpass.Vault, args *Args) {
161+
card, err := vault.GetEntry(*args.cardType, args.filters, true)
107162
if err != nil {
108163
logger.WithError(err).Fatal("could not retrieve unique card")
109164
}
110165

111-
password, err := card.Decrypt()
166+
decrypted, err := card.Decrypt()
112167
if err != nil {
113168
logger.WithError(err).Fatal("could not decrypt card")
114169
}
115170

116-
if err := clipboard.WriteAll(password); err != nil {
171+
if *args.clipboardPrimary {
172+
clipboard.Primary = true
173+
logger.Debug("primary X selection enabled")
174+
}
175+
176+
if err := clipboard.WriteAll(decrypted); err != nil {
117177
logger.WithError(err).Fatal("could not copy password to clipboard")
118178
}
119179
}
120180

121-
func entryPassword(logger *logrus.Logger, vault *enpass.Vault, cardType string, filters []string) {
122-
card, err := vault.GetUniqueEntry(cardType, filters)
181+
func entryPassword(logger *logrus.Logger, vault *enpass.Vault, args *Args) {
182+
card, err := vault.GetEntry(*args.cardType, args.filters, true)
123183
if err != nil {
124184
logger.WithError(err).Fatal("could not retrieve unique card")
125185
}
126186

127-
if password, err := card.Decrypt(); err != nil {
187+
if decrypted, err := card.Decrypt(); err != nil {
128188
logger.WithError(err).Fatal("could not decrypt card")
129189
} else {
130-
fmt.Println(password)
190+
fmt.Println(decrypted)
131191
}
132192
}
133193

134-
func main() {
135-
vaultPath := flag.String("vault", "", "Path to your Enpass vault.")
136-
cardType := flag.String("type", "password", "The type of your card. (password, ...)")
137-
keyFilePath := flag.String("keyfile", "", "Path to your Enpass vault keyfile.")
138-
logLevelStr := flag.String("log", defaultLogLevel.String(), "The log level from debug (5) to error (1).")
139-
nonInteractive := flag.Bool("nonInteractive", false, "Disable prompts and fail instead.")
140-
sort := flag.Bool("sort", false, "Sort the output by title and username of the 'list' and 'show' command.")
141-
trashed := flag.Bool("trashed", false, "Show trashed items in the 'list' and 'show' command.")
142-
clipboardPrimary := flag.Bool("clipboardPrimary", false, "Use primary X selection instead of clipboard for the 'copy' command.")
194+
func assembleVaultCredentials(logger *logrus.Logger, args *Args, store *unlock.SecureStore) *enpass.VaultCredentials {
195+
credentials := &enpass.VaultCredentials{
196+
Password: os.Getenv("MASTERPW"),
197+
KeyfilePath: *args.keyFilePath,
198+
}
143199

144-
flag.Parse()
200+
if !credentials.IsComplete() && store != nil {
201+
var err error
202+
if credentials.DBKey, err = store.Read(); err != nil {
203+
logger.WithError(err).Fatal("could not read credentials from store")
204+
}
205+
logger.Debug("read credentials from store")
206+
}
207+
208+
if !credentials.IsComplete() {
209+
credentials.Password = prompt(logger, args, "master password")
210+
}
211+
212+
return credentials
213+
}
214+
215+
func initializeStore(logger *logrus.Logger, args *Args) *unlock.SecureStore {
216+
vaultPath, _ := filepath.EvalSymlinks(*args.vaultPath)
217+
store, err := unlock.NewSecureStore(filepath.Base(vaultPath), logger.Level)
218+
if err != nil {
219+
logger.WithError(err).Fatal("could not create store")
220+
}
221+
222+
pin := os.Getenv("ENP_PIN")
223+
if pin == "" {
224+
pin = prompt(logger, args, "PIN")
225+
}
226+
if len(pin) < pinMinLength {
227+
logger.Fatal("PIN too short")
228+
}
229+
230+
pepper := os.Getenv("ENP_PIN_PEPPER")
231+
232+
pinKdfIterCount, err := strconv.ParseInt(os.Getenv("ENP_PIN_ITER_COUNT"), 10, 32)
233+
if err != nil {
234+
pinKdfIterCount = pinDefaultKdfIterCount
235+
}
145236

146-
if flag.NArg() == 0 {
147-
fmt.Println("Specify a command: version, list, show, copy, pass")
148-
flag.Usage()
149-
os.Exit(1)
237+
if err := store.GeneratePassphrase(pin, pepper, int(pinKdfIterCount)); err != nil {
238+
logger.WithError(err).Fatal("could not initialize store")
150239
}
151240

152-
logLevel, err := logrus.ParseLevel(*logLevelStr)
241+
return store
242+
}
243+
244+
func main() {
245+
args := &Args{}
246+
args.parse()
247+
248+
logLevel, err := logrus.ParseLevel(*args.logLevelStr)
153249
if err != nil {
154250
logrus.WithError(err).Fatal("invalid log level specified")
155251
}
156252
logger := logrus.New()
157253
logger.SetLevel(logLevel)
158254

159-
command := strings.ToLower(flag.Arg(0))
160-
filters := flag.Args()[1:]
161-
162-
interactive = !*nonInteractive
163-
164-
if *clipboardPrimary {
165-
clipboard.Primary = true
166-
logger.Debug("primary X selection enabled")
255+
if _, contains := commands[args.command]; !contains {
256+
printHelp()
257+
logger.Exit(1)
167258
}
168259

169-
if command == "version" {
260+
switch args.command {
261+
case cmdHelp:
262+
printHelp()
263+
return
264+
case cmdVersion:
170265
logger.Printf(
171266
"%s arch=%s os=%s version=%s",
172267
filepath.Base(os.Args[0]), runtime.GOARCH, runtime.GOOS, version,
173268
)
174269
return
175270
}
176271

177-
masterPassword := os.Getenv("MASTERPW")
178-
if masterPassword == "" {
179-
masterPassword = prompt(logger, "master password")
272+
vault, err := enpass.NewVault(*args.vaultPath, logger.Level)
273+
if err != nil {
274+
logger.WithError(err).Fatal("could not create vault")
180275
}
181276

182-
if masterPassword == "" {
183-
logger.Fatal("no master password provided. (via cli or MASTERPW env variable)")
277+
var store *unlock.SecureStore
278+
if !*args.pinEnable {
279+
logger.Debug("PIN disabled")
280+
} else {
281+
logger.Debug("PIN enabled, using store")
282+
store = initializeStore(logger, args)
283+
logger.Debug("initialized store")
184284
}
185285

186-
vault := enpass.Vault{Logger: *logrus.New()}
187-
vault.Logger.SetLevel(logger.Level)
286+
credentials := assembleVaultCredentials(logger, args, store)
188287

189-
if err := vault.Initialize(*vaultPath, *keyFilePath, masterPassword); err != nil {
288+
defer func() {
289+
vault.Close()
290+
}()
291+
if err := vault.Open(credentials); err != nil {
190292
logger.WithError(err).Error("could not open vault")
191293
logger.Exit(2)
192294
}
193-
defer func() { _ = vault.Close() }()
194-
195-
logger.Debug("initialized vault")
295+
logger.Debug("opened vault")
196296

197-
switch command {
198-
case "list":
199-
listEntries(logger, &vault, *cardType, *sort, *trashed, filters)
200-
case "show":
201-
showEntries(logger, &vault, *cardType, *sort, *trashed, filters)
202-
case "copy":
203-
copyEntry(logger, &vault, *cardType, filters)
204-
case "pass":
205-
entryPassword(logger, &vault, *cardType, filters)
297+
switch args.command {
298+
case cmdDryRun:
299+
logger.Debug("dry run complete") // just init vault and store without doing anything
300+
case cmdList:
301+
listEntries(logger, vault, args)
302+
case cmdShow:
303+
showEntries(logger, vault, args)
304+
case cmdCopy:
305+
copyEntry(logger, vault, args)
306+
case cmdPass:
307+
entryPassword(logger, vault, args)
206308
default:
207-
logger.WithField("command", command).Fatal("unknown command")
309+
logger.WithField("command", args.command).Fatal("unknown command")
310+
}
311+
312+
if store != nil {
313+
if err := store.Write(credentials.DBKey); err != nil {
314+
logger.WithError(err).Fatal("failed to write credentials to store")
315+
}
208316
}
209317
}

0 commit comments

Comments
 (0)