Skip to content

Commit

Permalink
Merge pull request #111 from msladek/pin
Browse files Browse the repository at this point in the history
Quick unlock: PIN
  • Loading branch information
hazcod authored Jan 16, 2022
2 parents 6a0861d + c99d0bb commit 8f3f1be
Show file tree
Hide file tree
Showing 8 changed files with 468 additions and 138 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ Commands
| `show FILTER` | List vault entries matching FILTER with password |
| `copy FILTER` | Copy the password of a vault entry matching FILTER to the clipboard |
| `pass FILTER` | Print the password of a vaulty entry matching FILTER to stdout |
| `dryrun` | Opens the vault without reading anything from it |
| `version` | Print the version |
| `help` | Print the help text |

Flags
-----
Expand All @@ -45,6 +47,7 @@ Flags
| `-type=TYPE` | The type of your card (password, ...) |
| `-log=LEVEL` | The log level from debug (5) to error (1) |
| `-nonInteractive` | Disable prompts and fail instead |
| `-pin` | Enable Quick Unlock using a PIN |
| `-sort` | Sort the output by title and username of the `list` and `show` command |
| `-trashed` | Show trashed items in the `list` and `show` command |
| `-clipboardPrimary` | Use primary X selection instead of clipboard for the `copy` command |
Expand Down
250 changes: 179 additions & 71 deletions cmd/enpasscli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,73 @@ import (
"os"
"path/filepath"
"runtime"
s "sort"
"sort"
"strconv"
"strings"

"github.com/hazcod/enpass-cli/pkg/clipboard"
"github.com/hazcod/enpass-cli/pkg/enpass"
"github.com/hazcod/enpass-cli/pkg/unlock"
"github.com/miquella/ask"
"github.com/sirupsen/logrus"
)

const (
defaultLogLevel = logrus.InfoLevel
// commands
cmdVersion = "version"
cmdHelp = "help"
cmdDryRun = "dryrun"
cmdList = "list"
cmdShow = "show"
cmdCopy = "copy"
cmdPass = "pass"
// defaults
defaultLogLevel = logrus.InfoLevel
pinMinLength = 8
pinDefaultKdfIterCount = 100000
)

var (
// overwritten by go build
version = "dev"
// enables prompts
interactive = true
// set of all commands
commands = map[string]struct{}{cmdVersion: {}, cmdHelp: {}, cmdDryRun: {}, cmdList: {},
cmdShow: {}, cmdCopy: {}, cmdPass: {}}
)

