diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2891c2d --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +# make debug PKG=./pkg/util +PKG ?= ./... +DLV_PORT ?= 38697 +COMMAND ?= + +test: + go test $(PKG) + +test-debug: + dlv test $(PKG) --headless --listen=:$(DLV_PORT) --api-version=2 + +build: + go build + +dev: + go run main.go app.go $(COMMAND) + +.PHONY: test debug build dev diff --git a/app.go b/app.go index e88ba18..1e9f939 100644 --- a/app.go +++ b/app.go @@ -2,22 +2,22 @@ package main import ( "context" - "fmt" "os" "sort" ap "passline/pkg/action" "passline/pkg/config" "passline/pkg/ctxutil" + "passline/pkg/util" "github.com/blang/semver" - ucli "github.com/urfave/cli/v2" + ucli "github.com/urfave/cli/v3" "golang.org/x/term" ) -func setupApp(ctx context.Context, sv semver.Version) (context.Context, *ucli.App) { +func setupApp(ctx context.Context, sv semver.Version) (context.Context, *ucli.Command) { // try to load config - cfg, err := config.Get() + cfg, err := config.Get(util.OSFileSystem{}) if err != nil { os.Exit(ap.ExitConfig) } @@ -30,27 +30,15 @@ func setupApp(ctx context.Context, sv semver.Version) (context.Context, *ucli.Ap os.Exit(ap.ExitUnknown) } - app := ucli.NewApp() - app.Name = "Passline" - app.Usage = "Password manager" - app.HelpName = "passline" - app.Version = sv.String() - app.Description = "Password manager for the command line" - app.EnableBashCompletion = true - - // Append website information to default helper print - app.CustomAppHelpTemplate = fmt.Sprintf(`%s -WEBSITE: - https://github.com/perryrh0dan/passline - - `, ucli.AppHelpTemplate) + var app = ucli.Command{ + Name: "Passline", + Usage: "Password manager", + Version: sv.String(), + Description: "Password manager for the command line", + EnableShellCompletion: true, + } app.Flags = []ucli.Flag{ - &ucli.BoolFlag{ - Name: "print", - Aliases: []string{"p"}, - Usage: "Prints the password to the terminal", - }, &ucli.BoolFlag{ Name: "yes", Usage: "Assume yes on all yes/no questions or use the default on all others", @@ -64,19 +52,23 @@ WEBSITE: Name: "noclip", Usage: "Disable copy to clipboard", }, + &ucli.BoolFlag{ + Name: "print", + Aliases: []string{"p"}, + Usage: "Prints the password to the terminal", + }, } // default command to get password - app.Action = func(c *ucli.Context) error { - return action.Default(c) + app.Action = func(c context.Context, command *ucli.Command) error { + return action.Default(c, command) } app.Commands = action.GetCommands() sort.Sort(ucli.FlagsByName(app.Flags)) - sort.Sort(ucli.CommandsByName(app.Commands)) - return ctx, app + return ctx, &app } func initContext(ctx context.Context, cfg *config.Config) context.Context { diff --git a/changelog.md b/changelog.md index 74836a1..4f769b0 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,12 @@ Changelog for passline +## Version 1.15.0 + +### Feature + +- Add experimental full encryption mode + ## Version 1.12.0 ### Feature @@ -52,7 +58,7 @@ Changelog for passline ### Fix -- Set default category to "*" +- Set default category to "\*" - Display Bug in edit crdential ## Version 1.7.0 @@ -60,7 +66,7 @@ Changelog for passline ### Feature - Add filterable categories to credentials -- Set Default category in the config.json +- Set Default category in the config.json ## Version 1.5.4 @@ -106,7 +112,6 @@ Changelog for passline - Increment version indicator - ## Version 0.3.0 - Generate Password method generates now password with at least one special character, small character, capital character and number. diff --git a/go.mod b/go.mod index 4400492..a06560a 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module passline -go 1.22 +go 1.23.4 require ( atomicgo.dev/keyboard v0.2.9 @@ -14,7 +14,8 @@ require ( github.com/mitchellh/go-ps v1.0.0 github.com/pkg/errors v0.9.1 github.com/rhysd/go-github-selfupdate v1.2.3 - github.com/urfave/cli/v2 v2.25.3 + github.com/stretchr/testify v1.10.0 + github.com/urfave/cli/v3 v3.3.8 golang.org/x/crypto v0.21.0 golang.org/x/net v0.23.0 golang.org/x/sys v0.18.0 @@ -32,7 +33,7 @@ require ( cloud.google.com/go/longrunning v0.4.1 // indirect cloud.google.com/go/storage v1.30.1 // indirect github.com/containerd/console v1.0.3 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-cmp v0.5.9 // indirect @@ -45,10 +46,10 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/tcnksm/go-gitconfig v0.1.2 // indirect github.com/ulikunitz/xz v0.5.9 // indirect - github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect go.opencensus.io v0.24.0 // indirect golang.org/x/oauth2 v0.7.0 // indirect golang.org/x/sync v0.1.0 // indirect @@ -58,4 +59,5 @@ require ( google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index a07eb20..3558b50 100644 --- a/go.sum +++ b/go.sum @@ -37,8 +37,6 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= -github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -102,8 +100,10 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02 github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -121,6 +121,7 @@ github.com/onsi/gomega v1.4.2 h1:3mYCb7aPxS/RU7TI1y4rkEn1oKmPRjNJLNEXgw7MH2I= github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= @@ -135,29 +136,29 @@ github.com/rhysd/go-github-selfupdate v1.2.3 h1:iaa+J202f+Nc+A8zi75uccC8Wg3omaM7 github.com/rhysd/go-github-selfupdate v1.2.3/go.mod h1:mp/N8zj6jFfBQy/XMYoWsmfzxazpPAODuqarmPDe2Rg= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw= github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE= github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I= github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/urfave/cli/v2 v2.25.3 h1:VJkt6wvEBOoSjPFQvOkv6iWIrsJyCrKGtCtxXWwmGeY= -github.com/urfave/cli/v2 v2.25.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= +github.com/urfave/cli/v3 v3.3.8 h1:BzolUExliMdet9NlJ/u4m5vHSotJ3PzEqSAZ1oPMa/E= +github.com/urfave/cli/v3 v3.3.8/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -259,6 +260,7 @@ google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGm google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2 h1:MZF6J7CV6s/h0HBkfqebrYfKCVEo5iN+wzE4QhV3Evo= @@ -270,6 +272,7 @@ gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/main.go b/main.go index 9cd00fd..70f7dad 100644 --- a/main.go +++ b/main.go @@ -23,7 +23,7 @@ const ( var ( // Version is the released version of passline - version string = "1.14.2" + version string = "1.15.0" // BuildTime is the time the binary was built date string ) @@ -34,7 +34,7 @@ func main() { // Get the initial state of the terminal. initialTermState, _ := term.GetState(int(syscall.Stdin)) - //trap Ctrl+C and call cancel on the context + // Trap Ctrl+C and call cancel on the context ctx, cancel := context.WithCancel(ctx) c := make(chan os.Signal) signal.Notify(c, os.Interrupt, syscall.SIGTERM, os.Kill) @@ -63,7 +63,7 @@ func main() { } ctx, app := setupApp(ctx, sv) - if err := app.RunContext(ctx, os.Args); err != nil { + if err := app.Run(ctx, os.Args); err != nil { log.Fatal(err) } } diff --git a/pkg/action/action.go b/pkg/action/action.go index 0728278..7ef4fa0 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -2,20 +2,17 @@ package action import ( "context" - "fmt" "os" "path/filepath" - "passline/pkg/cli/input" "passline/pkg/cli/selection" "passline/pkg/config" - "passline/pkg/crypt" "passline/pkg/ctxutil" "passline/pkg/out" "passline/pkg/storage" "github.com/blang/semver" - ucli "github.com/urfave/cli/v2" + ucli "github.com/urfave/cli/v3" ) // Action knows everything to run passline CLI actions @@ -69,75 +66,6 @@ func (s *Action) selectCredential(ctx context.Context, args ucli.Args, item stor return credential, nil } -func (s *Action) getMasterKey(ctx context.Context) ([]byte, error) { - // Get encrypted content encryption key from store - encryptedEncryptionKey, err := s.Store.GetKey(ctx) - if err != nil { - return []byte{}, ExitError(ExitUnknown, err, "Unable to load key: %s", err) - } - - if encryptedEncryptionKey != "" { - // If encrypted encryption key exists decrypt it - envKey := []byte(os.Getenv("PASSLINE_MASTER_KEY")) - if len(envKey) > 0 { - encryptionKey, err := crypt.DecryptKey(envKey, encryptedEncryptionKey) - if err == nil { - return []byte(encryptionKey), nil - } - } - - // try password three times - counter := 0 - for counter < 3 { - password := input.Password("Enter master password: ") - fmt.Println() - - encryptionKey, err := crypt.DecryptKey(password, encryptedEncryptionKey) - if err == nil { - return []byte(encryptionKey), nil - } else if counter != 2 { - fmt.Println("Wrong password! Please try again") - } - - counter++ - } - - return []byte{}, ExitError(ExitPassword, err, "Wrong Password") - } - - // initiate new encryption key - encryptionKey, err := s.initMasterKey(ctx) - if err != nil { - return nil, err - } - - return encryptionKey, nil -} - -func (s *Action) initMasterKey(ctx context.Context) ([]byte, error) { - decryptedEncryptionKey, err := crypt.GenerateKey() - if err != nil { - return []byte{}, ExitError(ExitUnknown, err, "Unable to generate key: %s", err) - } - - password := input.Password("Enter master password: ") - fmt.Println() - passwordTwo := input.Password("Enter master password again: ") - fmt.Println() - - if string(password) != string(passwordTwo) { - return []byte{}, ExitError(ExitPassword, err, "Password do not match") - } - - encryptedEncryptionKey, err := crypt.EncryptKey(password, decryptedEncryptionKey) - if err != nil { - return []byte{}, ExitError(ExitUnknown, err, "Unable to store key: %s", err) - } - s.Store.SetKey(ctx, encryptedEncryptionKey) - - return []byte(decryptedEncryptionKey), nil -} - func (s *Action) getSites(ctx context.Context) ([]storage.Item, error) { items, err := s.Store.GetAllItems(ctx) if err != nil { diff --git a/pkg/action/add.go b/pkg/action/add.go index 38766d3..15bf077 100644 --- a/pkg/action/add.go +++ b/pkg/action/add.go @@ -3,28 +3,27 @@ package action import ( "context" "passline/pkg/cli/input" - "passline/pkg/crypt" "passline/pkg/ctxutil" "passline/pkg/out" "passline/pkg/storage" "passline/pkg/util" - ucli "github.com/urfave/cli/v2" + ucli "github.com/urfave/cli/v3" ) -func addParseArgs(c *ucli.Context) context.Context { - ctx := ctxutil.WithGlobalFlags(c) - if c.IsSet("advanced") { - ctx = ctxutil.WithAdvanced(ctx, c.Bool("advanced")) +func addParseArgs(c context.Context, cmd *ucli.Command) context.Context { + ctx := ctxutil.WithGlobalFlags(c, cmd) + if cmd.IsSet("advanced") { + ctx = ctxutil.WithAdvanced(ctx, cmd.Bool("advanced")) } return ctx } -func (s *Action) Add(c *ucli.Context) error { - ctx := addParseArgs(c) +func (s *Action) Add(c context.Context, cmd *ucli.Command) error { + ctx := addParseArgs(c, cmd) - args := c.Args() + args := cmd.Args() out.CreateMessage() // User input name @@ -65,12 +64,12 @@ func (s *Action) Add(c *ucli.Context) error { } if ctxutil.IsAdvanced(ctx) { - category, err = input.Default("Please enter a category []: (%s)", "default", "") + category, err = input.Default("Please enter a category (%s): ", ctxutil.GetCategory(ctx), "") if err != nil { return err } - comment, err = input.Default("Please enter a comment []: (%s)", "default", "") + comment, err = input.Default("Please enter a comment []:", "", "") if err != nil { return err } @@ -86,7 +85,7 @@ func (s *Action) Add(c *ucli.Context) error { } // get and check global password - globalPassword, err := s.getMasterKey(ctx) + globalPassword, err := s.Store.GetDecryptedKey(ctx, "to encrypt the new password") if err != nil { return err } @@ -94,7 +93,7 @@ func (s *Action) Add(c *ucli.Context) error { // Create Credentials credential := storage.Credential{Username: username, Password: password, RecoveryCodes: recoveryCodes, Category: category, Comment: comment} - err = crypt.EncryptCredential(&credential, globalPassword) + err = storage.EncryptCredential(&credential, globalPassword) if err != nil { return ExitError(ExitEncrypt, err, "Error Encrypting credentials") } @@ -107,7 +106,8 @@ func (s *Action) Add(c *ucli.Context) error { credential.Password = password out.SuccessfulAddedItem(name, credential.Username) - if c.Bool("print") { + + if cmd.Bool("print") { out.DisplayCredential(credential) } diff --git a/pkg/action/backup.go b/pkg/action/backup.go index f3dacce..734f9ae 100644 --- a/pkg/action/backup.go +++ b/pkg/action/backup.go @@ -14,20 +14,19 @@ import ( "passline/pkg/out" "passline/pkg/storage" - ucli "github.com/urfave/cli/v2" + ucli "github.com/urfave/cli/v3" ) -func (s *Action) Backup(c *ucli.Context) error { - ctx := ctxutil.WithGlobalFlags(c) +func (s *Action) Backup(c context.Context, cmd *ucli.Command) error { + ctx := ctxutil.WithGlobalFlags(c, cmd) - args := c.Args() + args := cmd.Args() out.BackupMessage() - //TODO this should happen in config - path := config.Directory() + "/backup" + path := config.BackupDirectory() now := time.Now().Format("2006-01-02-15-04-05") - path = filepath.Join(path, now) + path = filepath.Join(path, now) + ".json" path, err := input.ArgOrInput(args, 0, "Path", path, "required") if err != nil { @@ -44,7 +43,7 @@ func (s *Action) Backup(c *ucli.Context) error { } func (s *Action) backup(ctx context.Context, path string) error { - items, err := s.Store.GetAllItems(ctx) + items, err := s.Store.GetRawItems(ctx) if err != nil { return err } @@ -58,14 +57,24 @@ func (s *Action) backup(ctx context.Context, path string) error { path = path + ".json" } - time := time.Now() - data := storage.Backup{ - Date: time, - Key: key, + t := time.Now() + type Alias storage.Backup + aux := struct { + Items json.RawMessage `json:"items"` + *Alias + }{ Items: items, + Alias: (*Alias)(&storage.Backup{ + Date: t, + Key: key, + }), + } + + file, err := json.MarshalIndent(aux, "", " ") + if err != nil { + return err } - file, _ := json.MarshalIndent(data, "", " ") _ = os.WriteFile(path, file, 0644) return nil diff --git a/pkg/action/category-list.go b/pkg/action/category-list.go index 569c902..93c7509 100644 --- a/pkg/action/category-list.go +++ b/pkg/action/category-list.go @@ -1,14 +1,15 @@ package action import ( + "context" "passline/pkg/ctxutil" "passline/pkg/out" - ucli "github.com/urfave/cli/v2" + ucli "github.com/urfave/cli/v3" ) -func (s *Action) CategoryList(c *ucli.Context) error { - ctx := ctxutil.WithGlobalFlags(c) +func (s *Action) CategoryList(c context.Context, cmd *ucli.Command) error { + ctx := ctxutil.WithGlobalFlags(c, cmd) items, err := s.getSites(ctx) if err != nil { diff --git a/pkg/action/commands.go b/pkg/action/commands.go index 3f8f5bf..64e5088 100644 --- a/pkg/action/commands.go +++ b/pkg/action/commands.go @@ -1,7 +1,7 @@ package action import ( - ucli "github.com/urfave/cli/v2" + ucli "github.com/urfave/cli/v3" ) // GetCommands returns the ucli commands exported by this module @@ -12,14 +12,7 @@ func (s *Action) GetCommands() []*ucli.Command { Aliases: []string{"a"}, Usage: "Adds an existing password for a website", ArgsUsage: " ", - Flags: []ucli.Flag{ - &ucli.BoolFlag{ - Name: "advanced", - Aliases: []string{"a"}, - Usage: "Enable advanced mode", - }, - }, - Action: s.Add, + Action: s.Add, }, { Name: "backup", @@ -29,8 +22,9 @@ func (s *Action) GetCommands() []*ucli.Command { Action: s.Backup, }, { - Name: "category", - Subcommands: []*ucli.Command{ + Name: "category", + Usage: "Manage categories", + Commands: []*ucli.Command{ { Name: "list", Aliases: []string{"ls"}, @@ -76,7 +70,7 @@ func (s *Action) GetCommands() []*ucli.Command { }, { Name: "me", - Usage: "Display info card with default username and phone number", + Usage: "Displays default username and phone number", Action: s.Me, }, { @@ -92,6 +86,12 @@ func (s *Action) GetCommands() []*ucli.Command { ArgsUsage: "", Action: s.Restore, }, + { + Name: "sync", + Aliases: []string{"s"}, + Usage: "Reapply config, such as encryption mode", + Action: s.Sync, + }, { Name: "unclip", Usage: "Internal command to clear clipboard", diff --git a/pkg/action/default.go b/pkg/action/default.go index 6b1b61f..51959e3 100644 --- a/pkg/action/default.go +++ b/pkg/action/default.go @@ -1,19 +1,22 @@ package action import ( + "context" "passline/pkg/cli/selection" "passline/pkg/clipboard" - "passline/pkg/crypt" "passline/pkg/ctxutil" "passline/pkg/out" + "passline/pkg/storage" - ucli "github.com/urfave/cli/v2" + ucli "github.com/urfave/cli/v3" ) -func (s *Action) Default(c *ucli.Context) error { - ctx := ctxutil.WithGlobalFlags(c) +func (s *Action) Default(c context.Context, cmd *ucli.Command) error { + ctx := ctxutil.WithGlobalFlags(c, cmd) + + args := cmd.Args() + out.DisplayMessage() - // Get all Sites names, err := s.getItemNamesByCategory(ctx) if err != nil { return err @@ -24,9 +27,6 @@ func (s *Action) Default(c *ucli.Context) error { return ExitError(ExitNotFound, err, "No items found") } - args := c.Args() - out.DisplayMessage() - name, err := selection.ArgOrSelect(ctx, args, 0, "URL", names) if err != nil { return ExitError(ExitUnknown, err, "Error selecting item: %s", err) @@ -45,17 +45,17 @@ func (s *Action) Default(c *ucli.Context) error { if ctxutil.IsQuickSelect(ctx) && !ctxutil.IsNoClip(ctx) { // disable notifications for quick select if err = clipboard.CopyTo(ctxutil.WithNotifications(ctx, false), "", []byte(credential.Username)); err != nil { - return ExitError(ExitIO, err, "failed to copy to clipboard: %s", err) + out.ClipboardError() } } // get and check global password - globalPassword, err := s.getMasterKey(ctx) + globalPassword, err := s.Store.GetDecryptedKey(ctx, "to decrypt the password") if err != nil { return err } - err = crypt.DecryptCredential(&credential, globalPassword) + err = storage.DecryptCredential(&credential, globalPassword) if err != nil { return err } @@ -63,15 +63,14 @@ func (s *Action) Default(c *ucli.Context) error { if ctxutil.IsAutoClip(ctx) && !ctxutil.IsNoClip(ctx) { identifier := out.BuildIdentifier(name, credential.Username) if err = clipboard.CopyTo(ctx, identifier, []byte(credential.Password)); err != nil { - return ExitError(ExitIO, err, "failed to copy to clipboard: %s", err) - } - if !c.Bool("print") { + out.FailedCopyToClipboard() + } else { out.SuccessfulCopiedToClipboard(name, credential.Username) - return nil } } - out.DisplayCredential(credential) - out.SuccessfulCopiedToClipboard(name, credential.Username) + if cmd.Bool("print") { + out.DisplayCredential(credential) + } return nil } diff --git a/pkg/action/delete.go b/pkg/action/delete.go index 9743901..4a9d21d 100644 --- a/pkg/action/delete.go +++ b/pkg/action/delete.go @@ -9,11 +9,11 @@ import ( "passline/pkg/ctxutil" "passline/pkg/out" - ucli "github.com/urfave/cli/v2" + ucli "github.com/urfave/cli/v3" ) -func (s *Action) Delete(c *ucli.Context) error { - ctx := ctxutil.WithGlobalFlags(c) +func (s *Action) Delete(c context.Context, cmd *ucli.Command) error { + ctx := ctxutil.WithGlobalFlags(c, cmd) // Get all Sites names, err := s.getItemNamesByCategory(ctx) @@ -26,7 +26,7 @@ func (s *Action) Delete(c *ucli.Context) error { return ExitError(ExitNotFound, err, "No items found") } - args := c.Args() + args := cmd.Args() out.DeleteMessage() name, err := selection.ArgOrSelect(ctx, args, 0, "URL", names) diff --git a/pkg/action/edit.go b/pkg/action/edit.go index dbd554a..b6d5f2a 100644 --- a/pkg/action/edit.go +++ b/pkg/action/edit.go @@ -1,20 +1,21 @@ package action import ( + "context" "fmt" "passline/pkg/cli/input" "passline/pkg/cli/selection" - "passline/pkg/crypt" "passline/pkg/ctxutil" "passline/pkg/out" + "passline/pkg/storage" "passline/pkg/util" - ucli "github.com/urfave/cli/v2" + ucli "github.com/urfave/cli/v3" ) -func (s *Action) Edit(c *ucli.Context) error { - ctx := ctxutil.WithGlobalFlags(c) +func (s *Action) Edit(c context.Context, cmd *ucli.Command) error { + ctx := ctxutil.WithGlobalFlags(c, cmd) // Get all Sites names, err := s.getItemNamesByCategory(ctx) @@ -27,7 +28,7 @@ func (s *Action) Edit(c *ucli.Context) error { return ExitError(ExitNotFound, err, "No items found") } - args := c.Args() + args := cmd.Args() out.EditMessage() name, err := selection.ArgOrSelect(ctx, args, 0, "URL", names) @@ -48,49 +49,57 @@ func (s *Action) Edit(c *ucli.Context) error { selectedUsername := credential.Username // get and check global password - globalPassword, err := s.getMasterKey(ctx) + globalPassword, err := s.Store.GetDecryptedKey(ctx, "to decrypt the password") if err != nil { return err } // Decrypt Credentials to display secrets - err = crypt.DecryptCredential(&credential, globalPassword) + err = storage.DecryptCredential(&credential, globalPassword) if err != nil { return err } // Get new URL - newName, err := input.Default("Please enter a new URL []: (%s) ", item.Name, "") + newName, err := input.Default("Please enter a new URL [%s]: ", item.Name, "") if err != nil { return err } // Get new username - newUsername, err := input.Default("Please enter a new username/Login []: (%s) ", credential.Username, "") + newUsername, err := input.Default("Please enter a new username/Login [%s]: ", credential.Username, "") if err != nil { return err } + // Get new password + newPassword := input.Password("Please enter a new password [****]: ") + println() + if len(newPassword) == 0 { + newPassword = []byte(credential.Password) + } + // Get new category - newCategory, err := input.Default("Please enter a new category []: (%s) ", credential.Category, "") + newCategory, err := input.Default("Please enter a new category [%s]: ", credential.Category, "") if err != nil { return err } - newComment, err := input.Default("Please enter a new comment []: (%s) ", credential.Comment, "") + newComment, err := input.Default("Please enter a new comment [%s]: ", credential.Comment, "") if err != nil { return err } - // Get new recoveryCodes + // Get new recovery codes recoveryCodes := util.ArrayToString(credential.RecoveryCodes) - newRecoveryCodes, err := input.Default("Please enter your recovery codes []: (%s) ", recoveryCodes, "") + newRecoveryCodes, err := input.Default("Please enter your recovery codes [%s]: ", recoveryCodes, "") if err != nil { return err } // Edit credential credential.Username = newUsername + credential.Password = string(newPassword) credential.Category = newCategory credential.Comment = newComment credential.RecoveryCodes = make([]string, 0) // TODO remove spaces @@ -100,7 +109,7 @@ func (s *Action) Edit(c *ucli.Context) error { credential.RecoveryCodes = util.StringToArray(newRecoveryCodes) } - err = crypt.EncryptCredential(&credential, globalPassword) + err = storage.EncryptCredential(&credential, globalPassword) if err != nil { return err } diff --git a/pkg/action/errors.go b/pkg/action/errors.go index 75e0545..a304be9 100644 --- a/pkg/action/errors.go +++ b/pkg/action/errors.go @@ -3,7 +3,7 @@ package action import ( "fmt" - ucli "github.com/urfave/cli/v2" + ucli "github.com/urfave/cli/v3" ) const ( diff --git a/pkg/action/generate.go b/pkg/action/generate.go index d929b41..dffdd7f 100644 --- a/pkg/action/generate.go +++ b/pkg/action/generate.go @@ -12,27 +12,27 @@ import ( "passline/pkg/storage" "passline/pkg/util" - ucli "github.com/urfave/cli/v2" + ucli "github.com/urfave/cli/v3" ) const PASSWORD_MIN_LENGTH = 8 -func generateParseArgs(c *ucli.Context) context.Context { - ctx := ctxutil.WithGlobalFlags(c) - if c.IsSet("advanced") { - ctx = ctxutil.WithAdvanced(ctx, c.Bool("advanced")) +func generateParseArgs(c context.Context, cmd *ucli.Command) context.Context { + ctx := ctxutil.WithGlobalFlags(c, cmd) + if cmd.IsSet("advanced") { + ctx = ctxutil.WithAdvanced(ctx, cmd.Bool("advanced")) } - if c.IsSet("force") { - ctx = ctxutil.WithForce(ctx, c.Bool("force")) + if cmd.IsSet("force") { + ctx = ctxutil.WithForce(ctx, cmd.Bool("force")) } return ctx } -func (s *Action) Generate(c *ucli.Context) error { - ctx := generateParseArgs(c) +func (s *Action) Generate(c context.Context, cmd *ucli.Command) error { + ctx := generateParseArgs(c, cmd) - args := c.Args() + args := cmd.Args() out.GenerateMessage() options := crypt.DefaultOptions() @@ -120,7 +120,7 @@ func (s *Action) Generate(c *ucli.Context) error { } // get and check global password - globalPassword, err := s.getMasterKey(ctx) + globalPassword, err := s.Store.GetDecryptedKey(ctx, "to encrypt the new password") if err != nil { return err } @@ -129,7 +129,7 @@ func (s *Action) Generate(c *ucli.Context) error { credential := storage.Credential{Username: username, Password: password, RecoveryCodes: recoveryCodes, Category: category} // Encrypt credentials - err = crypt.EncryptCredential(&credential, globalPassword) + err = storage.EncryptCredential(&credential, globalPassword) if err != nil { return ExitError(ExitEncrypt, err, "Error Encrypting credentials") } @@ -140,21 +140,21 @@ func (s *Action) Generate(c *ucli.Context) error { return ExitError(ExitUnknown, err, "Error occured: %s", err) } - // set unencrypted password to copy to clipboard and to show in terminal + // Set decrypted password to copy to clipboard and to show in terminal credential.Password = password if ctxutil.IsAutoClip(ctx) { identifier := out.BuildIdentifier(name, credential.Username) if err = clipboard.CopyTo(ctx, identifier, []byte(credential.Password)); err != nil { - return ExitError(ExitIO, err, "failed to copy to clipboard: %s", err) - } - if ctxutil.IsAutoClip(ctx) && !c.Bool("print") { + out.FailedCopyToClipboard() + } else { out.SuccessfulCopiedToClipboard(name, credential.Username) - return nil } } - out.DisplayCredential(credential) - out.SuccessfulCopiedToClipboard(name, credential.Username) + if cmd.Bool("print") { + out.DisplayCredential(credential) + } + return nil } diff --git a/pkg/action/list.go b/pkg/action/list.go index 7533a53..a6735df 100644 --- a/pkg/action/list.go +++ b/pkg/action/list.go @@ -1,16 +1,17 @@ package action import ( + "context" "passline/pkg/ctxutil" "passline/pkg/out" - ucli "github.com/urfave/cli/v2" + ucli "github.com/urfave/cli/v3" ) -func (s *Action) List(c *ucli.Context) error { - ctx := ctxutil.WithGlobalFlags(c) +func (s *Action) List(c context.Context, cmd *ucli.Command) error { + ctx := ctxutil.WithGlobalFlags(c, cmd) - args := c.Args() + args := cmd.Args() if args.Len() >= 1 { item, err := s.getSite(ctx, args.Get(0)) diff --git a/pkg/action/me.go b/pkg/action/me.go index 191097d..f3d5083 100644 --- a/pkg/action/me.go +++ b/pkg/action/me.go @@ -1,14 +1,15 @@ package action import ( + "context" "passline/pkg/ctxutil" "passline/pkg/out" - ucli "github.com/urfave/cli/v2" + ucli "github.com/urfave/cli/v3" ) -func (s *Action) Me(c *ucli.Context) error { - ctx := ctxutil.WithGlobalFlags(c) +func (s *Action) Me(c context.Context, cmd *ucli.Command) error { + ctx := ctxutil.WithGlobalFlags(c, cmd) username := ctxutil.GetDefaultUsername(ctx) phoneNumber := ctxutil.GetPhoneNumber(ctx) diff --git a/pkg/action/password.go b/pkg/action/password.go index 816142a..2ef34f8 100644 --- a/pkg/action/password.go +++ b/pkg/action/password.go @@ -1,6 +1,7 @@ package action import ( + "context" "errors" "passline/pkg/cli/input" @@ -8,14 +9,14 @@ import ( "passline/pkg/ctxutil" "passline/pkg/out" - ucli "github.com/urfave/cli/v2" + ucli "github.com/urfave/cli/v3" ) -func (s *Action) Password(c *ucli.Context) error { - ctx := ctxutil.WithGlobalFlags(c) +func (s *Action) Password(c context.Context, cmd *ucli.Command) error { + ctx := ctxutil.WithGlobalFlags(c, cmd) // User input - key, err := s.getMasterKey(ctx) + key, err := s.Store.GetDecryptedKey(ctx, "") if err != nil { return err } diff --git a/pkg/action/restore.go b/pkg/action/restore.go index 4d9e6f9..e740e5e 100644 --- a/pkg/action/restore.go +++ b/pkg/action/restore.go @@ -7,17 +7,18 @@ import ( "os" "passline/pkg/cli/input" + "passline/pkg/crypt" "passline/pkg/ctxutil" "passline/pkg/out" "passline/pkg/storage" - ucli "github.com/urfave/cli/v2" + ucli "github.com/urfave/cli/v3" ) -func (s *Action) Restore(c *ucli.Context) error { - ctx := ctxutil.WithGlobalFlags(c) +func (s *Action) Restore(c context.Context, cmd *ucli.Command) error { + ctx := ctxutil.WithGlobalFlags(c, cmd) - args := c.Args() + args := cmd.Args() out.RestoreMessage() // User input path @@ -45,7 +46,11 @@ func (s *Action) Restore(c *ucli.Context) error { } func (s *Action) restore(ctx context.Context, path string) error { - data := storage.Data{} + type Alias storage.Backup + aux := struct { + Items json.RawMessage `json:"items"` + *Alias + }{} _, err := os.Stat(path) if err != nil { @@ -54,12 +59,53 @@ func (s *Action) restore(ctx context.Context, path string) error { } file, _ := os.ReadFile(path) - _ = json.Unmarshal([]byte(file), &data) + _ = json.Unmarshal([]byte(file), &aux) - err = s.Store.SetData(ctx, data) + rawItems := json.RawMessage(aux.Items) + + globalPassword, err := input.MasterPassword(aux.Key, "to decrypt your vault") + if err != nil { + return err + } + + var js json.RawMessage + + preparedItems := removeQuotes(string(aux.Items)) + if json.Unmarshal([]byte(preparedItems), &js) != nil { + decryptedItems, err := crypt.AesGcmDecrypt(globalPassword, preparedItems) + if err != nil { + return err + } + + rawItems = json.RawMessage(decryptedItems) + } + + items := []storage.Item{} + err = json.Unmarshal(rawItems, &items) + if err != nil { + return err + } + + err = s.Store.SetKey(ctx, aux.Key) + if err != nil { + return err + } + + err = s.Store.SetItems(ctx, items, globalPassword) if err != nil { return err } return nil } + +func removeQuotes(s string) string { + if len(s) > 0 && s[0] == '"' { + s = s[1:] + } + if len(s) > 0 && s[len(s)-1] == '"' { + s = s[:len(s)-1] + } + + return s +} diff --git a/pkg/action/sync.go b/pkg/action/sync.go new file mode 100644 index 0000000..03fb0db --- /dev/null +++ b/pkg/action/sync.go @@ -0,0 +1,27 @@ +package action + +import ( + "context" + "passline/pkg/out" + + ucli "github.com/urfave/cli/v3" +) + +// Sync config settings with vault e.g. reapply encryption mode +func (s *Action) Sync(c context.Context, cmd *ucli.Command) error { + ctx := generateParseArgs(c, cmd) + + out.SyncMessage() + + items, err := s.Store.GetAllItems(ctx) + if err != nil { + return ExitError(ExitUnknown, err, "Failed to get all items: %s", err) + } + + err = s.Store.SetItems(ctx, items, nil) + if err != nil { + return ExitError(ExitUnknown, err, "Failed to update data: %s", err) + } + + return nil +} diff --git a/pkg/action/unclip.go b/pkg/action/unclip.go index 34783b3..c9e7ac0 100644 --- a/pkg/action/unclip.go +++ b/pkg/action/unclip.go @@ -1,23 +1,23 @@ package action import ( + "context" "os" "time" - ucli "github.com/urfave/cli/v2" + ucli "github.com/urfave/cli/v3" "passline/pkg/clipboard" ) // Unclip tries to erase the content of the clipboard -func (s *Action) Unclip(c *ucli.Context) error { - ctx := c.Context - force := c.Bool("force") - timeout := c.Int("timeout") +func (s *Action) Unclip(c context.Context, cmd *ucli.Command) error { + force := cmd.Bool("force") + timeout := cmd.Int("timeout") checksum := os.Getenv("PASSLINE_UNCLIP_CHECKSUM") time.Sleep(time.Second * time.Duration(timeout)) - if err := clipboard.Clear(ctx, checksum, force); err != nil { + if err := clipboard.Clear(c, checksum, force); err != nil { return ExitError(ExitIO, err, "Failed to clear clipboard: %s", err) } return nil diff --git a/pkg/action/update.go b/pkg/action/update.go index 97209ef..3c11ae4 100644 --- a/pkg/action/update.go +++ b/pkg/action/update.go @@ -1,21 +1,22 @@ package action import ( + "context" "log" "os" - + "passline/pkg/cli/input" "passline/pkg/out" "github.com/rhysd/go-github-selfupdate/selfupdate" - ucli "github.com/urfave/cli/v2" + ucli "github.com/urfave/cli/v3" ) const ( repo = "perryrh0dan/passline" ) -func (s *Action) Update(c *ucli.Context) error { +func (s *Action) Update(c context.Context, cmd *ucli.Command) error { latest, found, err := selfupdate.DetectLatest(repo) if err != nil { out.DetectVersionError(err) diff --git a/pkg/cli/input/input.go b/pkg/cli/input/input.go index fc92586..ab2981e 100644 --- a/pkg/cli/input/input.go +++ b/pkg/cli/input/input.go @@ -4,13 +4,14 @@ import ( "bufio" "fmt" "os" + "passline/pkg/crypt" "regexp" "runtime" "strconv" "strings" "syscall" - ucli "github.com/urfave/cli/v2" + ucli "github.com/urfave/cli/v3" "golang.org/x/term" ) @@ -20,9 +21,9 @@ func ArgOrInput(args ucli.Args, index int, message, defaultValue, rules string) if userInput == "" { message := fmt.Sprintf("Please enter a %s", message) if defaultValue != "" { - message += " (%s): " + message += " [%s]: " } else { - message += " []: " + message += ": " } var err error @@ -100,6 +101,40 @@ func Password(message string) []byte { return p } +func MasterPassword(encryptedEncryptionKey string, reason string) ([]byte, error) { + // If encrypted encryption key exists decrypt it + envKey := []byte(os.Getenv("PASSLINE_MASTER_KEY")) + if len(envKey) > 0 { + encryptionKey, err := crypt.DecryptKey(envKey, encryptedEncryptionKey) + if err == nil { + return []byte(encryptionKey), nil + } + } + + prompt := "Enter master password: " + if reason != "" { + prompt = fmt.Sprintf("Enter master password %s: ", reason) + } + + counter := 0 + for counter < 3 { + password := Password(prompt) + fmt.Println() + + encryptionKey, err := crypt.DecryptKey(password, encryptedEncryptionKey) + if err == nil { + return []byte(encryptionKey), nil + } else if counter != 2 { + fmt.Println("Wrong password! Please try again") + } + + counter++ + } + + return []byte{}, fmt.Errorf("Wrong password") + +} + func validate(input string, meta string) bool { input = strings.TrimSpace(input) rules := strings.Split(meta, ",") diff --git a/pkg/cli/selection/selection.go b/pkg/cli/selection/selection.go index f2d35ba..6afb231 100644 --- a/pkg/cli/selection/selection.go +++ b/pkg/cli/selection/selection.go @@ -4,11 +4,11 @@ import ( "context" "errors" "fmt" - "strings" "passline/pkg/cli/terminal" + "passline/pkg/util" - ucli "github.com/urfave/cli/v2" + ucli "github.com/urfave/cli/v3" ) type SelectItem struct { @@ -61,7 +61,8 @@ func arrayContains(l []SelectItem, i string) bool { func filterArray(l []SelectItem, filter string) []SelectItem { filteredNames := make([]SelectItem, 0) for _, i := range l { - if strings.Contains(i.Label, filter) { + _, distance := util.LevenshteinDistanceSubstring(i.Label, filter) + if distance <= 2 { filteredNames = append(filteredNames, i) } } diff --git a/pkg/config/config.go b/pkg/config/config.go index 7f218d4..75d79f9 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -2,11 +2,10 @@ package config import ( "encoding/json" - "io/ioutil" "os" + "passline/pkg/util" "path" "path/filepath" - "strings" "github.com/pkg/errors" ) @@ -20,6 +19,7 @@ var ( type Config struct { Storage string `yaml:"storage"` + Encryption int `yaml:"encryption"` AutoClip bool `yaml:"autoclip"` Notifications bool `yaml:"notifications"` QuickSelect bool `yaml:"quickselect"` @@ -48,9 +48,8 @@ func (c *Config) UnmarshalJSON(data []byte) error { func init() { ensureConfigFile() - config, _ := Get() - _ = ensureMainDir(config) - _ = ensureBackupDir(config) + _ = ensureMainDir() + _ = ensureBackupDir() } func ensureConfigFile() { @@ -61,10 +60,10 @@ func ensureConfigFile() { config := new() file, _ := json.MarshalIndent(config, "", " ") - _ = ioutil.WriteFile(configLocation(), file, 0644) + _ = os.WriteFile(configLocation(), file, 0644) } -func ensureMainDir(config *Config) error { +func ensureMainDir() error { mainDir := Directory() _, err := os.Stat(mainDir) if err != nil { @@ -77,7 +76,7 @@ func ensureMainDir(config *Config) error { return nil } -func ensureBackupDir(config *Config) error { +func ensureBackupDir() error { backupDir := Directory() + "/backup" _, err := os.Stat(backupDir) if err != nil { @@ -90,26 +89,24 @@ func ensureBackupDir(config *Config) error { return nil } -func formatPasslineDir(dirPath string) (string, error) { - homeDir, err := os.UserHomeDir() - if err != nil { - return "", err - } - return path.Join(homeDir, strings.Replace(dirPath, "~", "", 1), ".passline"), nil -} +const ( + PartialEncryption = 1 + FullEncryption = 2 +) func new() Config { return Config{ Storage: "local", AutoClip: true, Notifications: true, + Encryption: PartialEncryption, } } -func Get() (*Config, error) { +func Get(fs util.FileSystem) (*Config, error) { config := new() - file, _ := ioutil.ReadFile(configLocation()) + file, _ := fs.ReadFile(configLocation()) _ = json.Unmarshal([]byte(file), &config) return &config, nil @@ -131,3 +128,7 @@ func configLocation() string { func Directory() string { return filepath.Dir(configLocation()) } + +func BackupDirectory() string { + return Directory() + "/backup" +} diff --git a/pkg/config/context.go b/pkg/config/context.go index 3326aef..e34af41 100644 --- a/pkg/config/context.go +++ b/pkg/config/context.go @@ -26,6 +26,7 @@ func (c *Config) WithContext(ctx context.Context) context.Context { if !ctxutil.HasPhoneNumber(ctx) { ctx = ctxutil.WithPhoneNumber(ctx, c.PhoneNumber) } + ctx = ctxutil.WithEncryption(ctx, c.Encryption) return ctx } diff --git a/pkg/crypt/crypt.go b/pkg/crypt/crypt.go index e57351a..7a567dd 100644 --- a/pkg/crypt/crypt.go +++ b/pkg/crypt/crypt.go @@ -10,8 +10,6 @@ import ( "fmt" "io" - "passline/pkg/storage" - "golang.org/x/crypto/pbkdf2" ) @@ -38,34 +36,6 @@ func DefaultOptions() Options { } } -func DecryptCredential(credential *storage.Credential, globalPassword []byte) error { - err := decryptPassword(credential, globalPassword) - if err != nil { - return err - } - - err = decryptRecoveryCodes(credential, globalPassword) - if err != nil { - return err - } - - return nil -} - -func EncryptCredential(credential *storage.Credential, key []byte) error { - err := encryptPassword(credential, key) - if err != nil { - return err - } - - err = encryptRecoveryCodes(credential, key) - if err != nil { - return err - } - - return nil -} - func EncryptKey(password []byte, key string) (string, error) { pwKey := GetKey(password) encryptedKey, err := AesGcmEncrypt(pwKey, key) @@ -150,53 +120,3 @@ func GetKey(password []byte) []byte { dk := pbkdf2.Key(password, salt, 4096, 32, sha1.New) return dk } - -func encryptPassword(credential *storage.Credential, key []byte) error { - var err error - credential.Password, err = AesGcmEncrypt(key, credential.Password) - if err != nil { - return err - } - - return nil -} - -func encryptRecoveryCodes(credential *storage.Credential, globalPassword []byte) error { - var encryptedRecoveryCodes = make([]string, 0) - - for _, c := range credential.RecoveryCodes { - encryptedRecoveryCode, err := AesGcmEncrypt(globalPassword, c) - if err != nil { - return err - } - encryptedRecoveryCodes = append(encryptedRecoveryCodes, encryptedRecoveryCode) - } - - credential.RecoveryCodes = encryptedRecoveryCodes - return nil -} - -func decryptPassword(credential *storage.Credential, globalPassword []byte) error { - // Decrypt passwords - var err error - credential.Password, err = AesGcmDecrypt(globalPassword, credential.Password) - if err != nil { - return err - } - - return nil -} - -func decryptRecoveryCodes(credential *storage.Credential, globalPassword []byte) error { - var decryptedRecoveryCodes = make([]string, 0) - for _, c := range credential.RecoveryCodes { - decryptedRecoveryCode, err := AesGcmDecrypt(globalPassword, c) - if err != nil { - return err - } - decryptedRecoveryCodes = append(decryptedRecoveryCodes, decryptedRecoveryCode) - } - - credential.RecoveryCodes = decryptedRecoveryCodes - return nil -} diff --git a/pkg/crypt/crypt_test.go b/pkg/crypt/crypt_test.go index de7ecd8..27fd715 100644 --- a/pkg/crypt/crypt_test.go +++ b/pkg/crypt/crypt_test.go @@ -1,7 +1,6 @@ package crypt import ( - "passline/pkg/storage" "testing" ) @@ -35,27 +34,3 @@ func TestEncryptWithShortKey(t *testing.T) { t.Errorf("Encrypt with short key should throw error") } } - -func TestEncryptCredentials(t *testing.T) { - credential := storage.Credential{ - Username: "perry", - Password: "123456789", - RecoveryCodes: []string{"test", "tee"}, - } - err := EncryptCredential(&credential, []byte(key)) - if err != nil || credential.Password == "123456789" { - t.Errorf("EncryptCredential() Password = %s; wanted != %s", credential.Password, "123456789") - } -} - -func TestDecryptCredentials(t *testing.T) { - credential := storage.Credential{ - Username: "perry", - Password: "3IbVQJqqSavvXiRdjffXWh3Z2d-4oxp_0zJ_VIDEcZmJ8aT_5g==", //123456789 - RecoveryCodes: []string{}, - } - err := DecryptCredential(&credential, []byte(key)) - if err != nil || credential.Password != "123456789" { - t.Errorf("EncryptCredential() Password = %s; wanted %s", credential.Password, "123456789") - } -} diff --git a/pkg/ctxutil/ctxutil.go b/pkg/ctxutil/ctxutil.go index bedcf98..646758a 100644 --- a/pkg/ctxutil/ctxutil.go +++ b/pkg/ctxutil/ctxutil.go @@ -3,7 +3,7 @@ package ctxutil import ( "context" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) type contextKey int @@ -27,13 +27,14 @@ const ( ctxKeyQuickSelect ctxKeyCategory ctxKeyPhoneNumber + ctxEncryption ctxNoClip + ctxPrint ) // WithGlobalFlags parses any global flags from the cli context and returns // a regular context -func WithGlobalFlags(c *cli.Context) context.Context { - ctx := c.Context +func WithGlobalFlags(ctx context.Context, c *cli.Command) context.Context { if c.Bool("yes") { ctx = WithAlwaysYes(ctx, true) } @@ -369,3 +370,15 @@ func IsNoClip(ctx context.Context) bool { return bv } +func WithEncryption(ctx context.Context, mode int) context.Context { + return context.WithValue(ctx, ctxEncryption, mode) +} + +// GetPhoneNumber returns the phone number from the context +func GetEncryption(ctx context.Context) int { + en, ok := ctx.Value(ctxEncryption).(int) + if !ok { + return -1 + } + return en +} diff --git a/pkg/migration/migration.go b/pkg/migration/migration.go deleted file mode 100644 index c7c446a..0000000 --- a/pkg/migration/migration.go +++ /dev/null @@ -1,80 +0,0 @@ -package migration - -import ( - "context" - "passline/pkg/config" - "passline/pkg/crypt" - "passline/pkg/storage" -) - -const ( - oldPassword = "" - newPassword = "" -) - -// 0.7.3 -> 1.0.0 -func MigrateV1() error { - cfg, err := config.Get() - if err != nil { - return err - } - - store, err := storage.New(cfg) - if err != nil { - return err - } - - items, err := store.GetAllItems(context.TODO()) - if err != nil { - return err - } - - // get pwKey - pwKey := crypt.GetKey([]byte(oldPassword)) - - // decrypt old data - for i := 0; i < len(items); i++ { - for x := 0; x < len(items[i].Credentials); x++ { - plainPW, err := crypt.AesGcmDecrypt([]byte(pwKey), items[i].Credentials[x].Password) - items[i].Credentials[x].Password = plainPW - if err != nil { - return err - } - } - } - - // encrypt with new logic - encryptionKey, err := crypt.GenerateKey() - if err != nil { - return err - } - - for i := 0; i < len(items); i++ { - for x := 0; x < len(items[i].Credentials); x++ { - pw, err := crypt.AesGcmEncrypt([]byte(encryptionKey), items[i].Credentials[x].Password) - items[i].Credentials[x].Password = pw - if err != nil { - return err - } - } - } - - // encrypt encryption key - pwKey = crypt.GetKey([]byte(newPassword)) - encryptedEncryptionKey, err := crypt.AesGcmEncrypt(pwKey, encryptionKey) - if err != nil { - return err - } - - data := storage.Data{ - Key: encryptedEncryptionKey, - Items: items, - } - - err = store.SetData(context.TODO(), data) - if err != nil { - return err - } - - return nil -} diff --git a/pkg/out/out.go b/pkg/out/out.go index 61746a4..5413fad 100644 --- a/pkg/out/out.go +++ b/pkg/out/out.go @@ -56,6 +56,11 @@ func SuccessfulCopiedToClipboard(name, username string) { fmt.Fprintf(color.Output, "Copied Password for %s to clipboard\n", identifier) } +func FailedCopyToClipboard() { + d := color.New(color.FgRed) + d.Printf("Failed to copy to clipboard\n") +} + func SuccessfulChangedItem(name, username string) { identifier := color.YellowString(BuildIdentifier(name, username)) d := color.New(color.FgGreen) @@ -153,6 +158,11 @@ func DisplayMessage() { d.Printf("Display item...\n") } +func SyncMessage() { + d := color.New(color.FgGreen) + d.Printf("Sync configuration...\n") +} + func BackupMessage() { d := color.New(color.FgGreen) d.Printf("Creating backup...\n") diff --git a/pkg/storage/crypt.go b/pkg/storage/crypt.go new file mode 100644 index 0000000..4d26ad2 --- /dev/null +++ b/pkg/storage/crypt.go @@ -0,0 +1,81 @@ +package storage + +import "passline/pkg/crypt" + +func DecryptCredential(credential *Credential, globalPassword []byte) error { + err := decryptPassword(credential, globalPassword) + if err != nil { + return err + } + + err = decryptRecoveryCodes(credential, globalPassword) + if err != nil { + return err + } + + return nil +} + +func EncryptCredential(credential *Credential, key []byte) error { + err := encryptPassword(credential, key) + if err != nil { + return err + } + + err = encryptRecoveryCodes(credential, key) + if err != nil { + return err + } + + return nil +} + +func encryptPassword(credential *Credential, key []byte) error { + var err error + credential.Password, err = crypt.AesGcmEncrypt(key, credential.Password) + if err != nil { + return err + } + + return nil +} + +func encryptRecoveryCodes(credential *Credential, globalPassword []byte) error { + var encryptedRecoveryCodes = make([]string, 0) + + for _, c := range credential.RecoveryCodes { + encryptedRecoveryCode, err := crypt.AesGcmEncrypt(globalPassword, c) + if err != nil { + return err + } + encryptedRecoveryCodes = append(encryptedRecoveryCodes, encryptedRecoveryCode) + } + + credential.RecoveryCodes = encryptedRecoveryCodes + return nil +} + +func decryptPassword(credential *Credential, globalPassword []byte) error { + // Decrypt passwords + var err error + credential.Password, err = crypt.AesGcmDecrypt(globalPassword, credential.Password) + if err != nil { + return err + } + + return nil +} + +func decryptRecoveryCodes(credential *Credential, globalPassword []byte) error { + var decryptedRecoveryCodes = make([]string, 0) + for _, c := range credential.RecoveryCodes { + decryptedRecoveryCode, err := crypt.AesGcmDecrypt(globalPassword, c) + if err != nil { + return err + } + decryptedRecoveryCodes = append(decryptedRecoveryCodes, decryptedRecoveryCode) + } + + credential.RecoveryCodes = decryptedRecoveryCodes + return nil +} diff --git a/pkg/storage/crypt_test.go b/pkg/storage/crypt_test.go new file mode 100644 index 0000000..b14c952 --- /dev/null +++ b/pkg/storage/crypt_test.go @@ -0,0 +1,31 @@ +package storage + +import "testing" + +const ( + key = "01234567890123456789012345678912" +) + +func TestEncryptCredentials(t *testing.T) { + credential := Credential{ + Username: "perry", + Password: "123456789", + RecoveryCodes: []string{"test", "tee"}, + } + err := EncryptCredential(&credential, []byte(key)) + if err != nil || credential.Password == "123456789" { + t.Errorf("EncryptCredential() Password = %s; wanted != %s", credential.Password, "123456789") + } +} + +func TestDecryptCredentials(t *testing.T) { + credential := Credential{ + Username: "perry", + Password: "3IbVQJqqSavvXiRdjffXWh3Z2d-4oxp_0zJ_VIDEcZmJ8aT_5g==", //123456789 + RecoveryCodes: []string{}, + } + err := DecryptCredential(&credential, []byte(key)) + if err != nil || credential.Password != "123456789" { + t.Errorf("EncryptCredential() Password = %s; wanted %s", credential.Password, "123456789") + } +} diff --git a/pkg/storage/firestore.go b/pkg/storage/firestore.go index 8aab21c..80fdcd3 100644 --- a/pkg/storage/firestore.go +++ b/pkg/storage/firestore.go @@ -2,10 +2,15 @@ package storage import ( "context" + "encoding/json" "errors" + "fmt" "log" "os" + "passline/pkg/cli/input" "passline/pkg/config" + "passline/pkg/crypt" + "passline/pkg/ctxutil" "path" "sort" @@ -18,8 +23,9 @@ import ( ) type FireStore struct { - client *firestore.Client - items []Item + client *firestore.Client + items []Item + decryptedKey []byte } const ( @@ -58,12 +64,17 @@ func NewFirestore() (*FireStore, error) { } func (fs *FireStore) GetItemByName(ctx context.Context, name string) (Item, error) { - dsnap, err := fs.client.Collection(DataCollection).Doc(name).Get(context.Background()) + items, err := fs.GetAllItems(ctx) if err != nil { return Item{}, err } + var item Item - dsnap.DataTo(&item) + for i := 0; i < len(items); i++ { + if items[i].Name == name { + item = items[i] + } + } // Add default Category if not exists for index, cred := range item.Credentials { @@ -157,32 +168,62 @@ func (fs *FireStore) DeleteCredential(ctx context.Context, item Item, username s return nil } -func (fs *FireStore) UpdateItem(ctx context.Context, item Item) error { - err := fs.deleteItem(ctx, item) +func (fs *FireStore) GetDecryptedKey(ctx context.Context, reason string) ([]byte, error) { + if fs.decryptedKey != nil { + return fs.decryptedKey, nil + } + + // Get encrypted content encryption key from store + encryptedEncryptionKey, err := fs.GetKey(ctx) if err != nil { - return err + return []byte{}, err } - err = fs.createItem(ctx, item) + if encryptedEncryptionKey != "" { + encryptionKey, err := input.MasterPassword(encryptedEncryptionKey, reason) + if err != nil { + return []byte{}, err + } + + fs.decryptedKey = encryptionKey + + return encryptionKey, nil + } + + decryptedEncryptionKey, err := crypt.GenerateKey() if err != nil { - return err + return []byte{}, err } - return nil + password := input.Password("Enter master password: ") + fmt.Println() + passwordTwo := input.Password("Enter master password again: ") + fmt.Println() + + if string(password) != string(passwordTwo) { + return []byte{}, err + } + + encryptedEncryptionKey, err = crypt.EncryptKey(password, decryptedEncryptionKey) + if err != nil { + return []byte{}, err + } + fs.SetKey(ctx, encryptedEncryptionKey) + + fs.decryptedKey = []byte(decryptedEncryptionKey) + + return fs.decryptedKey, nil } -func (fs *FireStore) SetData(ctx context.Context, data Data) error { +func (fs *FireStore) SetItems(ctx context.Context, items []Item, key []byte) error { fs.deleteCollection(ctx, 100) batch := fs.client.Batch() - for _, item := range data.Items { + for _, item := range items { itemRef := fs.client.Collection(DataCollection).Doc(item.Name) batch.Set(itemRef, item) } - itemRef := fs.client.Collection(ConfigCollection).Doc("config") - batch.Set(itemRef, Config{Key: data.Key}) - _, err := batch.Commit(ctx) if err != nil { return err @@ -212,12 +253,64 @@ func (fs *FireStore) SetKey(ctx context.Context, key string) error { return nil } +func (fs *FireStore) GetRawItems(ctx context.Context) (json.RawMessage, error) { + iter := fs.client.Collection(DataCollection).Documents(ctx) + for { + doc, err := iter.Next() + if err == iterator.Done { + break + } + if err != nil { + return nil, err + } + + var item Item + doc.DataTo(&item) + + // Add default Category if not exists + for index, cred := range item.Credentials { + if cred.Category == "" { + item.Credentials[index].Category = DefaultCategory + } + } + + fs.items = append(fs.items, item) + } + + return nil, nil +} + func (fs *FireStore) createItem(ctx context.Context, item Item) error { - _, err := fs.client.Collection(DataCollection).Doc(item.Name).Set(ctx, item) - if err != nil { - log.Fatalf("Failed adding item: %v", err) + encryption := ctxutil.GetEncryption(ctx) + if encryption == config.FullEncryption { + items, err := fs.GetAllItems(ctx) + items = append(items, item) + + file, err := json.Marshal(items) + if err != nil { + return fmt.Errorf("failed to marshal items: %w", err) + } + + key, err := fs.GetDecryptedKey(ctx, "to decrypt the password") + if err != nil { + return err + } + + encryptedResult, err := crypt.AesGcmEncrypt(key, string(file)) + if err != nil { + return fmt.Errorf("encryption failed: %w", err) + } + + fs.client.Collection("vault").Doc("items").Set(ctx, encryptedResult) + } else { + _, err := fs.client.Collection(DataCollection).Doc(item.Name).Set(ctx, item) + if err != nil { + log.Fatalf("Failed adding item: %v", err) + } } + fs.items = []Item{} + return nil } @@ -228,6 +321,8 @@ func (fs *FireStore) deleteItem(ctx context.Context, item Item) error { return err } + fs.items = []Item{} + return nil } diff --git a/pkg/storage/localstorage.go b/pkg/storage/localstorage.go index 2727878..1863ebb 100644 --- a/pkg/storage/localstorage.go +++ b/pkg/storage/localstorage.go @@ -4,34 +4,55 @@ import ( "context" "encoding/json" "errors" + "fmt" "os" "path" "sort" + "passline/pkg/cli/input" "passline/pkg/config" + "passline/pkg/crypt" + "passline/pkg/ctxutil" + "passline/pkg/util" ) type LocalStorage struct { - storageDir string - storageFile string + fs util.FileSystem + storageDir string + storageFile string + keyFile string + items []Item + decryptedKey []byte } -func NewLocalStorage() (*LocalStorage, error) { +func NewLocalStorage(fs util.FileSystem) (*LocalStorage, error) { mainDir := config.Directory() storageDir := path.Join(mainDir, "storage") - storageFile := path.Join(storageDir, "storage.json") + storageFile := path.Join(storageDir, "storage") + keyFile := path.Join(storageDir, "key") - ensureDirectories(storageDir, storageFile) - return &LocalStorage{storageDir: storageDir, storageFile: storageFile}, nil + ensureDirectories(fs, storageDir) + + ls := LocalStorage{ + fs: fs, + storageDir: storageDir, + storageFile: storageFile, + keyFile: keyFile, + } + + return &ls, nil } -// Get item by name func (ls *LocalStorage) GetItemByName(ctx context.Context, name string) (Item, error) { - data := ls.getData() - for i := 0; i < len(data.Items); i++ { - if data.Items[i].Name == name { - return data.Items[i], nil + items, err := ls.getItems(ctx) + if err != nil { + return Item{}, err + } + + for i := 0; i < len(items); i++ { + if items[i].Name == name { + return items[i], nil } } @@ -39,18 +60,26 @@ func (ls *LocalStorage) GetItemByName(ctx context.Context, name string) (Item, e } func (ls *LocalStorage) GetItemByIndex(ctx context.Context, index int) (Item, error) { - data := ls.getData() - if index < 0 && index > len(data.Items) { + items, err := ls.getItems(ctx) + if err != nil { + return Item{}, err + } + + if index < 0 && index > len(items) { return Item{}, errors.New("Out of index") } - return data.Items[index], nil + return items[index], nil } func (ls *LocalStorage) GetAllItems(ctx context.Context) ([]Item, error) { - data := ls.getData() - sort.Sort(ByName(data.Items)) - return data.Items, nil + items, err := ls.getItems(ctx) + if err != nil { + return nil, err + } + + sort.Sort(ByName(items)) + return items, nil } func (ls *LocalStorage) AddCredential(ctx context.Context, name string, credential Credential) error { @@ -64,89 +93,139 @@ func (ls *LocalStorage) AddCredential(ctx context.Context, name string, credenti } // if item exists just append - data := ls.getData() - for i := 0; i < len(data.Items); i++ { - if data.Items[i].Name == name { - for y := 0; y < len(data.Items[i].Credentials); y++ { - if data.Items[i].Credentials[y].Username == credential.Username { + items, err := ls.getItems(ctx) + if err != nil { + return err + } + + for i := 0; i < len(items); i++ { + if items[i].Name == name { + for y := 0; y < len(items[i].Credentials); y++ { + if items[i].Credentials[y].Username == credential.Username { return errors.New("Username already exists") } } - data.Items[i].Credentials = append(data.Items[i].Credentials, credential) + items[i].Credentials = append(items[i].Credentials, credential) break } } - ls.setData(data) + ls.SetItems(ctx, items, nil) return nil } func (ls *LocalStorage) DeleteCredential(ctx context.Context, item Item, username string) error { - data := ls.getData() - indexItem := getIndexOfItem(data.Items, item.Name) + items, err := ls.getItems(ctx) + if err != nil { + return err + } + + indexItem := getIndexOfItem(items, item.Name) if indexItem == -1 { return errors.New("Item not found") } - if len(data.Items[indexItem].Credentials) > 1 { - indexCredential := getIndexOfCredential(data.Items[indexItem].Credentials, username) + if len(items[indexItem].Credentials) > 1 { + indexCredential := getIndexOfCredential(items[indexItem].Credentials, username) if indexCredential == -1 { return errors.New("Item not found") } - data.Items[indexItem].Credentials = removeFromCredentials(data.Items[indexItem].Credentials, indexCredential) - ls.setData(data) + items[indexItem].Credentials = removeFromCredentials(items[indexItem].Credentials, indexCredential) + ls.SetItems(ctx, items, nil) } else { - ls.deleteItem(data.Items[indexItem]) + ls.deleteItem(ctx, items[indexItem]) } return nil } -func (ls *LocalStorage) UpdateItem(ctx context.Context, item Item) error { - // TODO check if username is valid - - ls.deleteItem(item) - ls.createItem(ctx, item) +func (ls *LocalStorage) GetKey(ctx context.Context) (string, error) { + key, err := ls.fs.ReadFile(ls.keyFile) + if ls.fs.IsNotExist(err) { + return "", nil + } else if err != nil { + return "", err + } - return nil + return string(key), nil } -func (ls *LocalStorage) SetData(ctx context.Context, data Data) error { - ls.setData(data) - return nil -} +func (ls *LocalStorage) GetDecryptedKey(ctx context.Context, reason string) ([]byte, error) { + if ls.decryptedKey != nil { + return ls.decryptedKey, nil + } -func (ls *LocalStorage) GetKey(ctx context.Context) (string, error) { - data := ls.getData() - return data.Key, nil + // Get encrypted content encryption key from store + encryptedEncryptionKey, err := ls.GetKey(ctx) + if err != nil { + return []byte{}, err + } + + if encryptedEncryptionKey != "" { + encryptionKey, err := input.MasterPassword(encryptedEncryptionKey, reason) + if err != nil { + return []byte{}, err + } + + ls.decryptedKey = encryptionKey + + return encryptionKey, nil + } + + decryptedEncryptionKey, err := crypt.GenerateKey() + if err != nil { + return []byte{}, err + } + + password := input.Password("Enter master password: ") + fmt.Println() + passwordTwo := input.Password("Enter master password again: ") + fmt.Println() + + if string(password) != string(passwordTwo) { + return []byte{}, err + } + + encryptedEncryptionKey, err = crypt.EncryptKey(password, decryptedEncryptionKey) + if err != nil { + return []byte{}, err + } + ls.SetKey(ctx, encryptedEncryptionKey) + + ls.decryptedKey = []byte(decryptedEncryptionKey) + + return ls.decryptedKey, nil } func (ls *LocalStorage) SetKey(ctx context.Context, key string) error { - data := ls.getData() - data.Key = key - ls.setData(data) + err := ls.fs.WriteFile(ls.keyFile, []byte(key), 0644) + if err != nil { + return err + } + return nil } -func (ls *LocalStorage) createItem(ctx context.Context, item Item) { - data := ls.getData() - data.Items = append(data.Items, item) - ls.setData(data) -} +func (ls *LocalStorage) GetRawItems(ctx context.Context) (json.RawMessage, error) { + file, err := ls.fs.ReadFile(ls.storageFile) + if err != nil { + return nil, err + } + + var js json.RawMessage + if json.Unmarshal(file, &js) != nil { + return json.RawMessage(fmt.Sprintf("\"%s\"", file)), nil + } -func (ls *LocalStorage) deleteItem(item Item) { - data := ls.getData() - index := getIndexOfItem(data.Items, item.Name) - data.Items = removeFromItems(data.Items, index) - ls.setData(data) + return json.RawMessage(file), nil } -func ensureDirectories(storageDir, storageFile string) { - ensureStorageDir(storageDir) +func ensureDirectories(fs util.FileSystem, storageDir string) { + ensureStorageDir(fs, storageDir) } -func ensureStorageDir(storageDir string) { - _, err := os.Stat(storageDir) +func ensureStorageDir(fs util.FileSystem, storageDir string) { + _, err := fs.Stat(storageDir) if err != nil { err := os.Mkdir(storageDir, os.ModePerm) if err != nil { @@ -155,22 +234,101 @@ func ensureStorageDir(storageDir string) { } } -func (ls LocalStorage) getData() Data { - data := Data{} +func (ls *LocalStorage) createItem(ctx context.Context, item Item) error { + items, err := ls.getItems(ctx) + if err != nil { + return err + } - _, err := os.Stat(ls.storageFile) - if err == nil { - file, _ := os.ReadFile(ls.storageFile) - _ = json.Unmarshal([]byte(file), &data) + items = append(items, item) + ls.SetItems(ctx, items, nil) + + return nil +} + +func (ls *LocalStorage) deleteItem(ctx context.Context, item Item) error { + items, err := ls.getItems(ctx) + if err != nil { + return err } - return data + index := getIndexOfItem(items, item.Name) + items = removeFromItems(items, index) + ls.SetItems(ctx, items, nil) + + return nil } -func (ls LocalStorage) setData(data Data) { - _, err := os.Stat(ls.storageDir) - if err == nil { - file, _ := json.MarshalIndent(data, "", " ") - _ = os.WriteFile(ls.storageFile, file, 0644) +func (ls *LocalStorage) SetItems(ctx context.Context, items []Item, decryptedKey []byte) error { + file, err := json.Marshal(items) + if err != nil { + return fmt.Errorf("failed to marshal items: %w", err) + } + + encryption := ctxutil.GetEncryption(ctx) + if encryption == config.FullEncryption { + var key = decryptedKey + if decryptedKey == nil { + key, err = ls.GetDecryptedKey(ctx, "encrypt the password") + if err != nil { + return err + } + } + + encryptedResult, err := crypt.AesGcmEncrypt(key, string(file)) + if err != nil { + return fmt.Errorf("encryption failed: %w", err) + } + + file = []byte(fmt.Sprintf("%s", encryptedResult)) + } + + err = ls.fs.WriteFile(ls.storageFile, file, 0644) + if err != nil { + return fmt.Errorf("failed to write to file: %w", err) } + + ls.items = items + + return nil +} + +func (ls *LocalStorage) getItems(ctx context.Context) ([]Item, error) { + if len(ls.items) > 0 { + return ls.items, nil + } + + _, err := os.Stat(ls.storageFile) + if err != nil { + return []Item{}, nil + } + + file, _ := ls.fs.ReadFile(ls.storageFile) + rawItems := json.RawMessage(file) + + // Check if the file is full encrypted by checking if it is a valid json + var js json.RawMessage + if json.Unmarshal(file, &js) != nil { + decryptedKey, err := ls.GetDecryptedKey(ctx, "to decrypt your vault") + if err != nil { + return nil, err + } + + decryptedItems, err := crypt.AesGcmDecrypt(decryptedKey, string(file)) + if err != nil { + return nil, err + } + + rawItems = json.RawMessage(decryptedItems) + } + + items := []Item{} + err = json.Unmarshal(rawItems, &items) + if err != nil { + return nil, err + } + + ls.items = items + + return ls.items, nil } diff --git a/pkg/storage/localstorage_test.go b/pkg/storage/localstorage_test.go index 2390093..df70f42 100644 --- a/pkg/storage/localstorage_test.go +++ b/pkg/storage/localstorage_test.go @@ -2,34 +2,184 @@ package storage import ( "context" + "encoding/json" + "os" "testing" "passline/pkg/config" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) +type MockFileSystem struct { + mock.Mock +} + +func (m *MockFileSystem) Stat(name string) (os.FileInfo, error) { + args := m.Called(name) + // Return os.FileInfo (can be nil) and error + fileInfo, _ := args.Get(0).(os.FileInfo) + return fileInfo, args.Error(1) +} + +func (m *MockFileSystem) ReadFile(name string) ([]byte, error) { + args := m.Called(name) + data, _ := args.Get(0).([]byte) + return data, args.Error(1) +} + +func (m *MockFileSystem) WriteFile(name string, data []byte, perm os.FileMode) error { + args := m.Called(name, data, perm) + return args.Error(0) +} + +func (m *MockFileSystem) IsNotExist(err error) bool { + args := m.Called(err) + return args.Bool(0) +} + +func TestGetAllItems(t *testing.T) { + mockFS := new(MockFileSystem) + + mockFS.On("Stat", "/root/.passline/storage").Return(nil, nil) + + mockedItems := []Item{{Name: "test", Credentials: []Credential{{Username: "tpoe", Password: "test"}}}} + mockedData, _ := json.Marshal(mockedItems) + mockFS.On("ReadFile", "/root/.passline/storage/storage").Return(mockedData, nil) + + s, err := NewLocalStorage(mockFS) + if err != nil { + t.Errorf("Unable to initialize storage") + } + + items, err := s.GetAllItems(context.Background()) + + assert.NoError(t, err) + assert.NotNil(t, items) + assert.Equal(t, 1, len(items)) +} + func TestAddItem(t *testing.T) { - cfg, err := config.Get() + rootDir := config.Directory() + + mockFS := new(MockFileSystem) + + mockFS.On("Stat", rootDir+"/storage").Return(nil, os.ErrNotExist) + + mockFS.On("ReadFile", rootDir+"/config.json").Return(nil, nil) + + mockedItems := []Item{{Name: "test", Credentials: []Credential{{Username: "tpoe", Password: "test"}}}} + mockedData, _ := json.Marshal(mockedItems) + mockFS.On("ReadFile", rootDir+"/storage/storage").Return(mockedData, nil) + + mockFS.On("WriteFile", rootDir+"/storage/storage", mock.Anything, mock.Anything).Return(nil) + + cfg, err := config.Get(mockFS) if err != nil { t.Errorf("Unable to initialize config") } ctx := cfg.WithContext(context.Background()) - s, err := NewLocalStorage() + s, err := NewLocalStorage(mockFS) if err != nil { t.Errorf("Unable to initialize storage") } credential := Credential{ - Username: "tpoe", + Username: "tpoe2", Password: "1234", } s.AddCredential(ctx, "test", credential) - i, _ := s.GetItemByName(ctx, "test") - c, _ := i.GetCredentialByUsername("tpoe") + mockFS.AssertCalled(t, "WriteFile", rootDir+"/storage/storage", mock.Anything, mock.Anything) +} + +func TestDeleteWholeItem(t *testing.T) { + rootDir := config.Directory() + + mockFS := new(MockFileSystem) + + mockFS.On("Stat", rootDir+"/storage").Return(nil, os.ErrNotExist) - if c.Password != credential.Password || c.Username != c.Username { - t.Errorf("Credential was not added correctly") + mockFS.On("ReadFile", rootDir+"/config.json").Return(nil, nil) + + mockedItems := []Item{{Name: "test", Credentials: []Credential{{Username: "tpoe", Password: "test"}}}} + mockedData, _ := json.Marshal(mockedItems) + mockFS.On("ReadFile", rootDir+"/storage/storage").Return(mockedData, nil) + + mockFS.On("WriteFile", rootDir+"/storage/storage", mock.Anything, mock.Anything).Return(nil) + + cfg, err := config.Get(mockFS) + if err != nil { + t.Errorf("Unable to initialize config") + } + ctx := cfg.WithContext(context.Background()) + + s, err := NewLocalStorage(mockFS) + if err != nil { + t.Errorf("Unable to initialize storage") + } + + s.DeleteCredential(ctx, Item{Name: "test"}, "tpoe") + + expectedItems := []Item{} + expectedData, _ := json.Marshal(expectedItems) + mockFS.AssertCalled(t, "WriteFile", rootDir+"/storage/storage", expectedData, mock.Anything) +} + +func TestDeleteOneCredential(t *testing.T) { + rootDir := config.Directory() + + mockFS := new(MockFileSystem) + + mockFS.On("Stat", rootDir+"/storage").Return(nil, os.ErrNotExist) + + mockFS.On("ReadFile", rootDir+"/config.json").Return(nil, nil) + + mockedItems := []Item{{ + Name: "test", + Credentials: []Credential{{ + Username: "tpoe1", + Password: "password1", + }, { + Username: "tpoe2", + Password: "password2", + }}, + }} + mockedData, _ := json.Marshal(mockedItems) + mockFS.On("ReadFile", rootDir+"/storage/storage").Return(mockedData, nil) + + mockFS.On("WriteFile", rootDir+"/storage/storage", mock.Anything, mock.Anything).Return(nil) + + cfg, err := config.Get(mockFS) + if err != nil { + t.Errorf("Unable to initialize config") } + ctx := cfg.WithContext(context.Background()) + + s, err := NewLocalStorage(mockFS) + if err != nil { + t.Errorf("Unable to initialize storage") + } + + s.DeleteCredential(ctx, Item{Name: "test"}, "tpoe2") + + expectedItems := []Item{{ + Name: "test", + Credentials: []Credential{{ + Category: "default", + Username: "tpoe1", + Password: "password1", + }}, + }} + expectedData, _ := json.Marshal(expectedItems) + mockFS.AssertCalled(t, + "WriteFile", + rootDir+"/storage/storage", + mock.MatchedBy(func(data []byte) bool { + return assert.ObjectsAreEqual(expectedData, data) + }), + mock.Anything) } diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 0424bd0..f3b835d 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -8,6 +8,7 @@ import ( "passline/pkg/cli/selection" "passline/pkg/config" + "passline/pkg/util" "golang.org/x/net/context" ) @@ -19,10 +20,11 @@ type Storage interface { GetAllItems(context.Context) ([]Item, error) AddCredential(context.Context, string, Credential) error DeleteCredential(context.Context, Item, string) error - UpdateItem(context.Context, Item) error - SetData(context.Context, Data) error GetKey(context.Context) (string, error) SetKey(context.Context, string) error + GetDecryptedKey(context.Context, string) ([]byte, error) + SetItems(context.Context, []Item, []byte) error + GetRawItems(context.Context) (json.RawMessage, error) } type Config struct { @@ -120,7 +122,7 @@ func New(cfg *config.Config) (Storage, error) { return nil, err } default: - store, err = NewLocalStorage() + store, err = NewLocalStorage(util.OSFileSystem{}) if err != nil { return nil, err } diff --git a/pkg/util/filesystem.go b/pkg/util/filesystem.go new file mode 100644 index 0000000..217a7ad --- /dev/null +++ b/pkg/util/filesystem.go @@ -0,0 +1,26 @@ +package util + +import "os" + +type FileSystem interface { + Stat(name string) (os.FileInfo, error) + ReadFile(name string) ([]byte, error) + WriteFile(name string, data []byte, perm os.FileMode) error + IsNotExist(error error) bool +} + +type OSFileSystem struct{} + +func (OSFileSystem) Stat(name string) (os.FileInfo, error) { + return os.Stat(name) +} + +func (OSFileSystem) ReadFile(name string) ([]byte, error) { + return os.ReadFile(name) +} +func (OSFileSystem) WriteFile(name string, data []byte, perm os.FileMode) error { + return os.WriteFile(name, data, perm) +} +func (OSFileSystem) IsNotExist(err error) bool { + return os.IsNotExist(err) +} diff --git a/pkg/util/util.go b/pkg/util/util.go index 61b7cbe..0d01db0 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -1,6 +1,8 @@ package util -import "strings" +import ( + "strings" +) func ArrayContains(l []string, i string) bool { for _, li := range l { @@ -29,3 +31,49 @@ func ArrayToString(l []string) string { func StringToArray(s string) []string { return strings.Split(s, ",") } + +func LevenshteinDistanceSubstring(target, pattern string) (string, int) { + minDistance := len(pattern) + bestMatch := "" + patternLength := len(pattern) + + for i := 0; i <= len(target)-patternLength; i++ { + substring := target[i : i+patternLength] + distance := LevenshteinDistance(substring, pattern) + + if distance < minDistance { + minDistance = distance + bestMatch = substring + } + } + + return bestMatch, minDistance +} + +func LevenshteinDistance(a string, b string) int { + rows := len(b) + 1 + cols := len(a) + 1 + + matrix := make([][]int, rows) + + for i := range matrix { + matrix[i] = make([]int, cols) + matrix[i][0] = i + } + + for i := range matrix[0] { + matrix[0][i] = i + } + + for y := 1; y < len(matrix); y++ { + for x := 1; x < len(matrix[y]); x++ { + if a[x-1] == b[y-1] { + matrix[y][x] = matrix[y-1][x-1] + } else { + matrix[y][x] = min(matrix[y][x-1], matrix[y-1][x-1], matrix[y-1][x]) + 1 + } + } + } + + return matrix[rows-1][cols-1] +} diff --git a/pkg/util/utils_test.go b/pkg/util/utils_test.go new file mode 100644 index 0000000..ef950a9 --- /dev/null +++ b/pkg/util/utils_test.go @@ -0,0 +1,1038 @@ +package util + +import ( + "fmt" + "testing" + "time" +) + +func TestLevenshteinDistanceSameLength(t *testing.T) { + distance := LevenshteinDistance("maus", "haus") + + if distance != 1 { + t.Errorf("LevenshteinDistance() = %d; wanted length %v", distance, 3) + } +} + +func TestLevenshteinDistanceDifferenceLength(t *testing.T) { + distance := LevenshteinDistance("flake", "brakes") + + if distance != 3 { + t.Errorf("LevenshteinDistance() = %d; wanted length %v", distance, 3) + } +} + +func TestFindClosestSubstringMass(t *testing.T) { + items := []string{ + "google.com", + "youtube.com", + "facebook.com", + "baidu.com", + "yahoo.com", + "amazon.com", + "wikipedia.org", + "google.co.in", + "twitter.com", + "qq.com", + "live.com", + "taobao.com", + "bing.com", + "google.co.jp", + "msn.com", + "yahoo.co.jp", + "linkedin.com", + "sina.com.cn", + "instagram.com", + "weibo.com", + "vk.com", + "yandex.ru", + "google.de", + "google.ru", + "hao123.com", + "ebay.com", + "reddit.com", + "google.co.uk", + "google.com.br", + "mail.ru", + "t.co", + "pinterest.com", + "amazon.co.jp", + "google.fr", + "netflix.com", + "gmw.cn", + "tmall.com", + "360.cn", + "google.it", + "microsoft.com", + "onclickads.net", + "google.es", + "paypal.com", + "sohu.com", + "wordpress.com", + "tumblr.com", + "blogspot.com", + "imgur.com", + "xvideos.com", + "google.com.mx", + "naver.com", + "stackoverflow.com", + "apple.com", + "chinadaily.com.cn", + "fc2.com", + "aliexpress.com", + "imdb.com", + "google.ca", + "google.co.kr", + "github.com", + "ok.ru", + "google.com.hk", + "whatsapp.com", + "diply.com", + "jd.com", + "amazon.de", + "google.com.tr", + "rakuten.co.jp", + "craigslist.org", + "office.com", + "google.co.id", + "kat.cr", + "amazon.in", + "tianya.cn", + "blogger.com", + "google.pl", + "nicovideo.jp", + "alibaba.com", + "soso.com", + "pixnet.net", + "google.com.au", + "go.com", + "amazon.co.uk", + "xhamster.com", + "dropbox.com", + "google.com.tw", + "outbrain.com", + "xinhuanet.com", + "cntv.cn", + "googleusercontent.com", + "cnn.com", + "ask.com", + "coccoc.com", + "booking.com", + "bbc.co.uk", + "popads.net", + "youth.cn", + "twitch.tv", + "wikia.com", + "microsoftonline.com", + "quora.com", + "chase.com", + "adobe.com", + "163.com", + "360.com", + "haosou.com", + "google.com.pk", + "google.co.th", + "google.com.eg", + "google.com.ar", + "youku.com", + "google.com.sa", + "bbc.com", + "flipkart.com", + "alipay.com", + "bongacams.com", + "adf.ly", + "nytimes.com", + "google.nl", + "sogou.com", + "livedoor.jp", + "daum.net", + "txxx.com", + "amazon.cn", + "espn.go.com", + "ebay.co.uk", + "ettoday.net", + "bankofamerica.com", + "china.com", + "indiatimes.com", + "myway.com", + "bilibili.com", + "walmart.com", + "ebay.de", + "china.com.cn", + "godaddy.com", + "dailymail.co.uk", + "buzzfeed.com", + "zillow.com", + "xnxx.com", + "salesforce.com", + "dailymotion.com", + "wellsfargo.com", + "detail.tmall.com", + "steampowered.com", + "steamcommunity.com", + "nametests.com", + "google.co.ve", + "theguardian.com", + "google.com.ua", + "indeed.com", + "ameblo.jp", + "aol.com", + "etsy.com", + "globo.com", + "google.co.za", + "yelp.com", + "amazonaws.com", + "huffingtonpost.com", + "tudou.com", + "so.com", + "zhihu.com", + "soundcloud.com", + "tripadvisor.com", + "google.gr", + "varzesh3.com", + "avito.ru", + "onlinesbi.com", + "vice.com", + "cnzz.com", + "directrev.com", + "uol.com.br", + "bet365.com", + "weather.com", + "mediafire.com", + "uptodown.com", + "cnet.com", + "washingtonpost.com", + "gfycat.com", + "goo.ne.jp", + "stackexchange.com", + "force.com", + "taboola.com", + "google.com.co", + "dmm.co.jp", + "tuberel.com", + "vimeo.com", + "google.com.ng", + "naver.jp", + "feedly.com", + "theladbible.com", + "pixiv.net", + "redtube.com", + "detik.com", + "homedepot.com", + "torrentz.eu", + "slideshare.net", + "google.ro", + "taringa.net", + "foxnews.com", + "target.com", + "amazon.it", + "google.com.pe", + "flickr.com", + "hclips.com", + "google.be", + "amazon.fr", + "9gag.com", + "kakaku.com", + "blogspot.in", + "ikea.com", + "mega.nz", + "ifeng.com", + "udn.com", + "web.de", + "americanexpress.com", + "iqiyi.com", + "bp.blogspot.com", + "fbcdn.net", + "google.com.ph", + "orange.fr", + "comcast.net", + "google.com.sg", + "terraclicks.com", + "youm7.com", + "putlocker.is", + "tribunnews.com", + "gmx.net", + "deviantart.com", + "nih.gov", + "zol.com.cn", + "ontests.me", + "roblox.com", + "doubleclick.net", + "hdfcbank.com", + "ozock.com", + "tistory.com", + "capitalone.com", + "leboncoin.fr", + "douyu.com", + "google.cn", + "51.la", + "google.se", + "spotify.com", + "wikihow.com", + "onet.pl", + "babytree.com", + "w3schools.com", + "snapdeal.com", + "forbes.com", + "google.at", + "wix.com", + "bestbuy.com", + "livejournal.com", + "mozilla.org", + "rdsa2013.com", + "xfinity.com", + "handycafe.com", + "groupon.com", + "adnetworkperformance.com", + "onedio.com", + "thepiratebay.org", + "skype.com", + "github.io", + "allegro.pl", + "google.dz", + "google.com.vn", + "paytm.com", + "twimg.com", + "wikimedia.org", + "icicibank.com", + "t-online.de", + "tokopedia.com", + "popcash.net", + "telegraph.co.uk", + "usps.com", + "slither.io", + "wp.pl", + "blog.jp", + "google.ch", + "webtretho.com", + "irctc.co.in", + "trello.com", + "google.pt", + "yesky.com", + "xywy.com", + "huanqiu.com", + "eksisozluk.com", + "blastingnews.com", + "citi.com", + "shutterstock.com", + "rediff.com", + "files.wordpress.com", + "ups.com", + "1688.com", + "google.cl", + "bitauto.com", + "speedtest.net", + "pandora.com", + "adexc.net", + "imzog.com", + "google.ae", + "2ch.net", + "google.cz", + "loading-delivery2.com", + "seznam.cz", + "ltn.com.tw", + "about.com", + "chaturbate.com", + "ebay-kleinanzeigen.de", + "slack.com", + "mercadolivre.com.br", + "google.co.il", + "doorblog.jp", + "goodreads.com", + "livejasmin.com", + "battle.net", + "softonic.com", + "accuweather.com", + "amazon.es", + "wordpress.org", + "mbc.net", + "slickdeals.net", + "icloud.com", + "caijing.com.cn", + "google.hu", + "kaskus.co.id", + "wittyfeed.com", + "fedex.com", + "ndtv.com", + "att.com", + "mlb.com", + "kompas.com", + "google.ie", + "giphy.com", + "usatoday.com", + "xcar.com.cn", + "hulu.com", + "archive.org", + "sberbank.ru", + "media.tumblr.com", + "pinimg.com", + "messenger.com", + "sourceforge.net", + "oracle.com", + "hp.com", + "lowes.com", + "zendesk.com", + "viralthread.com", + "csdn.net", + "1905.com", + "mama.cn", + "youtube-mp3.org", + "39.net", + "digikala.com", + "badoo.com", + "businessinsider.com", + "kinogo.co", + "weebly.com", + "samsung.com", + "abs-cbn.com", + "reimageplus.com", + "airbnb.com", + "sabah.com.tr", + "wordreference.com", + "hurriyet.com.tr", + "uefa.com", + "rambler.ru", + "51yes.com", + "cnnic.cn", + "webmd.com", + "ign.com", + "liputan6.com", + "dell.com", + "gizmodo.com", + "blogspot.com.br", + "bloomberg.com", + "blkget.com", + "gsmarena.com", + "evernote.com", + "verizonwireless.com", + "secureserver.net", + "repubblica.it", + "milliyet.com.tr", + "isanalyze.com", + "sharepoint.com", + "eastday.com", + "mailchimp.com", + "nyaa.se", + "enet.com.cn", + "medium.com", + "google.az", + "akamaihd.net", + "dmm.com", + "billdesk.com", + "wsj.com", + "naukri.com", + "kinopoisk.ru", + "friv.com", + "expedia.com", + "livedoor.biz", + "macys.com", + "avg.com", + "tube8.com", + "libero.it", + "intuit.com", + "google.no", + "google.fi", + "urdupoint.com", + "taleo.net", + "addthis.com", + "sahibinden.com", + "ancestry.com", + "likes.com", + "savefrom.net", + "haber7.com", + "answers.com", + "google.sk", + "kissanime.to", + "marca.com", + "k618.cn", + "ebay.in", + "ask.fm", + "wetransfer.com", + "adplxmd.com", + "spiegel.de", + "bild.de", + "livedoor.com", + "ouo.io", + "nownews.com", + "elpais.com", + "thesaurus.com", + "rt.com", + "amazon.ca", + "gearbest.com", + "ck101.com", + "independent.co.uk", + "reuters.com", + "google.dk", + "suning.com", + "daikynguyenvn.com", + "scribd.com", + "themeforest.net", + "lifebuzz.com", + "acfun.tv", + "box.com", + "newegg.com", + "nbcnews.com", + "rutracker.org", + "freepik.com", + "telegram.org", + "kapanlagi.com", + "goal.com", + "blogfa.com", + "southwest.com", + "openload.co", + "hm.com", + "kickstarter.com", + "cloudfront.net", + "thesportbible.com", + "engadget.com", + "impress.co.jp", + "kohls.com", + "playstation.com", + "clickadu.com", + "shopify.com", + "tabelog.com", + "mashable.com", + "upwork.com", + "espncricinfo.com", + "conservativetribune.com", + "siteadvisor.com", + "realtor.com", + "asos.com", + "ci123.com", + "hatena.ne.jp", + "usaa.com", + "xe.com", + "olx.pl", + "videomega.tv", + "ameba.jp", + "atlassian.net", + "sh.st", + "ebay.it", + "doublepimp.com", + "costco.com", + "albawabhnews.com", + "hatenablog.com", + "gismeteo.ru", + "souq.com", + "google.by", + "extratorrent.cc", + "yallakora.com", + "abcnews.go.com", + "baike.com", + "moneycontrol.com", + "lifehacker.com", + "wunderground.com", + "hotels.com", + "zippyshare.com", + "zoho.com", + "discovercard.com", + "fidelity.com", + "cnblogs.com", + "zomato.com", + "jabong.com", + "blogimg.jp", + "nikkei.com", + "4shared.com", + "offerhaus.link", + "hespress.com", + "theverge.com", + "exoclick.com", + "bhaskar.com", + "gamer.com.tw", + "webex.com", + "17ok.com", + "elmogaz.com", + "gap.com", + "gamepedia.com", + "gamersky.com", + "okezone.com", + "gamefaqs.com", + "infusionsoft.com", + "ebay.com.au", + "clipconverter.cc", + "umblr.com", + "ibm.com", + "ticketmaster.com", + "mercadolibre.com.ar", + "beeg.com", + "behance.net", + "surveymonkey.com", + "rbc.ru", + "rednet.cn", + "ca.gov", + "trulia.com", + "zing.vn", + "elmundo.es", + "mi.com", + "aparat.com", + "free.fr", + "airtel.in", + "blackboard.com", + "meetup.com", + "trackingclick.net", + "google.bg", + "google.lk", + "adidas.tmall.com", + "nordstrom.com", + "merdeka.com", + "list-manage.com", + "nike.com", + "list.tmall.com", + "seesaa.net", + "blogspot.jp", + "allrecipes.com", + "google.kz", + "huaban.com", + "nifty.com", + "fiverr.com", + "donga.com", + "mobile.de", + "npr.org", + "kooora.com", + "wayfair.com", + "coursera.org", + "redirectvoluum.com", + "tutorialspoint.com", + "panda.tv", + "lenta.ru", + "weblio.jp", + "xda-developers.com", + "asahi.com", + "google.com.kw", + "thefreedictionary.com", + "adp.com", + "kayak.com", + "yandex.ua", + "kijiji.ca", + "youdao.com", + "korabia.com", + "chaoshi.tmall.com", + "netteller.com", + "drudgereport.com", + "japanpost.jp", + "hotstar.com", + "tradeadexchange.com", + "hdzog.com", + "change.org", + "hootsuite.com", + "dictionary.com", + "makemytrip.com", + "sq.cn", + "streamcloud.eu", + "norton.com", + "blog.me", + "watsons.tmall.com", + "prothom-alo.com", + "researchgate.net", + "ebates.com", + "hotmovs.com", + "olx.in", + "sakura.ne.jp", + "bookmyshow.com", + "subscene.com", + "le.com", + "emol.com", + "justdial.com", + "11st.co.kr", + "cookpad.com", + "retailmenot.com", + "thewatchseries.to", + "alicdn.com", + "shaparak.ir", + "corriere.it", + "gmarket.co.kr", + "overstock.com", + "asus.com", + "y8.com", + "yaolan.com", + "momoshop.com.tw", + "woot.com", + "shopclues.com", + "csgolounge.com", + "disqus.com", + "so-net.ne.jp", + "wattpad.com", + "ytimg.com", + "cbsnews.com", + "leagueoflegends.com", + "prjcq.com", + "reddituploads.com", + "patch.com", + "4399.com", + "adbooth.com", + "sabq.org", + "dropbooks.tv", + "www.gov.uk", + "udemy.com", + "cnmo.com", + "bleacherreport.com", + "aa.com", + "lapatilla.com", + "instructables.com", + "clien.net", + "bidverdrd.com", + "nba.com", + "mirror.co.uk", + "interia.pl", + "nhk.or.jp", + "ensonhaber.com", + "yadi.sk", + "line.me", + "mercadolibre.com.ve", + "e-hentai.org", + "uploaded.net", + "verizon.com", + "alexa.cn", + "namu.wiki", + "yodobashi.com", + "heroquizz.com", + "latimes.com", + "123cha.com", + "pof.com", + "eyny.com", + "azlyrics.com", + "aliyun.com", + "people.com", + "yandex.com.tr", + "rottentomatoes.com", + "104.com.tw", + "houzz.com", + "time.com", + "cisco.com", + "ppomppu.co.kr", + "delta.com", + "biglobe.ne.jp", + "jrj.com.cn", + "xunlei.com", + "lenovo.com", + "usbank.com", + "vk.me", + "zulily.com", + "asana.com", + "youboy.com", + "zone-telechargement.com", + "glassdoor.com", + "seasonvar.ru", + "nikkeibp.co.jp", + "investing.com", + "google.com.do", + "united.com", + "teepr.com", + "indianexpress.com", + "wikiwiki.jp", + "pch.com", + "jimdo.com", + "oeeee.com", + "gyazo.com", + "myfitnesspal.com", + "yts.ag", + "custhelp.com", + "paytm.in", + "nextdoor.com", + "voc.com.cn", + "cnbc.com", + "sears.com", + "bs.to", + "itmedia.co.jp", + "thekitchn.com", + "bomb01.com", + "slate.com", + "torcache.net", + "google.com.my", + "4chan.org", + "biobiochile.cl", + "google.co.nz", + "okcupid.com", + "google.rs", + "instructure.com", + "intoday.in", + "zhanqi.tv", + "chinaso.com", + "squarespace.com", + "fitbit.com", + "photobucket.com", + "eskimi.com", + "mixi.jp", + "4dsply.com", + "thatviralfeed.com", + "pantip.com", + "as.com", + "elfagr.org", + "3dmgame.com", + "howtogeek.com", + "aastocks.com", + "manoramaonline.com", + "alwafd.org", + "inquirer.net", + "himado.in", + "xbox.com", + "meaww.com", + "pogo.com", + "redfin.com", + "buy.tmall.com", + "drom.ru", + "infoseek.co.jp", + "buzzfil.net", + "urbandictionary.com", + "mit.edu", + "olx.com.br", + "chinaz.com", + "chip.de", + "panet.co.il", + "gamespot.com", + "google.com.ec", + "subito.it", + "gazetaexpress.com", + "zara.com", + "swagbucks.com", + "tmz.com", + "nexusmods.com", + "tsite.jp", + "timeanddate.com", + "discuss.com.hk", + "quanjing.com", + "bhphotovideo.com", + "lequipe.fr", + "superuser.com", + "ero-advertising.com", + "myntra.com", + "cdiscount.com", + "issuu.com", + "filehippo.com", + "quikr.com", + "axisbank.co.in", + "gameforge.com", + "twoo.com", + "4pda.ru", + "java.com", + "blogspot.mx", + "spankbang.com", + "discover.com", + "kdnet.net", + "agar.io", + "priceline.com", + "mercadolibre.com.mx", + "atwiki.jp", + "audible.com", + "mynavi.jp", + "vodlocker.com", + "sky.com", + "sciencedirect.com", + "gigazine.net", + "olx.ua", + "livescore.com", + "58.com", + "syosetu.com", + "prezi.com", + "hotnewhiphop.com", + "jcpenney.com", + "ero-video.net", + "nypost.com", + "nydailynews.com", + "acunn.com", + "foodnetwork.com", + "mint.com", + "bitbucket.org", + "creditkarma.com", + "mataharimall.com", + "rarbg.to", + "tim.it", + "anitube.se", + "mapquest.com", + "westernjournalism.com", + "eventbrite.com", + "liveinternet.ru", + "politico.com", + "android.com", + "weather.gov", + "lun.com", + "bankmellat.ir", + "sfgate.com", + "misrjournal.com", + "lemonde.fr", + "uber.com", + "givemesport.com", + "stockstar.com", + "wiktionary.org", + "51sole.com", + "google.hr", + "reverso.net", + "thehindu.com", + "marriott.com", + "yomiuri.co.jp", + "topix.com", + "prpops.com", + "sankei.com", + "europa.eu", + "pcmag.com", + "constantcontact.com", + "battlefield.com", + "php.net", + "ria.ru", + "fanpage.gr", + "getpocket.com", + "cpasbien.cm", + "diep.io", + "rightmove.co.uk", + "sfr.fr", + "hilton.com", + "sozcu.com.tr", + "blog-newstime.com", + "agoda.com", + "india.com", + "bodybuilding.com", + "techcrunch.com", + "thisav.com", + "hbogo.com", + "zappos.com", + "google.lt", + "appledaily.com.tw", + "geocities.jp", + "duckduckgo.com", + "irs.gov", + "kotaku.com", + "marktplaats.nl", + "eroterest.net", + "duolingo.com", + "ebay.fr", + "wp.com", + "jin115.com", + "staples.com", + "youjizz.com", + "fanfiction.net", + "walgreens.com", + "bt.com", + "microsoftstore.com", + "123movies.to", + "126.com", + "exblog.jp", + "yjc.ir", + "t-mobile.com", + "farsnews.com", + "chron.com", + "stumbleupon.com", + "parentanswercentre.com", + "123rf.com", + "breitbart.com", + "hh.ru", + "cbssports.com", + "sex.com", + "pnc.com", + "mega.co.nz", + "lazada.co.id", + "marketwatch.com", + "ew.com", + "ryanair.com", + "gazeta.pl", + "gmanetwork.com", + "dcinside.com", + "ampclicks.com", + "slimspots.com", + "basecamp.com", + "cctv.com", + "incometaxindiaefiling.gov.in", + "citibank.co.in", + "brightonclick.com", + "gazzetta.it", + "indianrail.gov.in", + "idnes.cz", + "thedailybeast.com", + "intel.com", + "auction.co.kr", + "fatosdesconhecidos.com.br", + "to8to.com", + "ultimate-guitar.com", + "mcafee.com", + "neobux.com", + "tripadvisor.co.uk", + "tomshardware.com", + "rapidgator.net", + "bola.net", + "hubspot.com", + "otto.de", + "digitaltrends.com", + "elevenia.co.id", + "n121adserv.com", + "gofundme.com", + "lacaixa.es", + "ctrip.com", + "flashx.tv", + "quizlet.com", + "banggood.com", + "garmin.com", + "primewire.ag", + "vox.com", + "labanquepostale.fr", + "telekom.com", + "drive2.ru", + "eonline.com", + "chosun.com", + "credit-agricole.fr", + "wowhead.com", + "match.com", + "hamariweb.com", + "vnexpress.net", + "familydoctor.com.cn", + "thethao247.vn", + "state.gov", + "todayhumor.co.kr", + "abril.com.br", + "uzone.id", + "hepsiburada.com", + "wwwpromoter.com", + "popsugar.com", + "panasonic.jp", + "360doc.com", + "minecraft.net", + "internethaber.com", + "dailypakistan.com.pk", + "bedbathandbeyond.com", + "pixabay.com", + "wav.tv", + "novinky.cz", + "uptobox.com", + "whitepages.com", + "icolor.com.cn", + "babycenter.com", + "qvc.com", + "indiamart.com", + "onlinecreditcenter6.com", + "6pm.com", + "g2a.com", + "milanuncios.com", + "mayoclinic.org", + "commentcamarche.net", + "gawker.com", + "fandango.com", + "duowan.com", + "myanimelist.net", + "telegram.me", + "genius.com", + "wiley.com", + "liveleak.com", + "divar.ir", + "bestadbid.com", + "niuche.com", + "chouftv.ma", + "life.tw", + "dream.co.id", + "cocolog-nifty.com", + "ptt.cc", + "legacy.com", + "mynet.com", + } + + start := time.Now() + for _, v := range items { + LevenshteinDistanceSubstring("flake", v) + } + elapsed := time.Since(start) + fmt.Printf("Time elapsed: %s\n", elapsed) + + maxTime := 100000 + + if elapsed > time.Duration(maxTime) { + t.Errorf("FindClosestSubstring duration = %d; wanted < %d", elapsed, maxTime) + } + +} diff --git a/readme.md b/readme.md index 84aeeff..bea8035 100644 --- a/readme.md +++ b/readme.md @@ -99,7 +99,7 @@ COMMANDS: help, h Shows a list of commands or help for one command GLOBAL OPTIONS: - --print, -p Displays password on the terminal (default: false) + --print, -p Displays password on the terminal (default: false) --help, -h Shows help (default: false) --version, -v Prints the version (default: false) @@ -113,14 +113,15 @@ To configure passline open to the ~/.passline/config.json file and modify any of The following illustrates all the available options with their respective default values. -``` json +```json { - "Storage": "firestore", - "AutoClip": true, - "Notifications": true, - "QuickSelect": true, - "DefaultUsername": "thomaspoehlmann96@googlemail.com", - "DefaultCategory": "*" + "Storage": "firestore", + "AutoClip": true, + "Notifications": true, + "QuickSelect": true, + "DefaultUsername": "thomaspoehlmann96@googlemail.com", + "DefaultCategory": "*", + "Encryption": 1 } ``` @@ -146,7 +147,11 @@ Default username to suggest ### DefaultCategory -If this is set to something else then "*" only this category will be used to suggest items. This can be overwritten by the --category option. +If this is set to something else then "\*" only this category will be used to suggest items. This can be overwritten by the --category option. + +### Encryption (EXPERIMENTAL) + +Encryption mode. When set to `1` only passwords and recovery codes are encrypted. When set to `2` the complete vault is encrypted. ## Before flight @@ -160,7 +165,6 @@ When you want to use the local storage module there is no further configuration or follow this [instruction page](https://cloud.google.com/docs/authentication/production#providing_credentials_to_your_application). - ## Development ### Linter @@ -177,13 +181,13 @@ VS-Code settings ### Build -``` bash +```bash GOOS=windows GOARCH=amd64 go build ``` ### Test -``` bash +```bash go test ./... ``` @@ -200,4 +204,4 @@ Use this [tool](https://tomeko.net/online_tools/file_to_hex.php?lang=en) to easy [MIT](https://github.com/perryrh0dan/passline/blob/master/license.md) -This repository was generated by [tmpo](https://github.com/perryrh0dan/tmpo) \ No newline at end of file +This repository was generated by [tmpo](https://github.com/perryrh0dan/tmpo)