From 21554880df32ca4c6003f821cbffcae8a19e3863 Mon Sep 17 00:00:00 2001 From: Robert Thanulingam Date: Sat, 20 Apr 2024 00:10:24 -0700 Subject: [PATCH] reorganized files with few local lib/modules --- .gitignore | 3 +- Taskfile.yaml | 30 ++- cmd/cmd.go | 293 +++++++++++++++------------- cmd/createdb.go | 138 ++++++------- cmd/diff.go | 29 ++- cmd/ls.go | 39 ++-- cmd/struct.go | 143 -------------- lib/config/configs.go | 93 +++++++++ lib/models/options.go | 79 ++++++++ cmd/notify.go => lib/utils/utils.go | 29 ++- 10 files changed, 490 insertions(+), 386 deletions(-) delete mode 100644 cmd/struct.go create mode 100644 lib/config/configs.go create mode 100644 lib/models/options.go rename cmd/notify.go => lib/utils/utils.go (68%) diff --git a/.gitignore b/.gitignore index 6c8b209..081d82a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ .envrc* .vscode/ bin/kpcli -*.password +*.creds tmp/ .DS_Store database1.out @@ -12,3 +12,4 @@ local/ *.log bin/ bkups/ +*.csv diff --git a/Taskfile.yaml b/Taskfile.yaml index 8457885..db269d2 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -8,7 +8,6 @@ vars: COMMIT_COUNT: sh: git rev-list HEAD --count VERSION: '{{ printf "%s.%s" .GIT_HASH .COMMIT_COUNT }}' - KDBX_BKUP_DIR: /Users/rt/Documents/RobertsFamily/backups/keepass/ tasks: default: @@ -19,6 +18,13 @@ tasks: cleanup: cmd: rm -rf ./tmp + generate-sample-config: + aliases: + - "gen" + cmds: + - | + bin/kpcli gen + add-entry: silent: true cmds: @@ -107,15 +113,15 @@ tasks: - | rm -f tmp.csv diff.log - recentFile=$(ls -tl /Users/rt/Documents/RobertsFamily/backups/keepass/ | awk 'NR==2 { print $9 }') + recentFile=$(ls -tl ${KDBX_BKUP_DIR} | awk 'NR==2 { print $9 }') echo "recentFile: ${recentFile}" - bin/kpcli --keyfile /Users/rt/Personal/password-databases/keepass-keys/RobertsFamily.key \ - --database {{.KDBX_BKUP_DIR}}/${recentFile} \ + bin/kpcli --keyfile ${KEYFILE} \ + --database ${KDBX_BKUP_DIR}/${recentFile} \ --pass $(pass RobertsFamily.kdbx) \ ls -of csv --quite > database1.out - bin/kpcli --keyfile /Users/rt/Personal/password-databases/keepass-keys/RobertsFamily.key \ - --database /Users/rt/syncWithCloud/googleDrive/keepass/RobertsFamily.kdbx \ + bin/kpcli --keyfile ${KEYFILE} \ + --database ${CURRENT_KDBX} \ --pass $(pass RobertsFamily.kdbx) \ ls -of csv --quite > database2.out @@ -123,12 +129,22 @@ tasks: diffs=$(diff --suppress-common-lines -U0 database1.out database2.out) || true echo "${diffs}" | grep -v '\-\-\-' | tail -n +3 >> tmp.csv - csvtable --border diff.log + csvtable diff.log diffCount=$(grep -c . diff.log) if [[ "${diffCount}" -gt "4" ]]; then cat diff.log fi + diff2: + cmds: + - | + bin/kpcli \ + --keyfile ./tmp/master-db.key \ + --database ./tmp/master-db.kdbx.194552000 \ + --pass "${KDBX_PASSWORD}" \ + diff \ + --database2 ./tmp/master-db.kdbx + # release-check: # desc: "check release tag" # cmds: diff --git a/cmd/cmd.go b/cmd/cmd.go index 35b04ea..1f80924 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -9,6 +9,10 @@ import ( "strings" "time" + "github.com/robertranjan/kpcli/lib/config" + "github.com/robertranjan/kpcli/lib/models" + "github.com/robertranjan/kpcli/lib/utils" + "github.com/brianvoe/gofakeit/v6" "github.com/sirupsen/logrus" "github.com/tobischo/gokeepasslib/v3" @@ -16,45 +20,21 @@ import ( "github.com/urfave/cli/v2" ) +type Client models.Client + +var client *Client + var ( // Note: credsFile used by cmds: [ add, create ] - credsFile string = "./tmp/master-db.creds" - colorGreen = "\033[32m" - colorReset = "\033[0m" - colorRed = "\033[31m" - colorYellow = "\033[33m" - TimeLayout = "2006-01-02 15:04:05" - lengthUser = 25 - configFile = "unavailable_kpcli.toml" - config Config - log *logrus.Logger + // credsFile = "./tmp/master-db.creds" + TimeLayout = "2006-01-02 15:04:05" + lengthUser = 25 + configFile = "unavailable_kpcli.toml" + cfg *config.Config + log *logrus.Logger ) -var sampleConfig = ` -[notify] -emailContent = "will be generated during execution" -emailPassword = "keepass_gmail_app_password" -from = "yourEmail@gmail.com" -smtpHost = "smtp.gmail.com" -smtpPort = 587 -subject = "here are the KDBX changes since last backup!" -to = ["yourEmail@gmail.com", "email2@domain.com"] - -[create] -database = "./tmp/master-db.kdbx" -keyfile = "./tmp/master-db.key" -password = "super_s3cr3t" - -[diffCfg] -database1 = "./tmp/database1" -database2 = "./tmp/database2" -keyfile1 = "./tmp/keyfile1" -keyfile2 = "./tmp/keyfile2" -outputFilename = "diffLog2Email.html" -password1 = "super_secret" -password2 = "super_secret" -` - +// CmdAdd helps user to add entry to password db var CmdAdd = &cli.Command{ Name: "add", Usage: "add an entry", @@ -98,6 +78,7 @@ Example: }, } +// CmdCreatedb creates password db var CmdCreatedb = &cli.Command{ Name: "create", Usage: "Create a new kdbx databse", @@ -122,13 +103,14 @@ Example: Action: runCreate, Flags: []cli.Flag{ &cli.IntFlag{ - Name: "entries", + Name: "sample-entries", Usage: "number of sample entries", - Aliases: []string{"e"}, + Aliases: []string{"se"}, }, }, } +// CmdDiff runs diff between 2 passward dbs var CmdDiff = &cli.Command{ Name: "diff", Usage: "diff entries between 2 kdbx databases", @@ -180,6 +162,7 @@ example: }, } +// CmdGenerateSampleConfig generate a sample config var CmdGenerateSampleConfig = &cli.Command{ Name: "generate-sample-config", Usage: "generate sample config file: kpcli.toml", @@ -194,10 +177,16 @@ Example: } func runGenerateSampleConfig(app *cli.Context) error { + InitGetLogger(app.String("log-level")) + if utils.IsFileExist("kpcli.toml") { + log.Info("found file, backing up existing file to tmp/") + backupFile("kpcli.toml") + } fmt.Println("writing sample config file: kpcli.toml") - return os.WriteFile("kpcli.toml", []byte(sampleConfig), 0600) + return os.WriteFile("kpcli.toml", []byte(config.SampleConfig), 0600) } +// CmdLs lists entries from a db var CmdLs = &cli.Command{ Name: "ls", Usage: "lists entries", @@ -274,140 +263,174 @@ func init() { rand.New(rand.NewSource(time.Now().UnixNano())) } +// LoadConfigOnDemand loads config if present +func LoadConfigOnDemand(configFile string) { + if configFile == "" { + // no need to load as configFile == null + return + } + var err error + if cfg, err = config.New(configFile); err != nil { + fmt.Println("LoadConfig failed. Continuing with cli args...") + } + log.Debugf("config: %s\n", cfg.String()) +} + +// runAddEntry - update pkg->var: client func runAddEntry(app *cli.Context) error { - d, err := newObject(app) + var err error + opts := getOptions(app) + + client, err = newClient(opts) if err != nil { fmt.Printf("failed to create db : %v\n", err) return err } - if d.Options.Pass == "" { - d.Options.Pass = "super_secret" + db = &kdbx{ + Options: client.Options, } - return d.AddEntry() -} -func loadConfigOnDemand(configFile string) { - if configFile == "" { - // no need to load as configFile == null - return + if client.Options.Pass == "" { + client.Options.Pass = "super_secret" } - if err := config.loadFromFile(configFile); err != nil { - fmt.Println("LoadConfig failed. Continuing with cli args...") - } - log.Debugf("config: %s\n", config.String()) + return db.AddEntry() } func getOutputFilename() string { outputFilename := "diffLog2Email.html" - if config.OutputFilename != "" { - outputFilename = config.OutputFilename + if cfg != nil && cfg.OutputFilename != "" { + outputFilename = cfg.OutputFilename } return outputFilename } -func newObject(app *cli.Context) (*db, error) { - - //setup logger - log = InitGetLogger(app.String("log-level")) - - // read config on demand - configFile = app.String("config") - loadConfigOnDemand(configFile) +func getOptions(app *cli.Context) *models.Options { outputFilename := getOutputFilename() - opts := Options{ + return &models.Options{ + // diff BackupDIR: app.String("backup-dir"), CacheFile: app.String("cachefile"), - Config: app.String("config"), Database: app.String("database"), Database2: app.String("database2"), - Days: app.Int("days"), DiffCalling: app.Bool("diff-calling"), - EntryPass: app.String("entry-pass"), - EntryTitle: app.String("entry-title"), - EntryUser: app.String("entry-user"), - Fields: app.String("fields"), - Key: app.String("keyfile"), - Key2: app.String("keyfile2"), - LogLevel: app.String("log-level"), - NoKey: app.Bool("nokey"), Notify: app.Bool("notify"), OutputFilename: outputFilename, - OutputFormat: app.String("output-format"), Pass: app.String("pass"), Pass2: app.String("pass2"), - Quite: app.Bool("quite"), - Reverse: app.Bool("reverse"), - SampleEntries: app.Int("entries"), - Sort: app.String("sort"), - SortbyCol: app.Int("sortby-col"), - } - d, err := NewDB(opts) - if err != nil { - return nil, err + // add entry + EntryPass: app.String("entry-pass"), + EntryTitle: app.String("entry-title"), + EntryUser: app.String("entry-user"), + + // ls + Days: app.Int("days"), + Fields: app.String("fields"), + Key: app.String("keyfile"), + Key2: app.String("keyfile2"), + Reverse: app.Bool("reverse"), + Sort: app.String("sort"), + SortbyCol: app.Int("sortby-col"), + + // common + Config: app.String("config"), + LogLevel: app.String("log-level"), + OutputFormat: app.String("output-format"), + Quite: app.Bool("quite"), + + // create db + NoKey: app.Bool("nokey"), + SampleEntries: app.Int("sample-entries"), } +} + +// newClient creates a base db object using cli-args +func newClient(opts *models.Options) (*Client, error) { + //setup logger + log = InitGetLogger(opts.LogLevel) - if d.Options.LogLevel == "debug" { - log.Printf("opts: \n%v\n", opts.String()) + // read config on demand + // configFile = app.String("config") + configFile = opts.Config + LoadConfigOnDemand(configFile) + + // opts := getOptions(app) + // log.Debugf("opts: \n%v\n", opts.String()) + + c := &Client{ + Options: opts, + CredentialFile: "./tmp/master-db.creds", } // generate credsFile path - if d.Options.Key != "" { - credsFile = strings.Split(filepath.Base(d.Options.Key), ".")[0] + ".creds" - credsFile = filepath.Join(filepath.Dir(d.Options.Key), credsFile) + // overwrite default value with user-args + credsFile := strings.Split(filepath.Base(c.Options.Database), ".")[0] + ".creds" + credsFile = filepath.Join(filepath.Dir(c.Options.Database), credsFile) + c.CredentialFile = credsFile + return c, nil +} + +func updateDefaultOptionValues() { + if client.Options.Database == "" { + client.Options.Database = "./tmp/master-db.kdbx" } - if d.Options.NoKey { - credsFile = strings.Split(filepath.Base(d.Options.Database), ".")[0] + ".creds" - credsFile = filepath.Join(filepath.Dir(d.Options.Database), credsFile) + if client.Options.Key == "" { + client.Options.Key = "./tmp/master-db.key" + } + if client.Options.SampleEntries == 0 { + client.Options.SampleEntries = rand.Intn(11) + 1 } - - return d, nil } func runCreate(app *cli.Context) error { - d, err := newObject(app) + var err error + opts := getOptions(app) + + client, err = newClient(opts) if err != nil { fmt.Printf("failed to create db : %v\n", err) return err } - if d.Options.Database == "" { - d.Options.Database = "./tmp/master-db.kdbx" - } - if d.Options.Pass == "" { - d.Options.Pass = gofakeit.Password(true, true, true, true, false, 16) - } - if d.Options.Key == "" { - d.Options.Key = "./tmp/master-db.key" - } + updateDefaultOptionValues() - if d.Options.SampleEntries == 0 { - d.Options.SampleEntries = rand.Intn(11) + 1 + if client.Options.Pass == "" { + // generate a sample password with(lwr, upr, numeric,spl, space, length ) + client.Options.Pass = gofakeit.Password(true, true, true, true, false, 16) } - err = d.PreVerifyCreate() + err = client.PreVerifyCreate() + if err != nil { + return err + } + // initialize global database: d + db = &kdbx{ + Options: client.Options, + } + db.RawData, err = client.CreateKDBX() if err != nil { return err } - return d.CreateKDBX() + return nil } func runDiff(app *cli.Context) error { - opts := Options{ - Pass: app.String("pass"), - Pass2: app.String("pass2"), - Database: app.String("database"), - Database2: app.String("database2"), - BackupDIR: app.String("backup-dir"), - Key: app.String("keyfile"), - Key2: app.String("keyfile2"), - Notify: app.Bool("notify"), - OutputFormat: "csv", - OutputFilename: "diffLog2Email.html", - } + opts := getOptions(app) + // opts := models.Options{ + // Pass: app.String("pass"), + // Pass2: app.String("pass2"), + // Database: app.String("database"), + // Database2: app.String("database2"), + // BackupDIR: app.String("backup-dir"), + // Key: app.String("keyfile"), + // Key2: app.String("keyfile2"), + // Notify: app.Bool("notify"), + // OutputFormat: "csv", + // OutputFilename: "diffLog2Email.html", + // } pattern := strings.Split(path.Base(opts.Database), ".")[0] if opts.Database2 == "" { @@ -419,36 +442,28 @@ func runDiff(app *cli.Context) error { } func runLs(app *cli.Context) error { - d, err := newObject(app) + var err error + opts := getOptions(app) + + client, err = newClient(opts) if err != nil { - fmt.Printf("failed to create db : %v\n", err) + fmt.Printf("failed to create client : %v\n", err) return err } - if d.Options.Key == "" || d.Options.Database == "" { - fmt.Printf("%v"+` --database is a required arguments. - If you are trying, run below commands: - 1. kpcli createdb - 2. kpcli ls`+"%v\nHere is usage:\n%v", colorYellow, colorGreen, colorReset) - cli.ShowAppHelpAndExit(app, 0) - return nil + // NewDB create and return a new kdbx db object + + db = &kdbx{ + Options: client.Options, } - if err = d.Unlock(); err != nil { - fmt.Printf("failed to unlock dbfile: %v, err: %v\n", d.Options.Database, err) + if err = db.Unlock(); err != nil { + fmt.Printf("failed to unlock dbfile: %v, err: %v\n", client.Options.Database, err) return err } - d.FetchDBEntries() + db.FetchDBEntries() - return d.List() -} - -// NewDB create and return a new kdbx db object -func NewDB(opts Options) (*db, error) { - d := &db{ - Options: &opts, - } - return d, nil + return db.List() } func MkValue(key string, value string) gokeepasslib.ValueData { diff --git a/cmd/createdb.go b/cmd/createdb.go index c85f78a..09a44c9 100644 --- a/cmd/createdb.go +++ b/cmd/createdb.go @@ -1,7 +1,6 @@ package cmd import ( - "errors" "fmt" "strconv" "time" @@ -11,32 +10,44 @@ import ( "path/filepath" "strings" + "github.com/robertranjan/kpcli/lib/models" + "github.com/bitfield/script" "github.com/brianvoe/gofakeit/v6" + "github.com/robertranjan/kpcli/lib/utils" "github.com/tobischo/gokeepasslib/v3" ) +type kdbx struct { + Entries []models.Interested + Options *models.Options + SelectedEntries []models.Interested + RawData *gokeepasslib.Database +} + +var db *kdbx + // NewDB creates and returns a new kdbx database -func (d *db) PreVerifyCreate() error { +func (c *Client) PreVerifyCreate() error { // return if database already exist - if _, err := os.Stat(d.Options.Database); !errors.Is(err, os.ErrNotExist) { + if utils.IsFileExist(c.Options.Database) { return fmt.Errorf("%s %s already exist, won't OVERWRITE%s", - colorRed, d.Options.Database, colorReset) + utils.ColorRed, c.Options.Database, utils.ColorReset) } // return if keyfile already exist - if !d.Options.NoKey { - if _, err := os.Stat(d.Options.Key); !errors.Is(err, os.ErrNotExist) { + if !c.Options.NoKey { + if utils.IsFileExist(c.Options.Key) { return fmt.Errorf("%sfile: %s already exist, won't OVERWRITE\n%s", - colorRed, d.Options.Key, colorReset) + utils.ColorRed, c.Options.Key, utils.ColorReset) } } - // write the password to file: {database}.creds - passFile := strings.Split(filepath.Base(d.Options.Database), ".")[0] + ".creds" - if _, err := os.Stat(passFile); !errors.Is(err, os.ErrNotExist) { + // write the credentials to file: {database}.creds + cred := strings.Split(filepath.Base(c.Options.Database), ".")[0] + ".creds" + if utils.IsFileExist(cred) { fmt.Printf("%sWill overwrite password file: %s\n%s", - colorGreen, passFile, colorReset) + utils.ColorGreen, cred, utils.ColorReset) } return nil } @@ -49,7 +60,7 @@ func GenerateKDBXEntries(n int) []gokeepasslib.Entry { return rv } -func (d *db) generateSampleEntries() gokeepasslib.Group { +func (d *kdbx) generateSampleEntries() gokeepasslib.Group { // create root group rootGroup := gokeepasslib.NewGroup() rootGroup.Name = "root group" @@ -67,54 +78,54 @@ func (d *db) generateSampleEntries() gokeepasslib.Group { return rootGroup } -func (d *db) writeCredentialsFile() error { - content := "export KDBX_DATABASE=" + d.Options.Database + "\n" - content += "export KDBX_PASSWORD='" + d.Options.Pass + "'\n" - if !d.Options.NoKey { - content += "export KDBX_KEYFILE=" + d.Options.Key + "\n" +func (c *Client) writeCredentialsFile() error { + content := "export KDBX_DATABASE=" + c.Options.Database + "\n" + content += "export KDBX_PASSWORD='" + c.Options.Pass + "'\n" + if !c.Options.NoKey { + content += "export KDBX_KEYFILE=" + c.Options.Key + "\n" } - if err := os.WriteFile(credsFile, []byte(content), 0600); err != nil { - return fmt.Errorf("failed to write file %v, err: %v", credsFile, err) + if err := os.WriteFile(c.CredentialFile, []byte(content), 0600); err != nil { + return fmt.Errorf("failed to write file %v, err: %v", c.CredentialFile, err) } return nil } -func (d *db) generateCredentials() error { +func (c *Client) generateCredentials() error { var cred *gokeepasslib.DBCredentials var err error - if d.Options.NoKey { - cred = gokeepasslib.NewPasswordCredentials(d.Options.Pass) + if c.Options.NoKey { + cred = gokeepasslib.NewPasswordCredentials(c.Options.Pass) if cred == nil { return fmt.Errorf("failed to create credentials with pass:%q ", - d.Options.Pass) - } - } else { - // Check if the keyfile exists - if _, err = os.Stat(d.Options.Key); err == nil { - cred, err = gokeepasslib.NewPasswordAndKeyCredentials(d.Options.Pass, d.Options.Key) - if err != nil { - return fmt.Errorf("failed to create credentials with pass:%q and keyFile:%q, err: %v", - d.Options.Pass, d.Options.Key, err) - } - } else if os.IsNotExist(err) { - return fmt.Errorf("%v file does not exist. \nDid you forget to mention --nokey option?", d.Options.Key) - } else { - return fmt.Errorf("failed to check %v file exist or not:%v", d.Options.Key, err) + c.Options.Pass) } + c.Credentials = cred + return nil + } + // check keyfile + if !utils.IsFileExist(c.Options.Key) { + return fmt.Errorf("%v file does not exist. \nDid you forget to mention --nokey option?", c.Options.Key) + } + // gen creds with keyfile + cred, err = gokeepasslib.NewPasswordAndKeyCredentials(c.Options.Pass, c.Options.Key) + if err != nil { + return fmt.Errorf("failed to create credentials with pass:%q and keyFile:%q, err: %v", + c.Options.Pass, c.Options.Key, err) } - d.Credentials = cred + + c.Credentials = cred return nil } -func (d *db) CreateKDBX() error { - err := os.MkdirAll(filepath.Dir(d.Options.Database), 0755) +func (c *Client) CreateKDBX() (*gokeepasslib.Database, error) { + err := os.MkdirAll(filepath.Dir(c.Options.Database), 0755) if err != nil { - return err + return nil, err } - file, err := os.Create(d.Options.Database) + file, err := os.Create(c.Options.Database) if err != nil { - return fmt.Errorf("failed to create dbfile: %v", err) + return nil, fmt.Errorf("failed to create dbfile: %v", err) } defer func() { if err := file.Close(); err != nil { @@ -123,25 +134,25 @@ func (d *db) CreateKDBX() error { }() // write keyfile and password file - if !d.Options.NoKey { - err = os.WriteFile(d.Options.Key, []byte(gofakeit.BitcoinPrivateKey()), 0600) + if !c.Options.NoKey { + err = os.WriteFile(c.Options.Key, []byte(gofakeit.BitcoinPrivateKey()), 0600) if err != nil { - return fmt.Errorf("failed to write keyfile: %v, err: %v", d.Options.Key, err) + return nil, fmt.Errorf("failed to write keyfile: %v, err: %v", c.Options.Key, err) } } - if err = d.generateCredentials(); err != nil { - return err + if err = c.generateCredentials(); err != nil { + return nil, err } - if err := d.writeCredentialsFile(); err != nil { - return err + if err := c.writeCredentialsFile(); err != nil { + return nil, err } // now create the database with the sample rootGroup - rootGroup := d.generateSampleEntries() - db := &gokeepasslib.Database{ + rootGroup := db.generateSampleEntries() + database := &gokeepasslib.Database{ Header: gokeepasslib.NewHeader(), - Credentials: d.Credentials, + Credentials: c.Credentials, Content: &gokeepasslib.DBContent{ Meta: gokeepasslib.NewMetaData(), Root: &gokeepasslib.RootData{ @@ -151,23 +162,23 @@ func (d *db) CreateKDBX() error { } // Lock entries using stream cipher - if err := db.LockProtectedEntries(); err != nil { + if err := database.LockProtectedEntries(); err != nil { log.Printf("error in Locking protected entries, err: %v", err) } // and encode it into the file keepassEncoder := gokeepasslib.NewEncoder(file) - if err := keepassEncoder.Encode(db); err != nil { - return fmt.Errorf("failed to encode db file: %v", err) + if err := keepassEncoder.Encode(database); err != nil { + return nil, fmt.Errorf("failed to encode db file: %v", err) } fmt.Printf(`Created %s file with %d sample entries. To list entries, 1. source %v - 2. kpcli ls`, d.Options.Database, d.Options.SampleEntries*2, credsFile) - return nil + 2. kpcli ls`, c.Options.Database, c.Options.SampleEntries*2, c.CredentialFile) + return database, nil } -func (d *db) AddEntry() error { +func (d *kdbx) AddEntry() error { err := d.Unlock() if err != nil { log.Print("failed to unlock db, err: ", err) @@ -197,30 +208,25 @@ func (d *db) AddEntry() error { } // make a copy/backup of kdbx database to backupDir - err = MakeACopy(d.Options.Database) + err = backupFile(d.Options.Database) if err != nil { return err } // make a copy/backup of keyfile to backupDir if !d.Options.NoKey { - err = MakeACopy(d.Options.Key) + err = backupFile(d.Options.Key) if err != nil { return err } } - // make a copy/backup of credsFile to backupDir - err = MakeACopy(credsFile) - if err != nil { - return err - } log.Debugf("kdbx with added entry(%v) has written to: %s. Total entries: %v\n", entry1.GetTitle(), newFile, len(rootgp.Entries)) return nil } -func MakeACopy(cur string) error { +func backupFile(cur string) error { d, f := filepath.Split(cur) newFile := filepath.Join(d, f+"."+strconv.Itoa(time.Now().Nanosecond())) diff --git a/cmd/diff.go b/cmd/diff.go index 93c891c..e837112 100644 --- a/cmd/diff.go +++ b/cmd/diff.go @@ -11,11 +11,15 @@ import ( "time" "github.com/fatih/color" + "github.com/robertranjan/kpcli/lib/models" + "github.com/robertranjan/kpcli/lib/utils" ) +type Diff models.Diff + // NewDiff returns a *Diff -func NewDiff(opts Options) *Diff { - var fromDBOpts = Options{ +func NewDiff(opts *models.Options) *Diff { + var fromDBOpts = models.Options{ CacheFile: "database2.out", Database: opts.Database2, Days: 10000, @@ -29,7 +33,7 @@ func NewDiff(opts Options) *Diff { // Quite: true, // options for diff cmd } - var toDBOpts = Options{ + var toDBOpts = models.Options{ CacheFile: "database1.out", Database: opts.Database, Days: 10000, @@ -47,7 +51,7 @@ func NewDiff(opts Options) *Diff { return &Diff{ ToDBOption: &toDBOpts, FromDBOption: &fromDBOpts, - options: &opts, + Options: opts, } } @@ -81,6 +85,13 @@ func getRecentFile(dir string, filename string) string { return filepath.Join(dir, recentFile) } +func NewDB(opts models.Options) (*kdbx, error) { + d := &kdbx{ + Options: &opts, + } + return d, nil +} + // Diff shows the difference between 2 databases // notify option can be used to notify your email id (work only for gmail at the moment) func (d *Diff) Diff() error { @@ -107,7 +118,7 @@ func (d *Diff) Diff() error { } outputHeader := []byte(fmt.Sprintf("here are the diffs between %v and %v\n", - d.options.Database2, d.options.Database)) + d.Options.Database2, d.Options.Database)) cmd := exec.Command("diff", []string{"database2.out", "database1.out"}...) outputHeader = append(outputHeader, []byte(strings.Repeat("-", 70))...) outputHeader = append(outputHeader, []byte("\n")...) @@ -145,13 +156,13 @@ func (d *Diff) Diff() error { HTMLOut = append(HTMLOut, outputHeader...) HTMLOut = append(HTMLOut, []byte(strings.Join(HTMLLines, "\n"))...) HTMLOut = append(HTMLOut, []byte("")...) - err = os.WriteFile(d.options.OutputFilename, HTMLOut, 0600) + err = os.WriteFile(d.Options.OutputFilename, HTMLOut, 0600) if err != nil { - return fmt.Errorf("failed to write file: %v, err: %v", d.options.OutputFilename, err) + return fmt.Errorf("failed to write file: %v, err: %v", d.Options.OutputFilename, err) } - if d.options.Notify && len(ANSILines) > 0 { - d.Notify(d.options.OutputFilename) + if d.Options.Notify && len(ANSILines) > 0 { + utils.Notify(d.Options.OutputFilename) } else { color.Yellow("\n >>> Not sending any emails " + "as there is no changes or notification wasn't requested.\n") diff --git a/cmd/ls.go b/cmd/ls.go index 7287d61..a7975e1 100644 --- a/cmd/ls.go +++ b/cmd/ls.go @@ -9,20 +9,23 @@ import ( "time" "github.com/jedib0t/go-pretty/v6/table" + "github.com/robertranjan/kpcli/lib/models" + "github.com/robertranjan/kpcli/lib/utils" "github.com/tobischo/gokeepasslib/v3" ) -func (d *db) Unlock() error { +func (d *kdbx) Unlock() error { + log.Debugf("options: %#v\n", d.Options) file, err := os.Open(d.Options.Database) if err != nil { return fmt.Errorf("failed open database %q file: %v", d.Options.Database, err) } db := gokeepasslib.NewDatabase(gokeepasslib.WithDatabaseKDBXVersion4()) - if err := d.generateCredentials(); err != nil { + if err := client.generateCredentials(); err != nil { return err } - db.Credentials = d.Credentials + db.Credentials = client.Credentials if err := gokeepasslib.NewDecoder(file).Decode(db); err != nil { log.Error("failed to decode dbfile: ", d.Options.Database, " err:", err) @@ -40,7 +43,7 @@ func (d *db) Unlock() error { return nil } -func (d *db) FetchDBEntries() { +func (d *kdbx) FetchDBEntries() { for _, rootgp := range d.RawData.Content.Root.Groups { for _, grp := range rootgp.Groups { d.FetchGrpEntries(grp) @@ -49,7 +52,7 @@ func (d *db) FetchDBEntries() { } } -func (d *db) FetchGrpEntries(grp gokeepasslib.Group) { +func (d *kdbx) FetchGrpEntries(grp gokeepasslib.Group) { for _, e := range grp.Entries { kv := make(map[string]string) for _, entry := range e.Values { @@ -60,7 +63,7 @@ func (d *db) FetchGrpEntries(grp gokeepasslib.Group) { if len(e.Histories) > 0 { hist = len(e.Histories[0].Entries) } - et := Interested{ + et := models.Interested{ Title: grp.Name + "/" + strings.TrimSpace(e.GetTitle()), User: strings.TrimSpace(e.GetContent("UserName")), Pass: e.GetPassword(), @@ -77,7 +80,7 @@ func (d *db) FetchGrpEntries(grp gokeepasslib.Group) { } } -func (d *db) Display() { +func (d *kdbx) Display() { t := d.getTable() t = d.updateTableWithSelectedEntries(t) @@ -124,22 +127,22 @@ func (d *db) Display() { } } -func (d *db) List() error { - d.SelectedEntries = d.Entries - d.Display() - if d.Options.Quite { +func (db *kdbx) List() error { + db.SelectedEntries = db.Entries + db.Display() + if db.Options.Quite { return nil } - if !d.Options.NoKey { + if !db.Options.NoKey { fmt.Printf("%sThis command used: \n\tkeyfile: %s\n\tdatabase: %s\n", - colorGreen, d.Options.Key, d.Options.Database) + utils.ColorGreen, db.Options.Key, db.Options.Database) } else { fmt.Printf("%sThis command used: \n\tdatabase: %s\n", - colorGreen, d.Options.Database) + utils.ColorGreen, db.Options.Database) } fmt.Printf("\nShowing %v of %v total entries%s\n", - len(d.SelectedEntries), len(d.Entries), colorReset) + len(db.SelectedEntries), len(db.Entries), utils.ColorReset) return nil } @@ -162,9 +165,9 @@ func cacheFile(t table.Writer, cacheFilename string) { // log.Printf("wrote cachefile: %v for options: %#v", d.Options.CacheFile, d.Options.String()) } -func (d *db) updateTableWithSelectedEntries(t table.Writer) table.Writer { +func (d *kdbx) updateTableWithSelectedEntries(t table.Writer) table.Writer { if d.Options.Days > 0 { - var newSelItems []Interested + var newSelItems []models.Interested for _, ent := range d.SelectedEntries { if ent.Created.After(time.Now().AddDate(0, 0, -1*d.Options.Days)) || ent.Modified.After(time.Now().AddDate(0, 0, -1*d.Options.Days)) { @@ -195,7 +198,7 @@ func (d *db) updateTableWithSelectedEntries(t table.Writer) table.Writer { return t } -func (d *db) getTable() table.Writer { +func (d *kdbx) getTable() table.Writer { t := table.NewWriter() t.SetOutputMirror(os.Stdout) diff --git a/cmd/struct.go b/cmd/struct.go deleted file mode 100644 index c11b7b7..0000000 --- a/cmd/struct.go +++ /dev/null @@ -1,143 +0,0 @@ -package cmd - -import ( - "encoding/json" - "fmt" - "os" - "time" - - "github.com/pelletier/go-toml" - "github.com/tobischo/gokeepasslib/v3" -) - -type Config struct { - Notify - Create - DiffCfg -} - -func (c *Config) String() string { - s, err := json.MarshalIndent(c, "", " ") - if err != nil { - return "failed to marshal config" - } - return string(s) -} - -func (c *Config) loadFromFile(filename string) error { - - // Check if the keyfile exists - if _, err := os.Stat(filename); os.IsNotExist(err) { - return fmt.Errorf("%v file does not exist", filename) - } - - b, err := os.ReadFile(filename) - if err != nil { - panic(err) - } - - if err = toml.Unmarshal(b, &config); err != nil { - return fmt.Errorf("unmarshall failed") - } - - return nil -} - -type DiffCfg struct { - Database1 string `toml:"database1"` - Database2 string `toml:"database2"` - Keyfile1 string `toml:"keyfile1"` - Keyfile2 string `toml:"keyfile2"` - OutputFilename string `toml:"outputFilename"` - Password1 string `toml:"password1"` - Password2 string `toml:"password2"` -} - -type Notify struct { - EmailContent string `toml:"emailContent"` - From string `toml:"from"` - EmailPassword string `toml:"emailPassword"` - SMTPHost string `toml:"smtpHost"` - SMTPPort int `toml:"smtpPort"` - Subject string `toml:"subject"` - To []string `toml:"to"` -} - -type Create struct { - Databaese string `toml:"databaese"` - Keyfile string `toml:"keyfile"` - Password string `toml:"password"` -} - -type Diff struct { - ToDBOption *Options - FromDBOption *Options - options *Options - OutputFilename string -} - -type db struct { - Entries []Interested - Options *Options - SelectedEntries []Interested - RawData *gokeepasslib.Database - Credentials *gokeepasslib.DBCredentials - // V *viper.Viper -} - -func (o *Options) String() string { - d, err := json.MarshalIndent(o, "", " ") - if err != nil { - log.Debugf("failed to marshal option, err: %v", err) - } - return string(d) -} - -// Options holds the cli options -type Options struct { - //diff - BackupDIR string - CacheFile string - Database string - Database2 string - DiffCalling bool - Pass string - Pass2 string - OutputFilename string - OutputFormat string - Notify bool - - // add entry - EntryPass string - EntryTitle string - EntryUser string - - // ls - Days int - Fields string - Key string - Key2 string - Reverse bool - Sort string - SortbyCol int - - // common - Config string - LogLevel string - Quite bool - - // create db - NoKey bool - SampleEntries int -} - -type Interested struct { - Created time.Time - Histories int - KeyValues map[string]string - Modified time.Time - Pass string - Tags string - Title string - User string -} diff --git a/lib/config/configs.go b/lib/config/configs.go new file mode 100644 index 0000000..3281cf7 --- /dev/null +++ b/lib/config/configs.go @@ -0,0 +1,93 @@ +package config + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/pelletier/go-toml" + "github.com/robertranjan/kpcli/lib/utils" +) + +type Config struct { + Notify + Create + DiffCfg +} + +type DiffCfg struct { + Database1 string `toml:"database1"` + Database2 string `toml:"database2"` + Keyfile1 string `toml:"keyfile1"` + Keyfile2 string `toml:"keyfile2"` + OutputFilename string `toml:"outputFilename"` + Password1 string `toml:"password1"` + Password2 string `toml:"password2"` +} + +type Notify struct { + EmailContent string `toml:"emailContent"` + From string `toml:"from"` + EmailPassword string `toml:"emailPassword"` + SMTPHost string `toml:"smtpHost"` + SMTPPort int `toml:"smtpPort"` + Subject string `toml:"subject"` + To []string `toml:"to"` +} + +type Create struct { + Databaese string `toml:"databaese"` + Keyfile string `toml:"keyfile"` + Password string `toml:"password"` +} + +func New(filename string) (*Config, error) { + // Check if the keyfile exists + if utils.IsFileNotExist(filename) { + return nil, fmt.Errorf("%v file does not exist", filename) + } + + b, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + + var c Config + if err = toml.Unmarshal(b, &c); err != nil { + return nil, fmt.Errorf("unmarshall failed") + } + return &c, nil +} + +func (c *Config) String() string { + s, err := json.MarshalIndent(c, "", " ") + if err != nil { + return "failed to marshal config" + } + return string(s) +} + +var SampleConfig = ` +[notify] +emailContent = "will be generated during execution" +emailPassword = "keepass_gmail_app_password" +from = "yourEmail@gmail.com" +smtpHost = "smtp.gmail.com" +smtpPort = 587 +subject = "here are the KDBX changes since last backup!" +to = ["yourEmail@gmail.com", "email2@domain.com"] + +[create] +database = "./tmp/master-db.kdbx" +keyfile = "./tmp/master-db.key" +password = "super_s3cr3t" + +[diffCfg] +database1 = "./tmp/database1" +database2 = "./tmp/database2" +keyfile1 = "./tmp/keyfile1" +keyfile2 = "./tmp/keyfile2" +outputFilename = "diffLog2Email.html" +password1 = "super_secret" +password2 = "super_secret" +` diff --git a/lib/models/options.go b/lib/models/options.go new file mode 100644 index 0000000..298cb5b --- /dev/null +++ b/lib/models/options.go @@ -0,0 +1,79 @@ +package models + +import ( + "encoding/json" + "time" + + log "github.com/sirupsen/logrus" + "github.com/tobischo/gokeepasslib/v3" +) + +type Client struct { + Options *Options + Credentials *gokeepasslib.DBCredentials + CredentialFile string +} + +// Options holds the cli options +type Options struct { + //diff + BackupDIR string + CacheFile string // ls + Database string + Database2 string + DiffCalling bool + Notify bool + OutputFilename string + Pass string + Pass2 string + + // add entry + EntryPass string + EntryTitle string + EntryUser string + + // ls + Days int + Fields string + Key string + Key2 string + Reverse bool + Sort string + SortbyCol int + + // common + Config string + LogLevel string + OutputFormat string // ls,diff + Quite bool // ls + + // create db + NoKey bool + SampleEntries int +} + +type Interested struct { + Created time.Time + Histories int + KeyValues map[string]string + Modified time.Time + Pass string + Tags string + Title string + User string +} + +type Diff struct { + ToDBOption *Options + FromDBOption *Options + Options *Options + OutputFilename string +} + +func (o *Options) String() string { + d, err := json.MarshalIndent(o, "", " ") + if err != nil { + log.Debugf("failed to marshal option, err: %v", err) + } + return string(d) +} diff --git a/cmd/notify.go b/lib/utils/utils.go similarity index 68% rename from cmd/notify.go rename to lib/utils/utils.go index fd938a3..3ca4a84 100644 --- a/cmd/notify.go +++ b/lib/utils/utils.go @@ -1,13 +1,36 @@ -package cmd +package utils import ( "fmt" - // "log" + "log" "os" gomail "gopkg.in/gomail.v2" ) +func IsFileExist(filename string) bool { + // Check if the keyfile exists + if _, err := os.Stat(filename); err == nil { + return true + } + return false +} + +func IsFileNotExist(filename string) bool { + // Check if the keyfile not exists + if _, err := os.Stat(filename); os.IsNotExist(err) { + return true + } + return false +} + +var ( + ColorGreen = "\033[32m" + ColorReset = "\033[0m" + ColorRed = "\033[31m" + ColorYellow = "\033[33m" +) + // TODO: below configs should moved out to a config file var ( // Configuration @@ -20,7 +43,7 @@ var ( emailContent = "will be generated during execution" ) -func (d *Diff) Notify(contentFile string) { +func Notify(contentFile string) { emailContentByte, err := os.ReadFile(contentFile) if err != nil {