func prompt(logger *logrus.Logger, msg string) string {
if interactive {
type Args struct {
command string
// params
filters []string
// flags
vaultPath *string
cardType *string
keyFilePath *string
logLevelStr *string
nonInteractive *bool
pinEnable *bool
sort *bool
trashed *bool
clipboardPrimary *bool
}

func (args *Args) parse() {
args.vaultPath = flag.String("vault", "", "Path to your Enpass vault.")
args.cardType = flag.String("type", "password", "The type of your card. (password, ...)")
args.keyFilePath = flag.String("keyfile", "", "Path to your Enpass vault keyfile.")
args.logLevelStr = flag.String("log", defaultLogLevel.String(), "The log level from debug (5) to error (1).")
args.nonInteractive = flag.Bool("nonInteractive", false, "Disable prompts and fail instead.")
args.pinEnable = flag.Bool("pin", false, "Enable PIN.")
args.sort = flag.Bool("sort", false, "Sort the output by title and username of the 'list' and 'show' command.")
args.trashed = flag.Bool("trashed", false, "Show trashed items in the 'list' and 'show' command.")
args.clipboardPrimary = flag.Bool("clipboardPrimary", false, "Use primary X selection instead of clipboard for the 'copy' command.")
flag.Parse()
args.command = strings.ToLower(flag.Arg(0))
args.filters = flag.Args()[1:]
}

func prompt(logger *logrus.Logger, args *Args, msg string) string {
if !*args.nonInteractive {
if response, err := ask.HiddenAsk("Enter " + msg + ": "); err != nil {
logger.WithError(err).Fatal("could not prompt for " + msg)
} else {
Expand All @@ -37,27 +82,37 @@ func prompt(logger *logrus.Logger, msg string) string {
return ""
}

func printHelp() {
fmt.Print("Valid commands: ")
for cmd := range commands {
fmt.Printf("%s, ", cmd)
}
fmt.Println()
flag.Usage()
os.Exit(1)
}

func sortEntries(cards []enpass.Card) {
// Sort by username preserving original order
s.SliceStable(cards, func(i, j int) bool {
sort.SliceStable(cards, func(i, j int) bool {
return strings.ToLower(cards[i].Subtitle) < strings.ToLower(cards[j].Subtitle)
})
// Sort by title, preserving username order
s.SliceStable(cards, func(i, j int) bool {
sort.SliceStable(cards, func(i, j int) bool {
return strings.ToLower(cards[i].Title) < strings.ToLower(cards[j].Title)
})
}

func listEntries(logger *logrus.Logger, vault *enpass.Vault, cardType string, sort bool, trashed bool, filters []string) {
cards, err := vault.GetEntries(cardType, filters)
func listEntries(logger *logrus.Logger, vault *enpass.Vault, args *Args) {
cards, err := vault.GetEntries(*args.cardType, args.filters)
if err != nil {
logger.WithError(err).Fatal("could not retrieve cards")
}
if sort {
if *args.sort {
sortEntries(cards)
}
for _, card := range cards {
if card.IsTrashed() && !trashed {
if card.IsTrashed() && !*args.trashed {
continue
}
logger.Printf(
Expand All @@ -71,19 +126,19 @@ func listEntries(logger *logrus.Logger, vault *enpass.Vault, cardType string, so
}
}

func showEntries(logger *logrus.Logger, vault *enpass.Vault, cardType string, sort bool, trashed bool, filters []string) {
cards, err := vault.GetEntries(cardType, filters)
func showEntries(logger *logrus.Logger, vault *enpass.Vault, args *Args) {
cards, err := vault.GetEntries(*args.cardType, args.filters)
if err != nil {
logger.WithError(err).Fatal("could not retrieve cards")
}
if sort {
if *args.sort {
sortEntries(cards)
}
for _, card := range cards {
if card.IsTrashed() && !trashed {
if card.IsTrashed() && !*args.trashed {
continue
}
password, err := card.Decrypt()
decrypted, err := card.Decrypt()
if err != nil {
logger.WithError(err).Error("could not decrypt " + card.Title)
continue
Expand All @@ -97,113 +152,166 @@ func showEntries(logger *logrus.Logger, vault *enpass.Vault, cardType string, so
card.Title,
card.Subtitle,
card.Category,
password,
decrypted,
)
}
}

func copyEntry(logger *logrus.Logger, vault *enpass.Vault, cardType string, filters []string) {
card, err := vault.GetUniqueEntry(cardType, filters)
func copyEntry(logger *logrus.Logger, vault *enpass.Vault, args *Args) {
card, err := vault.GetEntry(*args.cardType, args.filters, true)
if err != nil {
logger.WithError(err).Fatal("could not retrieve unique card")
}

password, err := card.Decrypt()
decrypted, err := card.Decrypt()
if err != nil {
logger.WithError(err).Fatal("could not decrypt card")
}

if err := clipboard.WriteAll(password); err != nil {
if *args.clipboardPrimary {
clipboard.Primary = true
logger.Debug("primary X selection enabled")
}

if err := clipboard.WriteAll(decrypted); err != nil {
logger.WithError(err).Fatal("could not copy password to clipboard")
}
}

func entryPassword(logger *logrus.Logger, vault *enpass.Vault, cardType string, filters []string) {
card, err := vault.GetUniqueEntry(cardType, filters)
func entryPassword(logger *logrus.Logger, vault *enpass.Vault, args *Args) {
card, err := vault.GetEntry(*args.cardType, args.filters, true)
if err != nil {
logger.WithError(err).Fatal("could not retrieve unique card")
}

if password, err := card.Decrypt(); err != nil {
if decrypted, err := card.Decrypt(); err != nil {
logger.WithError(err).Fatal("could not decrypt card")
} else {
fmt.Println(password)
fmt.Println(decrypted)
}
}

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

flag.Parse()
if !credentials.IsComplete() && store != nil {
var err error
if credentials.DBKey, err = store.Read(); err != nil {
logger.WithError(err).Fatal("could not read credentials from store")
}
logger.Debug("read credentials from store")
}

if !credentials.IsComplete() {
credentials.Password = prompt(logger, args, "master password")
}

return credentials
}

func initializeStore(logger *logrus.Logger, args *Args) *unlock.SecureStore {
vaultPath, _ := filepath.EvalSymlinks(*args.vaultPath)
store, err := unlock.NewSecureStore(filepath.Base(vaultPath), logger.Level)
if err != nil {
logger.WithError(err).Fatal("could not create store")
}

pin := os.Getenv("ENP_PIN")
if pin == "" {
pin = prompt(logger, args, "PIN")
}
if len(pin) < pinMinLength {
logger.Fatal("PIN too short")
}

pepper := os.Getenv("ENP_PIN_PEPPER")

pinKdfIterCount, err := strconv.ParseInt(os.Getenv("ENP_PIN_ITER_COUNT"), 10, 32)
if err != nil {
pinKdfIterCount = pinDefaultKdfIterCount
}

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

logLevel, err := logrus.ParseLevel(*logLevelStr)
return store
}

func main() {
args := &Args{}
args.parse()

logLevel, err := logrus.ParseLevel(*args.logLevelStr)
if err != nil {
logrus.WithError(err).Fatal("invalid log level specified")
}
logger := logrus.New()
logger.SetLevel(logLevel)

command := strings.ToLower(flag.Arg(0))
filters := flag.Args()[1:]

interactive = !*nonInteractive

if *clipboardPrimary {
clipboard.Primary = true
logger.Debug("primary X selection enabled")
if _, contains := commands[args.command]; !contains {
printHelp()
logger.Exit(1)
}

if command == "version" {
switch args.command {
case cmdHelp:
printHelp()
return
case cmdVersion:
logger.Printf(
"%s arch=%s os=%s version=%s",
filepath.Base(os.Args[0]), runtime.GOARCH, runtime.GOOS, version,
)
return
}

masterPassword := os.Getenv("MASTERPW")
if masterPassword == "" {
masterPassword = prompt(logger, "master password")
vault, err := enpass.NewVault(*args.vaultPath, logger.Level)
if err != nil {
logger.WithError(err).Fatal("could not create vault")
}

if masterPassword == "" {
logger.Fatal("no master password provided. (via cli or MASTERPW env variable)")
var store *unlock.SecureStore
if !*args.pinEnable {
logger.Debug("PIN disabled")
} else {
logger.Debug("PIN enabled, using store")
store = initializeStore(logger, args)
logger.Debug("initialized store")
}

vault := enpass.Vault{Logger: *logrus.New()}
vault.Logger.SetLevel(logger.Level)
credentials := assembleVaultCredentials(logger, args, store)

if err := vault.Initialize(*vaultPath, *keyFilePath, masterPassword); err != nil {
defer func() {
vault.Close()
}()
if err := vault.Open(credentials); err != nil {
logger.WithError(err).Error("could not open vault")
logger.Exit(2)
}
defer func() { _ = vault.Close() }()

logger.Debug("initialized vault")
logger.Debug("opened vault")

switch command {
case "list":
listEntries(logger, &vault, *cardType, *sort, *trashed, filters)
case "show":
showEntries(logger, &vault, *cardType, *sort, *trashed, filters)
case "copy":
copyEntry(logger, &vault, *cardType, filters)
case "pass":
entryPassword(logger, &vault, *cardType, filters)
switch args.command {
case cmdDryRun:
logger.Debug("dry run complete") // just init vault and store without doing anything
case cmdList:
listEntries(logger, vault, args)
case cmdShow:
showEntries(logger, vault, args)
case cmdCopy:
copyEntry(logger, vault, args)
case cmdPass:
entryPassword(logger, vault, args)
default:
logger.WithField("command", command).Fatal("unknown command")
logger.WithField("command", args.command).Fatal("unknown command")
}

if store != nil {
if err := store.Write(credentials.DBKey); err != nil {
logger.WithError(err).Fatal("failed to write credentials to store")
}
}
}
Loading

0 comments on commit 8f3f1be

Please sign in to comment.