From 15147e8d9b272cedba7ad08ec778daebe896d7cc Mon Sep 17 00:00:00 2001 From: Anand Date: Sun, 12 Dec 2021 23:57:41 +0530 Subject: [PATCH 01/27] ref issue #6 - Allow custom fields --- actions.go | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 2 deletions(-) diff --git a/actions.go b/actions.go index e6db91c..e413c78 100644 --- a/actions.go +++ b/actions.go @@ -15,6 +15,11 @@ import ( "syscall" ) +type CustomEntry struct { + fieldName string + fieldValue string +} + // Wrappers (closures) for functions accepting strings as input for in/out encryption func WrapperMaxKryptStringFunc(fn actionFunc) actionFunc { @@ -219,6 +224,7 @@ func addNewEntry() error { var notes string var passwd string var err error + var customEntries []CustomEntry if err = checkActiveDatabase(); err != nil { return err @@ -260,8 +266,10 @@ func addNewEntry() error { return errors.New("invalid input") } + customEntries = addCustomFields(reader) + // Trim spaces - err = addNewDatabaseEntry(title, userName, url, passwd, notes) + err = addNewDatabaseEntry(title, userName, url, passwd, notes, customEntries) if err != nil { fmt.Printf("Error adding entry - \"%s\"\n", err.Error()) @@ -270,6 +278,86 @@ func addNewEntry() error { return err } +// Function to update existing custom entries and add new ones +// The bool part of the return value indicates whether to take action +func addOrUpdateCustomFields(reader *bufio.Reader, entry *Entry) ([]CustomEntry, bool) { + + var customEntries []ExtendedEntry + var editedCustomEntries []CustomEntry + var newCustomEntries []CustomEntry + var flag bool + + customEntries = getExtendedEntries(entry) + + if len(customEntries) > 0 { + + fmt.Println("Editing/deleting custom fields") + for _, customEntry := range customEntries { + var fieldName string + var fieldValue string + + fmt.Println("Field Name: " + customEntry.FieldName) + fieldName = readInput(reader, "\tNew Field Name (Enter to keep, \"x\" to delete)") + if strings.ToLower(strings.TrimSpace(fieldName)) == "x" { + fmt.Println("Deleting field " + fieldName) + } else { + if strings.TrimSpace(fieldName) == "" { + fieldName = customEntry.FieldName + } + + fmt.Println("Field Value: " + customEntry.FieldValue) + fieldValue = readInput(reader, "\tNew Field Value (Enter to keep)") + if strings.TrimSpace(fieldValue) == "" { + fieldValue = customEntry.FieldValue + } + + editedCustomEntries = append(editedCustomEntries, CustomEntry{fieldName, fieldValue}) + } + } + } + + newCustomEntries = addCustomFields(reader) + + editedCustomEntries = append(editedCustomEntries, newCustomEntries...) + + // Cases where length == 0 + // 1. Existing entries - all deleted + flag = len(customEntries) > 0 || len(editedCustomEntries) > 0 + + return editedCustomEntries, flag +} + +// Function to add custom fields to an entry +func addCustomFields(reader *bufio.Reader) []CustomEntry { + + // Custom fields + var custom string + var customEntries []CustomEntry + + custom = readInput(reader, "Do you want to add custom fields [y/N]") + if strings.ToLower(custom) == "y" { + + fmt.Println("Keep entering custom field name followed by the value. Press return with no input once done.") + for true { + var customFieldName string + var customFieldValue string + + customFieldName = strings.TrimSpace(readInput(reader, "Field Name")) + if customFieldName != "" { + customFieldValue = strings.TrimSpace(readInput(reader, "Value for "+customFieldName)) + } + + if customFieldName == "" && customFieldValue == "" { + break + } + + customEntries = append(customEntries, CustomEntry{customFieldName, customFieldValue}) + } + } + + return customEntries +} + // Edit a current entry by id func editCurrentEntry(idString string) error { @@ -322,8 +410,10 @@ func editCurrentEntry(idString string) error { fmt.Printf("\nCurrent Notes: %s\n", entry.Notes) notes = readInput(reader, "New Notes") + customEntries, flag := addOrUpdateCustomFields(reader, entry) + // Update - err = updateDatabaseEntry(entry, title, userName, url, passwd, notes) + err = updateDatabaseEntry(entry, title, userName, url, passwd, notes, customEntries, flag) if err != nil { fmt.Printf("Error updating entry - \"%s\"\n", err.Error()) } From b9386fd1e7a9c4eef42b79badfcb184be1b55d6b Mon Sep 17 00:00:00 2001 From: Anand Date: Mon, 13 Dec 2021 00:00:01 +0530 Subject: [PATCH 02/27] ref issue #6 - custom fields --- db.go | 114 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 111 insertions(+), 3 deletions(-) diff --git a/db.go b/db.go index 9992591..423085f 100644 --- a/db.go +++ b/db.go @@ -29,6 +29,21 @@ func (e *Entry) TableName() string { return "entries" } +// Structure representing an extended entry in the db - for custom fields +type ExtendedEntry struct { + ID int `gorm:"column:id;autoIncrement;primaryKey"` + FieldName string `gorm:"column:field_name"` + FieldValue string `gorm:"column:field_value"` + Timestamp time.Time `gorm:"type:timestamp;default:(datetime('now','localtime'))"` // sqlite3 + + Entry Entry `gorm:"foreignKey:EntryID"` + EntryID int +} + +func (ex *ExtendedEntry) TableName() string { + return "exentries" +} + // Clone an entry func (e1 *Entry) Copy(e2 *Entry) { @@ -55,6 +70,11 @@ func createNewEntry(db *gorm.DB) error { return db.AutoMigrate(&Entry{}) } +// Create a new table for Extended Entries in the database +func createNewExEntry(db *gorm.DB) error { + return db.AutoMigrate(&ExtendedEntry{}) +} + // Init new database including tables func initNewDatabase(dbPath string) error { @@ -94,6 +114,12 @@ func initNewDatabase(dbPath string) error { return err } + err = createNewExEntry(db) + if err != nil { + fmt.Printf("Error creating schema - \"%s\"\n", err.Error()) + return err + } + fmt.Printf("Created new database - %s\n", dbPath) // Update config @@ -133,8 +159,67 @@ func openActiveDatabase() (error, *gorm.DB) { return nil, db } +// Add custom entries to a database entry +func addCustomEntries(db *gorm.DB, entry *Entry, customEntries []CustomEntry) error { + + var count int + var err error + + err = createNewExEntry(db) + if err != nil { + fmt.Printf("Error creating schema - \"%s\"\n", err.Error()) + return err + } + + for _, customEntry := range customEntries { + var exEntry ExtendedEntry + + exEntry = ExtendedEntry{FieldName: customEntry.fieldName, FieldValue: customEntry.fieldValue, + EntryID: entry.ID} + + resultEx := db.Create(&exEntry) + if resultEx.Error == nil && resultEx.RowsAffected == 1 { + count += 1 + } + } + + fmt.Printf("Created %d custom entries for entry: %d.\n", count, entry.ID) + return nil +} + +// Replace custom entries to a database entry (Drop existing and add fresh) +func replaceCustomEntries(db *gorm.DB, entry *Entry, updatedEntries []CustomEntry) error { + + var count int + var err error + var customEntries []ExtendedEntry + + err = createNewExEntry(db) + if err != nil { + fmt.Printf("Error creating schema - \"%s\"\n", err.Error()) + return err + } + + db.Where("entry_id = ?", entry.ID).Delete(&customEntries) + + for _, customEntry := range updatedEntries { + var exEntry ExtendedEntry + + exEntry = ExtendedEntry{FieldName: customEntry.fieldName, FieldValue: customEntry.fieldValue, + EntryID: entry.ID} + + resultEx := db.Create(&exEntry) + if resultEx.Error == nil && resultEx.RowsAffected == 1 { + count += 1 + } + } + + fmt.Printf("Created %d custom entries for entry: %d.\n", count, entry.ID) + return nil +} + // Add a new entry to current database -func addNewDatabaseEntry(title, userName, url, passwd, notes string) error { +func addNewDatabaseEntry(title, userName, url, passwd, notes string, customEntries []CustomEntry) error { var entry Entry var err error @@ -147,7 +232,11 @@ func addNewDatabaseEntry(title, userName, url, passwd, notes string) error { // result := db.Debug().Create(&entry) result := db.Create(&entry) if result.Error == nil && result.RowsAffected == 1 { + // Add custom fields if given fmt.Printf("Created new entry with id: %d.\n", entry.ID) + if len(customEntries) > 0 { + return addCustomEntries(db, &entry, customEntries) + } return nil } else if result.Error != nil { return result.Error @@ -158,7 +247,7 @@ func addNewDatabaseEntry(title, userName, url, passwd, notes string) error { } // Update current database entry with new values -func updateDatabaseEntry(entry *Entry, title, userName, url, passwd, notes string) error { +func updateDatabaseEntry(entry *Entry, title, userName, url, passwd, notes string, customEntries []CustomEntry, flag bool) error { var updateMap map[string]interface{} @@ -172,7 +261,7 @@ func updateDatabaseEntry(entry *Entry, title, userName, url, passwd, notes strin } } - if len(updateMap) == 0 { + if len(updateMap) == 0 && !flag { fmt.Printf("Nothing to update\n") return nil } @@ -188,6 +277,9 @@ func updateDatabaseEntry(entry *Entry, title, userName, url, passwd, notes strin return result.Error } + if flag { + replaceCustomEntries(db, entry, customEntries) + } fmt.Println("Updated entry.") return nil } @@ -354,3 +446,19 @@ func entriesToStringArray(skipLongFields bool) (error, [][]string) { return err, dataArray } + +// Get extended entries associated to an entry +func getExtendedEntries(entry *Entry) []ExtendedEntry { + + var err error + var db *gorm.DB + var customEntries []ExtendedEntry + + err, db = openActiveDatabase() + + if err == nil && db != nil { + db.Where("entry_id = ?", entry.ID).Find(&customEntries) + } + + return customEntries +} From ccc29313061c79544f484242021741d5a3778031 Mon Sep 17 00:00:00 2001 From: Anand Date: Mon, 13 Dec 2021 00:00:39 +0530 Subject: [PATCH 03/27] ref issue #9 - minor fix in cmd line --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 6f90f87..0df1d8f 100644 --- a/main.go +++ b/main.go @@ -185,7 +185,7 @@ func initializeCmdLine(parser *argparse.Parser) map[string]interface{} { {"A", "add", "Add a new entry", "", ""}, {"p", "path", "Show current database path", "", ""}, {"a", "list-all", "List all entries in current database", "", ""}, - {"g", "genpass", "Generate a strong password of length from 12 - 16", "", ""}, + {"g", "genpass", "Generate a strong password (length: 12 - 16)", "", ""}, {"s", "show", "Show passwords when listing entries", "", ""}, {"c", "copy", "Copy password to clipboard", "", ""}, {"v", "version", "Show version information and exit", "", ""}, From 3cb75a3751809441919e5a0ff7a8259418f68cfa Mon Sep 17 00:00:00 2001 From: Anand Date: Mon, 13 Dec 2021 00:01:04 +0530 Subject: [PATCH 04/27] ref issue #6 - print custom fields --- utils.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/utils.go b/utils.go index dab41f7..440cad3 100644 --- a/utils.go +++ b/utils.go @@ -283,6 +283,7 @@ func printEntry(entry *Entry, delim bool) error { var err error var settings *Settings + var customEntries []ExtendedEntry err, settings = getOrCreateLocalConfig(APP) @@ -316,7 +317,17 @@ func printEntry(entry *Entry, delim bool) error { fmt.Printf("Password: %s\n", strings.Join(asterisks, "")) } fmt.Printf("Notes: %s\n", entry.Notes) + // Query extended entries + customEntries = getExtendedEntries(entry) + + if len(customEntries) > 0 { + for _, customEntry := range customEntries { + fmt.Printf("%s: %s\n", customEntry.FieldName, customEntry.FieldValue) + } + } + fmt.Printf("Modified: %s\n", entry.Timestamp.Format("2006-06-02 15:04:05")) + printDelim(settings.Delim, settings.Color) // Reset From 227d8d014a022d770c9d626fef3ebfdafd3faaf7 Mon Sep 17 00:00:00 2001 From: Anand Date: Mon, 13 Dec 2021 00:18:47 +0530 Subject: [PATCH 05/27] ref issue #6 - Updated README for custom fields --- README.md | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 72 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ed0ead8..e30b918 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,7 @@ The password database is created and is active now. You can start adding entries Password (enter to generate new): Generating password ...done Notes: Website uses Nginx auth + Do you want to add custom fields [y/N]: Created new entry with id: 1 You can now list the entry with one of the list options. @@ -163,6 +164,36 @@ You can now list the entry with one of the list options. Modified: 2021-21-09 23:12:35 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +## Add an entry with custom fields + +From version 0.3 onwards, custom fields are supported. + + $ varuh -A + Title: Github token + URL: https://github.com/mydev/myproject + Username: mydev + Password (enter to generate new): ghp_ipQrStuVwxYz1a2b3cdEF10ghI689kLaMnOp + Notes: Never Expires + Do you want to add custom fields [y/N]: y + Field Name: Domain + Value for Domain: github.com + Field Name: Type + Value for Type: Auth Token + Field Name: + Created new entry with id: 6 + + $ varuh -l 6 + ID: 6 + Title: Github token + User: mydev + URL: https://github.com/mydev/myproject + Password: ghp_ipQrStuVwxYz1a2b3cdEF10ghI689kLaMnOp + Notes: Never Expires + Domain: github.com + Type: Auth Token + Modified: 2021-21-13 00:07:18 + + For more on listing see the [Listing and Searching](#listing-and-searching) section below. ## Edit an entry @@ -178,6 +209,7 @@ For more on listing see the [Listing and Searching](#listing-and-searching) sect New Password ([y/Y] to generate new, enter will keep old one): Current Notes: Website uses Nginx auth New Notes: Website uses Apache + Do you want to add custom fields [y/N]: Updated entry. $ varuh -l 1 -s @@ -191,6 +223,42 @@ For more on listing see the [Listing and Searching](#listing-and-searching) sect Modified: 2021-21-09 23:15:29 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +## Edit an entry with custom fields + +When you edit an entry with custom fields, you get the option to change the name of the fields or delete the fields entirely. + + $ varuh -E 6 + Current Title: Github token + New Title: + Current URL: https://github.com/mydev/myproject + New URL: + Current Username: mydev + New Username: + Current Password: ghp_ipQrStuVwxYz1a2b3cdEF10ghI689kLaMnOp + New Password ([y/Y] to generate new, enter will keep old one): + Current Notes: Never Expires + New Notes: + Editing/deleting custom fields + Field Name: Domain + New Field Name (Enter to keep, "x" to delete): x + Field Name: Type + New Field Name (Enter to keep, "x" to delete): Token Type + Field Value: Auth Token + New Field Value (Enter to keep): + Do you want to add custom fields [y/N]: + Created 1 custom entries for entry: 21. + Updated entry. + + $ varuh -l 6 -s + ID: 6 + Title: Github token + User: mydev + URL: https://github.com/mydev/myproject + Password: ghp_ipQrStuVwxYz1a2b3cdEF10ghI689kLaMnOp + Notes: Never Expires + Token Type: Auth Token + Modified: 2021-21-13 00:16:41 + (*-s* turns on visible passwords) ## Clone an entry @@ -198,7 +266,7 @@ For more on listing see the [Listing and Searching](#listing-and-searching) sect To clone (copy) an entry, $ $ varuh -C 1 - Cloned to new entry, id: 2 + Cloned to new entry, id: 3 ## Remove an entry @@ -207,8 +275,8 @@ To clone (copy) an entry, It is an error if the id does not exist. - $ varuh -R 3 - No entry with id 3 was found + $ varuh -R 4 + No entry with id 4 was found ## Switch to a new database @@ -260,7 +328,7 @@ Manually decrypt the database using `-d` option. Now the database is active again and you can see the listings. - $ varuh -l 2 + $ varuh -l 3 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ID: 2 Title: My Blog Login From 61b95cce7521ffae0c36fb43836db52fff50a3b6 Mon Sep 17 00:00:00 2001 From: Anand Date: Mon, 13 Dec 2021 23:55:30 +0530 Subject: [PATCH 06/27] ref issue #21 - Removal also accepts a range of ids --- actions.go | 74 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 58 insertions(+), 16 deletions(-) diff --git a/actions.go b/actions.go index e413c78..f4b8153 100644 --- a/actions.go +++ b/actions.go @@ -248,7 +248,7 @@ func addNewEntry() error { err, passwd = generateStrongPassword() fmt.Printf("done") } - // fmt.Printf("Password => %s\n", passwd) + // fmt.Printf("Password => %s\n", passwd) notes = readInput(reader, "\nNotes") @@ -405,7 +405,7 @@ func editCurrentEntry(idString string) error { fmt.Printf("\nGenerating new password ...") err, passwd = generateStrongPassword() } - // fmt.Printf("Password => %s\n", passwd) + // fmt.Printf("Password => %s\n", passwd) fmt.Printf("\nCurrent Notes: %s\n", entry.Notes) notes = readInput(reader, "New Notes") @@ -434,7 +434,7 @@ func listCurrentEntry(idString string) error { id, _ = strconv.Atoi(idString) - // fmt.Printf("Listing current entry - %d\n", id) + // fmt.Printf("Listing current entry - %d\n", id) err, entry = getEntryById(id) if err != nil || entry == nil { fmt.Printf("No entry found for id %d\n", id) @@ -444,7 +444,7 @@ func listCurrentEntry(idString string) error { err = printEntry(entry, true) if err == nil && settingsRider.CopyPassword { - // fmt.Printf("Copying password " + entry.Password + " to clipboard\n") + // fmt.Printf("Copying password " + entry.Password + " to clipboard\n") copyPasswordToClipboard(entry.Password) } @@ -547,17 +547,51 @@ func findCurrentEntry(term string) error { return err } +// Remove a range of entries - say 10-14 +func removeMultipleEntries(idRangeEntry string) error { + + var err error + var idRange []string + var id1, id2 int + + idRange = strings.Split(idRangeEntry, "-") + + if len(idRange) != 2 { + fmt.Println("Invalid id range - " + idRangeEntry) + return errors.New("Invalid id range - " + idRangeEntry) + } + + id1, _ = strconv.Atoi(idRange[0]) + id2, _ = strconv.Atoi(idRange[1]) + + if id1 >= id2 { + fmt.Println("Invalid id range - " + idRangeEntry) + return errors.New("Invalid id range - " + idRangeEntry) + } + + for idNum := id1; idNum <= id2; idNum++ { + err = removeCurrentEntry(fmt.Sprintf("%d", idNum)) + } + + return err +} + // Remove current entry by id func removeCurrentEntry(idString string) error { var err error var entry *Entry var id int + var response string if err = checkActiveDatabase(); err != nil { return err } + if strings.Contains(idString, "-") { + return removeMultipleEntries(idString) + } + id, _ = strconv.Atoi(idString) err, entry = getEntryById(id) @@ -566,10 +600,18 @@ func removeCurrentEntry(idString string) error { return err } - // Drop from the database - err = removeDatabaseEntry(entry) - if err == nil { - fmt.Printf("Entry with id %d was removed from the database\n", id) + printEntryMinimal(entry, true) + + response = readInput(bufio.NewReader(os.Stdin), "Please confirm removal [Y/n]: ") + + if strings.ToLower(response) != "n" { + // Drop from the database + err = removeDatabaseEntry(entry) + if err == nil { + fmt.Printf("Entry with id %d was removed from the database\n", id) + } + } else { + fmt.Println("Removal of entry canceled by user.") } return err @@ -655,7 +697,7 @@ func encryptDatabase(dbPath string, givenPasswd *string) error { } } - // err = encryptFileAES(dbPath, passwd) + // err = encryptFileAES(dbPath, passwd) _, settings := getOrCreateLocalConfig(APP) switch settings.Cipher { @@ -796,7 +838,7 @@ func exportToMarkdown(fileName string) error { } } - // fmt.Printf("%+v\n", maxLengths) + // fmt.Printf("%+v\n", maxLengths) fh, err = os.Create(fileName) if err != nil { fmt.Printf("Cannt open \"%s\" for writing - \"%s\"\n", fileName, err.Error()) @@ -810,7 +852,7 @@ func exportToMarkdown(fileName string) error { // Write markdown header for idx, length := range maxLengths { delta := length - len(headers[idx]) - // fmt.Printf("%d\n", delta) + // fmt.Printf("%d\n", delta) if delta > 0 { for i := 0; i < delta+2; i++ { headers[idx] += " " @@ -872,7 +914,7 @@ func exportToPDF(fileName string) error { } tmpFile = randomFileName(os.TempDir(), ".tmp") - // fmt.Printf("Temp file => %s\n", tmpFile) + // fmt.Printf("Temp file => %s\n", tmpFile) err = exportToMarkdownLimited(tmpFile) if err == nil { @@ -889,7 +931,7 @@ func exportToPDF(fileName string) error { if pdfTkFound && len(passwd) > 0 { tmpFile = randomFileName(".", ".pdf") - // fmt.Printf("pdf file => %s\n", tmpFile) + // fmt.Printf("pdf file => %s\n", tmpFile) args = []string{fileName, "output", tmpFile, "user_pw", passwd} cmd = exec.Command("pdftk", args...) _, err = cmd.Output() @@ -935,7 +977,7 @@ func exportToMarkdownLimited(fileName string) error { } } - // fmt.Printf("%+v\n", maxLengths) + // fmt.Printf("%+v\n", maxLengths) fh, err = os.Create(fileName) if err != nil { fmt.Printf("Cannt open \"%s\" for writing - \"%s\"\n", fileName, err.Error()) @@ -949,7 +991,7 @@ func exportToMarkdownLimited(fileName string) error { // Write markdown header for idx, length := range maxLengths { delta := length - len(headers[idx]) - // fmt.Printf("%d\n", delta) + // fmt.Printf("%d\n", delta) if delta > 0 { for i := 0; i < delta+2; i++ { headers[idx] += " " @@ -1000,7 +1042,7 @@ func exportToHTML(fileName string) error { return err } - // fmt.Printf("%+v\n", maxLengths) + // fmt.Printf("%+v\n", maxLengths) fh, err = os.Create(fileName) if err != nil { fmt.Printf("Cannt open \"%s\" for writing - \"%s\"\n", fileName, err.Error()) From 91ff95a2a1aba8f24f65c3cdcedc053252f25771 Mon Sep 17 00:00:00 2001 From: Anand Date: Mon, 13 Dec 2021 23:56:14 +0530 Subject: [PATCH 07/27] ref issue #21 - Addded printEntryMinimal --- utils.go | 42 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/utils.go b/utils.go index 440cad3..abd84f7 100644 --- a/utils.go +++ b/utils.go @@ -104,7 +104,7 @@ func getOrCreateLocalConfig(app string) (error, *Settings) { } configFile = filepath.Join(configPath, "config.json") - // fmt.Printf("Config file, path => %s %s\n", configFile, configPath) + // fmt.Printf("Config file, path => %s %s\n", configFile, configPath) if _, err = os.Stat(configFile); err == nil { fh, err = os.Open(configFile) @@ -121,7 +121,7 @@ func getOrCreateLocalConfig(app string) (error, *Settings) { } } else { - // fmt.Printf("Creating default configuration ...") + // fmt.Printf("Creating default configuration ...") settings = Settings{"", "aes", true, true, false, configFile, "id,asc", "+", "default", "bgblack"} if err = writeSettings(&settings, configFile); err == nil { @@ -337,6 +337,42 @@ func printEntry(entry *Entry, delim bool) error { } +// Print an entry to the console with minimal data +func printEntryMinimal(entry *Entry, delim bool) error { + + var err error + var settings *Settings + + err, settings = getOrCreateLocalConfig(APP) + + if err != nil { + fmt.Printf("Error parsing config - \"%s\"\n", err.Error()) + return err + } + + fmt.Printf("%s", getColor(strings.ToLower(settings.Color))) + if strings.HasPrefix(settings.BgColor, "bg") { + fmt.Printf("%s", getColor(strings.ToLower(settings.BgColor))) + } + + if delim { + printDelim(settings.Delim, settings.Color) + } + + fmt.Printf("Title: %s\n", entry.Title) + fmt.Printf("User: %s\n", entry.User) + fmt.Printf("URL: %s\n", entry.Url) + fmt.Printf("Modified: %s\n", entry.Timestamp.Format("2006-06-02 15:04:05")) + + printDelim(settings.Delim, settings.Color) + + // Reset + fmt.Printf("%s", getColor("default")) + + return nil + +} + // Read user input and return entered value func readInput(reader *bufio.Reader, prompt string) string { @@ -397,7 +433,7 @@ func isActiveDatabaseEncryptedAndMaxKryptOn() (bool, string) { // (Temporarily) enable showing of passwords func setShowPasswords() error { - // fmt.Printf("Setting show passwords to true\n") + // fmt.Printf("Setting show passwords to true\n") settingsRider.ShowPasswords = true return nil } From 0bbb983b32f25d4aec20da0417e30224a6bb21a4 Mon Sep 17 00:00:00 2001 From: Anand Date: Tue, 14 Dec 2021 10:15:13 +0530 Subject: [PATCH 08/27] ref issue #21 canceled -> cancelled --- actions.go | 1796 ++++++++++++++++++++++++++-------------------------- 1 file changed, 898 insertions(+), 898 deletions(-) diff --git a/actions.go b/actions.go index f4b8153..9bec025 100644 --- a/actions.go +++ b/actions.go @@ -2,1127 +2,1127 @@ package main import ( - "bufio" - "encoding/csv" - "errors" - "fmt" - "os" - "os/exec" - "os/signal" - "path/filepath" - "strconv" - "strings" - "syscall" + "bufio" + "encoding/csv" + "errors" + "fmt" + "os" + "os/exec" + "os/signal" + "path/filepath" + "strconv" + "strings" + "syscall" ) type CustomEntry struct { - fieldName string - fieldValue string + fieldName string + fieldValue string } // Wrappers (closures) for functions accepting strings as input for in/out encryption func WrapperMaxKryptStringFunc(fn actionFunc) actionFunc { - return func(inputStr string) error { - var maxKrypt bool - var defaultDB string - var encPasswd string - var err error + return func(inputStr string) error { + var maxKrypt bool + var defaultDB string + var encPasswd string + var err error - maxKrypt, defaultDB = isActiveDatabaseEncryptedAndMaxKryptOn() + maxKrypt, defaultDB = isActiveDatabaseEncryptedAndMaxKryptOn() - // If max krypt on - then autodecrypt on call and auto encrypt after call - if maxKrypt { - err, encPasswd = decryptDatabase(defaultDB) - if err != nil { - return err - } + // If max krypt on - then autodecrypt on call and auto encrypt after call + if maxKrypt { + err, encPasswd = decryptDatabase(defaultDB) + if err != nil { + return err + } - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) - go func() { - sig := <-sigChan - fmt.Println("Received signal", sig) - // Reencrypt - encryptDatabase(defaultDB, &encPasswd) - os.Exit(1) - }() - } + go func() { + sig := <-sigChan + fmt.Println("Received signal", sig) + // Reencrypt + encryptDatabase(defaultDB, &encPasswd) + os.Exit(1) + }() + } - err = fn(inputStr) + err = fn(inputStr) - // If max krypt on - then autodecrypt on call and auto encrypt after call - if maxKrypt { - encryptDatabase(defaultDB, &encPasswd) - } + // If max krypt on - then autodecrypt on call and auto encrypt after call + if maxKrypt { + encryptDatabase(defaultDB, &encPasswd) + } - return err - } + return err + } } // Wrappers (closures) for functions accepting no input for in/out encryption func WrapperMaxKryptVoidFunc(fn voidFunc) voidFunc { - return func() error { - var maxKrypt bool - var defaultDB string - var encPasswd string - var err error + return func() error { + var maxKrypt bool + var defaultDB string + var encPasswd string + var err error - maxKrypt, defaultDB = isActiveDatabaseEncryptedAndMaxKryptOn() + maxKrypt, defaultDB = isActiveDatabaseEncryptedAndMaxKryptOn() - // If max krypt on - then autodecrypt on call and auto encrypt after call - if maxKrypt { - err, encPasswd = decryptDatabase(defaultDB) - if err != nil { - return err - } + // If max krypt on - then autodecrypt on call and auto encrypt after call + if maxKrypt { + err, encPasswd = decryptDatabase(defaultDB) + if err != nil { + return err + } - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) - go func() { - sig := <-sigChan - fmt.Println("Received signal", sig) - // Reencrypt - encryptDatabase(defaultDB, &encPasswd) - os.Exit(1) - }() - } + go func() { + sig := <-sigChan + fmt.Println("Received signal", sig) + // Reencrypt + encryptDatabase(defaultDB, &encPasswd) + os.Exit(1) + }() + } - err = fn() + err = fn() - // If max krypt on - then autodecrypt on call and auto encrypt after call - if maxKrypt { - encryptDatabase(defaultDB, &encPasswd) - } + // If max krypt on - then autodecrypt on call and auto encrypt after call + if maxKrypt { + encryptDatabase(defaultDB, &encPasswd) + } - return err - } + return err + } } // Print the current active database path func showActiveDatabasePath() error { - err, settings := getOrCreateLocalConfig(APP) - - if err != nil { - fmt.Printf("Error parsing config - \"%s\"\n", err.Error()) - return err - } - - if settings != nil { - if settings.ActiveDB != "" { - fmt.Printf("%s\n", settings.ActiveDB) - } else { - fmt.Println("No active database") - } - return nil - } else { - fmt.Printf("Error - null config\n") - return errors.New("null config") - } + err, settings := getOrCreateLocalConfig(APP) + + if err != nil { + fmt.Printf("Error parsing config - \"%s\"\n", err.Error()) + return err + } + + if settings != nil { + if settings.ActiveDB != "" { + fmt.Printf("%s\n", settings.ActiveDB) + } else { + fmt.Println("No active database") + } + return nil + } else { + fmt.Printf("Error - null config\n") + return errors.New("null config") + } } // Set the current active database path func setActiveDatabasePath(dbPath string) error { - var fullPath string - var activeEncrypted bool - var newEncrypted bool - - err, settings := getOrCreateLocalConfig(APP) - - if err != nil { - fmt.Printf("Error parsing config - \"%s\"\n", err.Error()) - return err - } - - if settings != nil { - var flag bool - - if _, err = os.Stat(dbPath); os.IsNotExist(err) { - fmt.Printf("Error - path %s does not exist\n", dbPath) - return err - } - - fullPath, _ = filepath.Abs(dbPath) - - if fullPath == settings.ActiveDB { - fmt.Printf("Current database is \"%s\" - nothing to do\n", fullPath) - return nil - } - - if _, flag = isFileEncrypted(settings.ActiveDB); flag { - activeEncrypted = true - } - - if _, flag = isFileEncrypted(fullPath); flag { - newEncrypted = true - } - - // If autoencrypt is true - encrypt current DB automatically - if settings.AutoEncrypt { - if !activeEncrypted { - fmt.Printf("Encrypting current active database - %s\n", settings.ActiveDB) - err = encryptActiveDatabase() - if err == nil { - activeEncrypted = true - } - } - - if newEncrypted { - // Decrypt new database if it is encrypted - fmt.Printf("Database %s is encrypted, decrypting it\n", fullPath) - err, _ = decryptDatabase(fullPath) - if err != nil { - fmt.Printf("Decryption Error - \"%s\", not switching databases\n", err.Error()) - return err - } else { - newEncrypted = false - } - } - } - - if !activeEncrypted { - // Use should manually encrypt before switching - fmt.Println("Auto-encrypt disabled, encrypt existing database before switching to new.") - return nil - } - - if newEncrypted { - // Use should manually decrypt before switching - fmt.Println("Auto-encrypt disabled, decrypt new database manually before switching.") - return nil - } - - settings.ActiveDB = fullPath - err = updateSettings(settings, settings.ConfigPath) - if err == nil { - fmt.Println("Switched active database successfully.") - } else { - fmt.Printf("Error updating settings - \"%s\"\n", err.Error()) - } - - return err - - } else { - fmt.Printf("Error - null config\n") - return errors.New("null config") - } + var fullPath string + var activeEncrypted bool + var newEncrypted bool + + err, settings := getOrCreateLocalConfig(APP) + + if err != nil { + fmt.Printf("Error parsing config - \"%s\"\n", err.Error()) + return err + } + + if settings != nil { + var flag bool + + if _, err = os.Stat(dbPath); os.IsNotExist(err) { + fmt.Printf("Error - path %s does not exist\n", dbPath) + return err + } + + fullPath, _ = filepath.Abs(dbPath) + + if fullPath == settings.ActiveDB { + fmt.Printf("Current database is \"%s\" - nothing to do\n", fullPath) + return nil + } + + if _, flag = isFileEncrypted(settings.ActiveDB); flag { + activeEncrypted = true + } + + if _, flag = isFileEncrypted(fullPath); flag { + newEncrypted = true + } + + // If autoencrypt is true - encrypt current DB automatically + if settings.AutoEncrypt { + if !activeEncrypted { + fmt.Printf("Encrypting current active database - %s\n", settings.ActiveDB) + err = encryptActiveDatabase() + if err == nil { + activeEncrypted = true + } + } + + if newEncrypted { + // Decrypt new database if it is encrypted + fmt.Printf("Database %s is encrypted, decrypting it\n", fullPath) + err, _ = decryptDatabase(fullPath) + if err != nil { + fmt.Printf("Decryption Error - \"%s\", not switching databases\n", err.Error()) + return err + } else { + newEncrypted = false + } + } + } + + if !activeEncrypted { + // Use should manually encrypt before switching + fmt.Println("Auto-encrypt disabled, encrypt existing database before switching to new.") + return nil + } + + if newEncrypted { + // Use should manually decrypt before switching + fmt.Println("Auto-encrypt disabled, decrypt new database manually before switching.") + return nil + } + + settings.ActiveDB = fullPath + err = updateSettings(settings, settings.ConfigPath) + if err == nil { + fmt.Println("Switched active database successfully.") + } else { + fmt.Printf("Error updating settings - \"%s\"\n", err.Error()) + } + + return err + + } else { + fmt.Printf("Error - null config\n") + return errors.New("null config") + } } // Text menu driven function to add a new entry func addNewEntry() error { - var userName string - var title string - var url string - var notes string - var passwd string - var err error - var customEntries []CustomEntry - - if err = checkActiveDatabase(); err != nil { - return err - } - - reader := bufio.NewReader(os.Stdin) - title = readInput(reader, "Title") - url = readInput(reader, "URL") - - if len(url) > 0 && !strings.HasPrefix(strings.ToLower(url), "http://") && !strings.HasPrefix(strings.ToLower(url), "https://") { - url = "http://" + url - } - - userName = readInput(reader, "Username") - - fmt.Printf("Password (enter to generate new): ") - err, passwd = readPassword() - - if len(passwd) == 0 { - fmt.Printf("\nGenerating password ...") - err, passwd = generateStrongPassword() - fmt.Printf("done") - } - // fmt.Printf("Password => %s\n", passwd) - - notes = readInput(reader, "\nNotes") - - // Title and username/password are mandatory - if len(title) == 0 { - fmt.Printf("Error - valid Title required\n") - return errors.New("invalid input") - } - if len(userName) == 0 { - fmt.Printf("Error - valid Username required\n") - return errors.New("invalid input") - } - if len(passwd) == 0 { - fmt.Printf("Error - valid Password required\n") - return errors.New("invalid input") - } - - customEntries = addCustomFields(reader) - - // Trim spaces - err = addNewDatabaseEntry(title, userName, url, passwd, notes, customEntries) - - if err != nil { - fmt.Printf("Error adding entry - \"%s\"\n", err.Error()) - } - - return err + var userName string + var title string + var url string + var notes string + var passwd string + var err error + var customEntries []CustomEntry + + if err = checkActiveDatabase(); err != nil { + return err + } + + reader := bufio.NewReader(os.Stdin) + title = readInput(reader, "Title") + url = readInput(reader, "URL") + + if len(url) > 0 && !strings.HasPrefix(strings.ToLower(url), "http://") && !strings.HasPrefix(strings.ToLower(url), "https://") { + url = "http://" + url + } + + userName = readInput(reader, "Username") + + fmt.Printf("Password (enter to generate new): ") + err, passwd = readPassword() + + if len(passwd) == 0 { + fmt.Printf("\nGenerating password ...") + err, passwd = generateStrongPassword() + fmt.Printf("done") + } + // fmt.Printf("Password => %s\n", passwd) + + notes = readInput(reader, "\nNotes") + + // Title and username/password are mandatory + if len(title) == 0 { + fmt.Printf("Error - valid Title required\n") + return errors.New("invalid input") + } + if len(userName) == 0 { + fmt.Printf("Error - valid Username required\n") + return errors.New("invalid input") + } + if len(passwd) == 0 { + fmt.Printf("Error - valid Password required\n") + return errors.New("invalid input") + } + + customEntries = addCustomFields(reader) + + // Trim spaces + err = addNewDatabaseEntry(title, userName, url, passwd, notes, customEntries) + + if err != nil { + fmt.Printf("Error adding entry - \"%s\"\n", err.Error()) + } + + return err } // Function to update existing custom entries and add new ones // The bool part of the return value indicates whether to take action func addOrUpdateCustomFields(reader *bufio.Reader, entry *Entry) ([]CustomEntry, bool) { - var customEntries []ExtendedEntry - var editedCustomEntries []CustomEntry - var newCustomEntries []CustomEntry - var flag bool + var customEntries []ExtendedEntry + var editedCustomEntries []CustomEntry + var newCustomEntries []CustomEntry + var flag bool - customEntries = getExtendedEntries(entry) + customEntries = getExtendedEntries(entry) - if len(customEntries) > 0 { + if len(customEntries) > 0 { - fmt.Println("Editing/deleting custom fields") - for _, customEntry := range customEntries { - var fieldName string - var fieldValue string + fmt.Println("Editing/deleting custom fields") + for _, customEntry := range customEntries { + var fieldName string + var fieldValue string - fmt.Println("Field Name: " + customEntry.FieldName) - fieldName = readInput(reader, "\tNew Field Name (Enter to keep, \"x\" to delete)") - if strings.ToLower(strings.TrimSpace(fieldName)) == "x" { - fmt.Println("Deleting field " + fieldName) - } else { - if strings.TrimSpace(fieldName) == "" { - fieldName = customEntry.FieldName - } + fmt.Println("Field Name: " + customEntry.FieldName) + fieldName = readInput(reader, "\tNew Field Name (Enter to keep, \"x\" to delete)") + if strings.ToLower(strings.TrimSpace(fieldName)) == "x" { + fmt.Println("Deleting field " + fieldName) + } else { + if strings.TrimSpace(fieldName) == "" { + fieldName = customEntry.FieldName + } - fmt.Println("Field Value: " + customEntry.FieldValue) - fieldValue = readInput(reader, "\tNew Field Value (Enter to keep)") - if strings.TrimSpace(fieldValue) == "" { - fieldValue = customEntry.FieldValue - } + fmt.Println("Field Value: " + customEntry.FieldValue) + fieldValue = readInput(reader, "\tNew Field Value (Enter to keep)") + if strings.TrimSpace(fieldValue) == "" { + fieldValue = customEntry.FieldValue + } - editedCustomEntries = append(editedCustomEntries, CustomEntry{fieldName, fieldValue}) - } - } - } + editedCustomEntries = append(editedCustomEntries, CustomEntry{fieldName, fieldValue}) + } + } + } - newCustomEntries = addCustomFields(reader) + newCustomEntries = addCustomFields(reader) - editedCustomEntries = append(editedCustomEntries, newCustomEntries...) + editedCustomEntries = append(editedCustomEntries, newCustomEntries...) - // Cases where length == 0 - // 1. Existing entries - all deleted - flag = len(customEntries) > 0 || len(editedCustomEntries) > 0 + // Cases where length == 0 + // 1. Existing entries - all deleted + flag = len(customEntries) > 0 || len(editedCustomEntries) > 0 - return editedCustomEntries, flag + return editedCustomEntries, flag } // Function to add custom fields to an entry func addCustomFields(reader *bufio.Reader) []CustomEntry { - // Custom fields - var custom string - var customEntries []CustomEntry + // Custom fields + var custom string + var customEntries []CustomEntry - custom = readInput(reader, "Do you want to add custom fields [y/N]") - if strings.ToLower(custom) == "y" { + custom = readInput(reader, "Do you want to add custom fields [y/N]") + if strings.ToLower(custom) == "y" { - fmt.Println("Keep entering custom field name followed by the value. Press return with no input once done.") - for true { - var customFieldName string - var customFieldValue string + fmt.Println("Keep entering custom field name followed by the value. Press return with no input once done.") + for true { + var customFieldName string + var customFieldValue string - customFieldName = strings.TrimSpace(readInput(reader, "Field Name")) - if customFieldName != "" { - customFieldValue = strings.TrimSpace(readInput(reader, "Value for "+customFieldName)) - } + customFieldName = strings.TrimSpace(readInput(reader, "Field Name")) + if customFieldName != "" { + customFieldValue = strings.TrimSpace(readInput(reader, "Value for "+customFieldName)) + } - if customFieldName == "" && customFieldValue == "" { - break - } + if customFieldName == "" && customFieldValue == "" { + break + } - customEntries = append(customEntries, CustomEntry{customFieldName, customFieldValue}) - } - } + customEntries = append(customEntries, CustomEntry{customFieldName, customFieldValue}) + } + } - return customEntries + return customEntries } // Edit a current entry by id func editCurrentEntry(idString string) error { - var userName string - var title string - var url string - var notes string - var passwd string - var err error - var entry *Entry - var id int + var userName string + var title string + var url string + var notes string + var passwd string + var err error + var entry *Entry + var id int - if err = checkActiveDatabase(); err != nil { - return err - } + if err = checkActiveDatabase(); err != nil { + return err + } - id, _ = strconv.Atoi(idString) + id, _ = strconv.Atoi(idString) - err, entry = getEntryById(id) - if err != nil || entry == nil { - fmt.Printf("No entry found for id %d\n", id) - return err - } + err, entry = getEntryById(id) + if err != nil || entry == nil { + fmt.Printf("No entry found for id %d\n", id) + return err + } - reader := bufio.NewReader(os.Stdin) + reader := bufio.NewReader(os.Stdin) - fmt.Printf("Current Title: %s\n", entry.Title) - title = readInput(reader, "New Title") + fmt.Printf("Current Title: %s\n", entry.Title) + title = readInput(reader, "New Title") - fmt.Printf("Current URL: %s\n", entry.Url) - url = readInput(reader, "New URL") + fmt.Printf("Current URL: %s\n", entry.Url) + url = readInput(reader, "New URL") - if len(url) > 0 && !strings.HasPrefix(strings.ToLower(url), "http://") && !strings.HasPrefix(strings.ToLower(url), "https://") { - url = "http://" + url - } + if len(url) > 0 && !strings.HasPrefix(strings.ToLower(url), "http://") && !strings.HasPrefix(strings.ToLower(url), "https://") { + url = "http://" + url + } - fmt.Printf("Current Username: %s\n", entry.User) - userName = readInput(reader, "New Username") + fmt.Printf("Current Username: %s\n", entry.User) + userName = readInput(reader, "New Username") - fmt.Printf("Current Password: %s\n", entry.Password) - fmt.Printf("New Password ([y/Y] to generate new, enter will keep old one): ") - err, passwd = readPassword() + fmt.Printf("Current Password: %s\n", entry.Password) + fmt.Printf("New Password ([y/Y] to generate new, enter will keep old one): ") + err, passwd = readPassword() - if strings.ToLower(passwd) == "y" { - fmt.Printf("\nGenerating new password ...") - err, passwd = generateStrongPassword() - } - // fmt.Printf("Password => %s\n", passwd) + if strings.ToLower(passwd) == "y" { + fmt.Printf("\nGenerating new password ...") + err, passwd = generateStrongPassword() + } + // fmt.Printf("Password => %s\n", passwd) - fmt.Printf("\nCurrent Notes: %s\n", entry.Notes) - notes = readInput(reader, "New Notes") + fmt.Printf("\nCurrent Notes: %s\n", entry.Notes) + notes = readInput(reader, "New Notes") - customEntries, flag := addOrUpdateCustomFields(reader, entry) + customEntries, flag := addOrUpdateCustomFields(reader, entry) - // Update - err = updateDatabaseEntry(entry, title, userName, url, passwd, notes, customEntries, flag) - if err != nil { - fmt.Printf("Error updating entry - \"%s\"\n", err.Error()) - } + // Update + err = updateDatabaseEntry(entry, title, userName, url, passwd, notes, customEntries, flag) + if err != nil { + fmt.Printf("Error updating entry - \"%s\"\n", err.Error()) + } - return err + return err } // List current entry by id func listCurrentEntry(idString string) error { - var id int - var err error - var entry *Entry + var id int + var err error + var entry *Entry - if err = checkActiveDatabase(); err != nil { - return err - } + if err = checkActiveDatabase(); err != nil { + return err + } - id, _ = strconv.Atoi(idString) + id, _ = strconv.Atoi(idString) - // fmt.Printf("Listing current entry - %d\n", id) - err, entry = getEntryById(id) - if err != nil || entry == nil { - fmt.Printf("No entry found for id %d\n", id) - return err - } + // fmt.Printf("Listing current entry - %d\n", id) + err, entry = getEntryById(id) + if err != nil || entry == nil { + fmt.Printf("No entry found for id %d\n", id) + return err + } - err = printEntry(entry, true) + err = printEntry(entry, true) - if err == nil && settingsRider.CopyPassword { - // fmt.Printf("Copying password " + entry.Password + " to clipboard\n") - copyPasswordToClipboard(entry.Password) - } + if err == nil && settingsRider.CopyPassword { + // fmt.Printf("Copying password " + entry.Password + " to clipboard\n") + copyPasswordToClipboard(entry.Password) + } - return err + return err } // List all entries func listAllEntries() error { - var err error - var maxKrypt bool - var defaultDB string - var passwd string - - maxKrypt, defaultDB = isActiveDatabaseEncryptedAndMaxKryptOn() - - // If max krypt on - then autodecrypt on call and auto encrypt after call - if maxKrypt { - err, passwd = decryptDatabase(defaultDB) - if err != nil { - return err - } - } - - if err = checkActiveDatabase(); err != nil { - return err - } - - err, settings := getOrCreateLocalConfig(APP) - - if err != nil { - fmt.Printf("Error parsing config - \"%s\"\n", err.Error()) - return err - } - - orderKeys := strings.Split(settings.ListOrder, ",") - err, entries := iterateEntries(orderKeys[0], orderKeys[1]) - - if err == nil { - if len(entries) > 0 { - fmt.Printf("%s", getColor(strings.ToLower(settings.Color))) - printDelim(settings.Delim, settings.Color) - for _, entry := range entries { - printEntry(&entry, false) - } - } else { - fmt.Println("No entries.") - } - } else { - fmt.Printf("Error fetching entries: \"%s\"\n", err.Error()) - return err - } - - // If max krypt on - then autodecrypt on call and auto encrypt after call - if maxKrypt { - err = encryptDatabase(defaultDB, &passwd) - } - - return err + var err error + var maxKrypt bool + var defaultDB string + var passwd string + + maxKrypt, defaultDB = isActiveDatabaseEncryptedAndMaxKryptOn() + + // If max krypt on - then autodecrypt on call and auto encrypt after call + if maxKrypt { + err, passwd = decryptDatabase(defaultDB) + if err != nil { + return err + } + } + + if err = checkActiveDatabase(); err != nil { + return err + } + + err, settings := getOrCreateLocalConfig(APP) + + if err != nil { + fmt.Printf("Error parsing config - \"%s\"\n", err.Error()) + return err + } + + orderKeys := strings.Split(settings.ListOrder, ",") + err, entries := iterateEntries(orderKeys[0], orderKeys[1]) + + if err == nil { + if len(entries) > 0 { + fmt.Printf("%s", getColor(strings.ToLower(settings.Color))) + printDelim(settings.Delim, settings.Color) + for _, entry := range entries { + printEntry(&entry, false) + } + } else { + fmt.Println("No entries.") + } + } else { + fmt.Printf("Error fetching entries: \"%s\"\n", err.Error()) + return err + } + + // If max krypt on - then autodecrypt on call and auto encrypt after call + if maxKrypt { + err = encryptDatabase(defaultDB, &passwd) + } + + return err } // Find current entry by term - prints all matches func findCurrentEntry(term string) error { - var err error - var entries []Entry - - if err = checkActiveDatabase(); err != nil { - return err - } - - err, entries = searchDatabaseEntry(term) - if err != nil || len(entries) == 0 { - fmt.Printf("Entry for query \"%s\" not found\n", term) - return err - } else { - var delim bool - var pcopy bool - - if len(entries) == 1 { - delim = true - pcopy = true - // Single entry means copy password can be enabled - } else { - _, settings := getOrCreateLocalConfig(APP) - fmt.Printf("%s", getColor(strings.ToLower(settings.Color))) - printDelim(settings.Delim, settings.Color) - } - - for _, entry := range entries { - printEntry(&entry, delim) - } - - if pcopy && settingsRider.CopyPassword { - // Single entry - copyPasswordToClipboard(entries[0].Password) - } - } - - return err + var err error + var entries []Entry + + if err = checkActiveDatabase(); err != nil { + return err + } + + err, entries = searchDatabaseEntry(term) + if err != nil || len(entries) == 0 { + fmt.Printf("Entry for query \"%s\" not found\n", term) + return err + } else { + var delim bool + var pcopy bool + + if len(entries) == 1 { + delim = true + pcopy = true + // Single entry means copy password can be enabled + } else { + _, settings := getOrCreateLocalConfig(APP) + fmt.Printf("%s", getColor(strings.ToLower(settings.Color))) + printDelim(settings.Delim, settings.Color) + } + + for _, entry := range entries { + printEntry(&entry, delim) + } + + if pcopy && settingsRider.CopyPassword { + // Single entry + copyPasswordToClipboard(entries[0].Password) + } + } + + return err } // Remove a range of entries - say 10-14 func removeMultipleEntries(idRangeEntry string) error { - var err error - var idRange []string - var id1, id2 int + var err error + var idRange []string + var id1, id2 int - idRange = strings.Split(idRangeEntry, "-") + idRange = strings.Split(idRangeEntry, "-") - if len(idRange) != 2 { - fmt.Println("Invalid id range - " + idRangeEntry) - return errors.New("Invalid id range - " + idRangeEntry) - } + if len(idRange) != 2 { + fmt.Println("Invalid id range - " + idRangeEntry) + return errors.New("Invalid id range - " + idRangeEntry) + } - id1, _ = strconv.Atoi(idRange[0]) - id2, _ = strconv.Atoi(idRange[1]) + id1, _ = strconv.Atoi(idRange[0]) + id2, _ = strconv.Atoi(idRange[1]) - if id1 >= id2 { - fmt.Println("Invalid id range - " + idRangeEntry) - return errors.New("Invalid id range - " + idRangeEntry) - } + if id1 >= id2 { + fmt.Println("Invalid id range - " + idRangeEntry) + return errors.New("Invalid id range - " + idRangeEntry) + } - for idNum := id1; idNum <= id2; idNum++ { - err = removeCurrentEntry(fmt.Sprintf("%d", idNum)) - } + for idNum := id1; idNum <= id2; idNum++ { + err = removeCurrentEntry(fmt.Sprintf("%d", idNum)) + } - return err + return err } // Remove current entry by id func removeCurrentEntry(idString string) error { - var err error - var entry *Entry - var id int - var response string + var err error + var entry *Entry + var id int + var response string - if err = checkActiveDatabase(); err != nil { - return err - } + if err = checkActiveDatabase(); err != nil { + return err + } - if strings.Contains(idString, "-") { - return removeMultipleEntries(idString) - } + if strings.Contains(idString, "-") { + return removeMultipleEntries(idString) + } - id, _ = strconv.Atoi(idString) + id, _ = strconv.Atoi(idString) - err, entry = getEntryById(id) - if err != nil || entry == nil { - fmt.Printf("No entry with id %d was found\n", id) - return err - } + err, entry = getEntryById(id) + if err != nil || entry == nil { + fmt.Printf("No entry with id %d was found\n", id) + return err + } - printEntryMinimal(entry, true) + printEntryMinimal(entry, true) - response = readInput(bufio.NewReader(os.Stdin), "Please confirm removal [Y/n]: ") + response = readInput(bufio.NewReader(os.Stdin), "Please confirm removal [Y/n]: ") - if strings.ToLower(response) != "n" { - // Drop from the database - err = removeDatabaseEntry(entry) - if err == nil { - fmt.Printf("Entry with id %d was removed from the database\n", id) - } - } else { - fmt.Println("Removal of entry canceled by user.") - } + if strings.ToLower(response) != "n" { + // Drop from the database + err = removeDatabaseEntry(entry) + if err == nil { + fmt.Printf("Entry with id %d was removed from the database\n", id) + } + } else { + fmt.Println("Removal of entry cancelled by user.") + } - return err + return err } // Copy current entry by id into new entry func copyCurrentEntry(idString string) error { - var err error - var entry *Entry - var id int + var err error + var entry *Entry + var id int - if err = checkActiveDatabase(); err != nil { - return err - } + if err = checkActiveDatabase(); err != nil { + return err + } - id, _ = strconv.Atoi(idString) + id, _ = strconv.Atoi(idString) - err, entry = getEntryById(id) - if err != nil || entry == nil { - fmt.Printf("No entry with id %d was found\n", id) - return err - } + err, entry = getEntryById(id) + if err != nil || entry == nil { + fmt.Printf("No entry with id %d was found\n", id) + return err + } - err, _ = cloneEntry(entry) - if err != nil { - fmt.Printf("Error cloning entry: \"%s\"\n", err.Error()) - return err - } + err, _ = cloneEntry(entry) + if err != nil { + fmt.Printf("Error cloning entry: \"%s\"\n", err.Error()) + return err + } - return err + return err } // Encrypt the active database func encryptActiveDatabase() error { - var err error - var dbPath string + var err error + var dbPath string - if err = checkActiveDatabase(); err != nil { - return err - } + if err = checkActiveDatabase(); err != nil { + return err + } - err, dbPath = getActiveDatabase() - if err != nil { - fmt.Printf("Error getting active database path - \"%s\"\n", err.Error()) - return err - } + err, dbPath = getActiveDatabase() + if err != nil { + fmt.Printf("Error getting active database path - \"%s\"\n", err.Error()) + return err + } - return encryptDatabase(dbPath, nil) + return encryptDatabase(dbPath, nil) } // Encrypt the database using AES func encryptDatabase(dbPath string, givenPasswd *string) error { - var err error - var passwd string - var passwd2 string - - // If password is given, use it - if givenPasswd != nil { - passwd = *givenPasswd - } - - if len(passwd) == 0 { - fmt.Printf("Password: ") - err, passwd = readPassword() - - if err == nil { - fmt.Printf("\nPassword again: ") - err, passwd2 = readPassword() - if err == nil { - if passwd != passwd2 { - fmt.Println("\nPassword mismatch.") - return errors.New("mismatched passwords") - } - } - } - - if err != nil { - fmt.Printf("Error reading password - \"%s\"\n", err.Error()) - return err - } - } - - // err = encryptFileAES(dbPath, passwd) - _, settings := getOrCreateLocalConfig(APP) - - switch settings.Cipher { - case "aes": - err = encryptFileAES(dbPath, passwd) - case "xchacha", "chacha", "xchachapoly": - err = encryptFileXChachaPoly(dbPath, passwd) - default: - fmt.Println("No cipher set, defaulting to AES") - err = encryptFileAES(dbPath, passwd) - } - - if err == nil { - fmt.Println("\nEncryption complete.") - } - - return err + var err error + var passwd string + var passwd2 string + + // If password is given, use it + if givenPasswd != nil { + passwd = *givenPasswd + } + + if len(passwd) == 0 { + fmt.Printf("Password: ") + err, passwd = readPassword() + + if err == nil { + fmt.Printf("\nPassword again: ") + err, passwd2 = readPassword() + if err == nil { + if passwd != passwd2 { + fmt.Println("\nPassword mismatch.") + return errors.New("mismatched passwords") + } + } + } + + if err != nil { + fmt.Printf("Error reading password - \"%s\"\n", err.Error()) + return err + } + } + + // err = encryptFileAES(dbPath, passwd) + _, settings := getOrCreateLocalConfig(APP) + + switch settings.Cipher { + case "aes": + err = encryptFileAES(dbPath, passwd) + case "xchacha", "chacha", "xchachapoly": + err = encryptFileXChachaPoly(dbPath, passwd) + default: + fmt.Println("No cipher set, defaulting to AES") + err = encryptFileAES(dbPath, passwd) + } + + if err == nil { + fmt.Println("\nEncryption complete.") + } + + return err } // Decrypt an encrypted database func decryptDatabase(dbPath string) (error, string) { - var err error - var passwd string - var flag bool + var err error + var passwd string + var flag bool - if err, flag = isFileEncrypted(dbPath); !flag { - fmt.Println(err.Error()) - return err, "" - } + if err, flag = isFileEncrypted(dbPath); !flag { + fmt.Println(err.Error()) + return err, "" + } - fmt.Printf("Password: ") - err, passwd = readPassword() + fmt.Printf("Password: ") + err, passwd = readPassword() - if err != nil { - fmt.Printf("\nError reading password - \"%s\"\n", err.Error()) - return err, "" - } + if err != nil { + fmt.Printf("\nError reading password - \"%s\"\n", err.Error()) + return err, "" + } - _, settings := getOrCreateLocalConfig(APP) + _, settings := getOrCreateLocalConfig(APP) - switch settings.Cipher { - case "aes": - err = decryptFileAES(dbPath, passwd) - case "xchacha", "chacha", "xchachapoly": - err = decryptFileXChachaPoly(dbPath, passwd) - default: - fmt.Println("No cipher set, defaulting to AES") - err = decryptFileAES(dbPath, passwd) - } + switch settings.Cipher { + case "aes": + err = decryptFileAES(dbPath, passwd) + case "xchacha", "chacha", "xchachapoly": + err = decryptFileXChachaPoly(dbPath, passwd) + default: + fmt.Println("No cipher set, defaulting to AES") + err = decryptFileAES(dbPath, passwd) + } - if err == nil { - fmt.Println("\nDecryption complete.") - } + if err == nil { + fmt.Println("\nDecryption complete.") + } - return err, passwd + return err, passwd } // Export data to a varity of file types func exportToFile(fileName string) error { - var err error - var maxKrypt bool - var defaultDB string - var passwd string - - ext := strings.ToLower(filepath.Ext(fileName)) - - maxKrypt, defaultDB = isActiveDatabaseEncryptedAndMaxKryptOn() - - if ext == ".csv" || ext == ".md" || ext == ".html" || ext == ".pdf" { - // If max krypt on - then autodecrypt on call and auto encrypt after call - if maxKrypt { - err, passwd = decryptDatabase(defaultDB) - if err != nil { - return err - } - } - } - - switch ext { - case ".csv": - err = exportToCsv(fileName) - case ".md": - err = exportToMarkdown(fileName) - case ".html": - err = exportToHTML(fileName) - case ".pdf": - err = exportToPDF(fileName) - default: - fmt.Printf("Error - extn %s not supported\n", ext) - return fmt.Errorf("format %s not supported", ext) - } - - if err != nil { - fmt.Printf("Error exporting to \"%s\" - \"%s\"\n", fileName, err.Error()) - return err - } else { - if _, err = os.Stat(fileName); err == nil { - fmt.Printf("Exported to %s.\n", fileName) - // Chmod 600 - os.Chmod(fileName, 0600) - - // If max krypt on - then autodecrypt on call and auto encrypt after call - if maxKrypt { - err = encryptDatabase(defaultDB, &passwd) - } - - return err - } - } - - return err + var err error + var maxKrypt bool + var defaultDB string + var passwd string + + ext := strings.ToLower(filepath.Ext(fileName)) + + maxKrypt, defaultDB = isActiveDatabaseEncryptedAndMaxKryptOn() + + if ext == ".csv" || ext == ".md" || ext == ".html" || ext == ".pdf" { + // If max krypt on - then autodecrypt on call and auto encrypt after call + if maxKrypt { + err, passwd = decryptDatabase(defaultDB) + if err != nil { + return err + } + } + } + + switch ext { + case ".csv": + err = exportToCsv(fileName) + case ".md": + err = exportToMarkdown(fileName) + case ".html": + err = exportToHTML(fileName) + case ".pdf": + err = exportToPDF(fileName) + default: + fmt.Printf("Error - extn %s not supported\n", ext) + return fmt.Errorf("format %s not supported", ext) + } + + if err != nil { + fmt.Printf("Error exporting to \"%s\" - \"%s\"\n", fileName, err.Error()) + return err + } else { + if _, err = os.Stat(fileName); err == nil { + fmt.Printf("Exported to %s.\n", fileName) + // Chmod 600 + os.Chmod(fileName, 0600) + + // If max krypt on - then autodecrypt on call and auto encrypt after call + if maxKrypt { + err = encryptDatabase(defaultDB, &passwd) + } + + return err + } + } + + return err } // Export current database to markdown func exportToMarkdown(fileName string) error { - var err error - var dataArray [][]string - var fh *os.File - var maxLengths [7]int - var headers []string = []string{" ID ", " Title ", " User ", " URL ", " Password ", " Notes ", " Modified "} - - err, dataArray = entriesToStringArray(false) - - if err != nil { - fmt.Printf("Error exporting entries to string array - \"%s\"\n", err.Error()) - return err - } - - for _, record := range dataArray { - for idx, field := range record { - - if len(field) > maxLengths[idx] { - maxLengths[idx] = len(field) - } - } - } - - // fmt.Printf("%+v\n", maxLengths) - fh, err = os.Create(fileName) - if err != nil { - fmt.Printf("Cannt open \"%s\" for writing - \"%s\"\n", fileName, err.Error()) - return err - } - - defer fh.Close() - - writer := bufio.NewWriter(fh) - - // Write markdown header - for idx, length := range maxLengths { - delta := length - len(headers[idx]) - // fmt.Printf("%d\n", delta) - if delta > 0 { - for i := 0; i < delta+2; i++ { - headers[idx] += " " - } - } - } - - writer.WriteString(" |" + strings.Join(headers, "|") + "|\n") - - // Write line separator - writer.WriteString(" | ") - for _, length := range maxLengths { - - for i := 0; i < length; i++ { - writer.WriteString("-") - } - writer.WriteString(" | ") - } - writer.WriteString("\n") - - // Write records - for _, record := range dataArray { - writer.WriteString(" | ") - for _, field := range record { - writer.WriteString(field + " | ") - } - writer.WriteString("\n") - } - - writer.Flush() - - return nil + var err error + var dataArray [][]string + var fh *os.File + var maxLengths [7]int + var headers []string = []string{" ID ", " Title ", " User ", " URL ", " Password ", " Notes ", " Modified "} + + err, dataArray = entriesToStringArray(false) + + if err != nil { + fmt.Printf("Error exporting entries to string array - \"%s\"\n", err.Error()) + return err + } + + for _, record := range dataArray { + for idx, field := range record { + + if len(field) > maxLengths[idx] { + maxLengths[idx] = len(field) + } + } + } + + // fmt.Printf("%+v\n", maxLengths) + fh, err = os.Create(fileName) + if err != nil { + fmt.Printf("Cannt open \"%s\" for writing - \"%s\"\n", fileName, err.Error()) + return err + } + + defer fh.Close() + + writer := bufio.NewWriter(fh) + + // Write markdown header + for idx, length := range maxLengths { + delta := length - len(headers[idx]) + // fmt.Printf("%d\n", delta) + if delta > 0 { + for i := 0; i < delta+2; i++ { + headers[idx] += " " + } + } + } + + writer.WriteString(" |" + strings.Join(headers, "|") + "|\n") + + // Write line separator + writer.WriteString(" | ") + for _, length := range maxLengths { + + for i := 0; i < length; i++ { + writer.WriteString("-") + } + writer.WriteString(" | ") + } + writer.WriteString("\n") + + // Write records + for _, record := range dataArray { + writer.WriteString(" | ") + for _, field := range record { + writer.WriteString(field + " | ") + } + writer.WriteString("\n") + } + + writer.Flush() + + return nil } // This needs pandoc and pdflatex support func exportToPDF(fileName string) error { - var err error - var tmpFile string - var passwd string - var pdfTkFound bool - - cmd := exec.Command("which", "pandoc") - if _, err = cmd.Output(); err != nil { - return errors.New("pandoc not found") - } - - cmd = exec.Command("which", "pdftk") - if _, err = cmd.Output(); err != nil { - fmt.Printf("pdftk not found, PDF won't be secure!\n") - } else { - pdfTkFound = true - } - - if pdfTkFound { - fmt.Printf("PDF Encryption Password: ") - err, passwd = readPassword() - } - - tmpFile = randomFileName(os.TempDir(), ".tmp") - // fmt.Printf("Temp file => %s\n", tmpFile) - err = exportToMarkdownLimited(tmpFile) - - if err == nil { - var args []string = []string{"-o", fileName, "-f", "markdown", "-V", "geometry:landscape", "--columns=600", "--pdf-engine", "xelatex", "--dpi=150", tmpFile} - - cmd = exec.Command("pandoc", args...) - _, err = cmd.Output() - // Remove tmpfile - os.Remove(tmpFile) - - // If the file is generated, encrypt it if pdfTkFound - if _, err = os.Stat(fileName); err == nil { - fmt.Printf("\nFile %s created without password.\n", fileName) - - if pdfTkFound && len(passwd) > 0 { - tmpFile = randomFileName(".", ".pdf") - // fmt.Printf("pdf file => %s\n", tmpFile) - args = []string{fileName, "output", tmpFile, "user_pw", passwd} - cmd = exec.Command("pdftk", args...) - _, err = cmd.Output() - - if err == nil { - // Copy over - fmt.Printf("Added password to %s.\n", fileName) - os.Remove(fileName) - err = os.Rename(tmpFile, fileName) - } else { - fmt.Printf("Error adding password to pdf - \"%s\"\n", err.Error()) - } - } - } - } - - return err + var err error + var tmpFile string + var passwd string + var pdfTkFound bool + + cmd := exec.Command("which", "pandoc") + if _, err = cmd.Output(); err != nil { + return errors.New("pandoc not found") + } + + cmd = exec.Command("which", "pdftk") + if _, err = cmd.Output(); err != nil { + fmt.Printf("pdftk not found, PDF won't be secure!\n") + } else { + pdfTkFound = true + } + + if pdfTkFound { + fmt.Printf("PDF Encryption Password: ") + err, passwd = readPassword() + } + + tmpFile = randomFileName(os.TempDir(), ".tmp") + // fmt.Printf("Temp file => %s\n", tmpFile) + err = exportToMarkdownLimited(tmpFile) + + if err == nil { + var args []string = []string{"-o", fileName, "-f", "markdown", "-V", "geometry:landscape", "--columns=600", "--pdf-engine", "xelatex", "--dpi=150", tmpFile} + + cmd = exec.Command("pandoc", args...) + _, err = cmd.Output() + // Remove tmpfile + os.Remove(tmpFile) + + // If the file is generated, encrypt it if pdfTkFound + if _, err = os.Stat(fileName); err == nil { + fmt.Printf("\nFile %s created without password.\n", fileName) + + if pdfTkFound && len(passwd) > 0 { + tmpFile = randomFileName(".", ".pdf") + // fmt.Printf("pdf file => %s\n", tmpFile) + args = []string{fileName, "output", tmpFile, "user_pw", passwd} + cmd = exec.Command("pdftk", args...) + _, err = cmd.Output() + + if err == nil { + // Copy over + fmt.Printf("Added password to %s.\n", fileName) + os.Remove(fileName) + err = os.Rename(tmpFile, fileName) + } else { + fmt.Printf("Error adding password to pdf - \"%s\"\n", err.Error()) + } + } + } + } + + return err } // Export current database to markdown minus the long fields func exportToMarkdownLimited(fileName string) error { - var err error - var dataArray [][]string - var fh *os.File - var maxLengths [5]int - var headers []string = []string{" ID ", " Title ", " User ", " Password ", " Modified "} - - err, dataArray = entriesToStringArray(true) - - if err != nil { - fmt.Printf("Error exporting entries to string array - \"%s\"\n", err.Error()) - return err - } - - for _, record := range dataArray { - for idx, field := range record { - - if len(field) > maxLengths[idx] { - maxLengths[idx] = len(field) - } - } - } - - // fmt.Printf("%+v\n", maxLengths) - fh, err = os.Create(fileName) - if err != nil { - fmt.Printf("Cannt open \"%s\" for writing - \"%s\"\n", fileName, err.Error()) - return err - } - - defer fh.Close() - - writer := bufio.NewWriter(fh) - - // Write markdown header - for idx, length := range maxLengths { - delta := length - len(headers[idx]) - // fmt.Printf("%d\n", delta) - if delta > 0 { - for i := 0; i < delta+2; i++ { - headers[idx] += " " - } - } - } - - writer.WriteString(" |" + strings.Join(headers, "|") + "|\n") - - // Write line separator - writer.WriteString(" | ") - for _, length := range maxLengths { - - for i := 0; i < length; i++ { - writer.WriteString("-") - } - writer.WriteString(" | ") - } - writer.WriteString("\n") - - // Write records - for _, record := range dataArray { - writer.WriteString(" | ") - for _, field := range record { - writer.WriteString(field + " | ") - } - writer.WriteString("\n") - } - - writer.Flush() - - return nil + var err error + var dataArray [][]string + var fh *os.File + var maxLengths [5]int + var headers []string = []string{" ID ", " Title ", " User ", " Password ", " Modified "} + + err, dataArray = entriesToStringArray(true) + + if err != nil { + fmt.Printf("Error exporting entries to string array - \"%s\"\n", err.Error()) + return err + } + + for _, record := range dataArray { + for idx, field := range record { + + if len(field) > maxLengths[idx] { + maxLengths[idx] = len(field) + } + } + } + + // fmt.Printf("%+v\n", maxLengths) + fh, err = os.Create(fileName) + if err != nil { + fmt.Printf("Cannt open \"%s\" for writing - \"%s\"\n", fileName, err.Error()) + return err + } + + defer fh.Close() + + writer := bufio.NewWriter(fh) + + // Write markdown header + for idx, length := range maxLengths { + delta := length - len(headers[idx]) + // fmt.Printf("%d\n", delta) + if delta > 0 { + for i := 0; i < delta+2; i++ { + headers[idx] += " " + } + } + } + + writer.WriteString(" |" + strings.Join(headers, "|") + "|\n") + + // Write line separator + writer.WriteString(" | ") + for _, length := range maxLengths { + + for i := 0; i < length; i++ { + writer.WriteString("-") + } + writer.WriteString(" | ") + } + writer.WriteString("\n") + + // Write records + for _, record := range dataArray { + writer.WriteString(" | ") + for _, field := range record { + writer.WriteString(field + " | ") + } + writer.WriteString("\n") + } + + writer.Flush() + + return nil } // Export current database to html func exportToHTML(fileName string) error { - var err error - var dataArray [][]string - var fh *os.File - var headers []string = []string{" ID ", " Title ", " User ", " URL ", " Password ", " Notes ", " Modified "} + var err error + var dataArray [][]string + var fh *os.File + var headers []string = []string{" ID ", " Title ", " User ", " URL ", " Password ", " Notes ", " Modified "} - err, dataArray = entriesToStringArray(false) + err, dataArray = entriesToStringArray(false) - if err != nil { - fmt.Printf("Error exporting entries to string array - \"%s\"\n", err.Error()) - return err - } + if err != nil { + fmt.Printf("Error exporting entries to string array - \"%s\"\n", err.Error()) + return err + } - // fmt.Printf("%+v\n", maxLengths) - fh, err = os.Create(fileName) - if err != nil { - fmt.Printf("Cannt open \"%s\" for writing - \"%s\"\n", fileName, err.Error()) - return err - } + // fmt.Printf("%+v\n", maxLengths) + fh, err = os.Create(fileName) + if err != nil { + fmt.Printf("Cannt open \"%s\" for writing - \"%s\"\n", fileName, err.Error()) + return err + } - defer fh.Close() + defer fh.Close() - writer := bufio.NewWriter(fh) + writer := bufio.NewWriter(fh) - writer.WriteString("\n") - writer.WriteString("\n") - writer.WriteString("\n") + writer.WriteString("\n") + writer.WriteString("
\n") + writer.WriteString("\n") - for _, h := range headers { - writer.WriteString(fmt.Sprintf("", h)) - } - writer.WriteString("\n") - writer.WriteString("\n") + for _, h := range headers { + writer.WriteString(fmt.Sprintf("", h)) + } + writer.WriteString("\n") + writer.WriteString("\n") - // Write records - for _, record := range dataArray { - writer.WriteString("") - for _, field := range record { - writer.WriteString(fmt.Sprintf("", field)) - } - writer.WriteString("\n") - } - writer.WriteString("\n") - writer.WriteString("
%s
%s
%s
\n") + // Write records + for _, record := range dataArray { + writer.WriteString("") + for _, field := range record { + writer.WriteString(fmt.Sprintf("%s", field)) + } + writer.WriteString("\n") + } + writer.WriteString("\n") + writer.WriteString("\n") - writer.WriteString("\n") + writer.WriteString("\n") - writer.Flush() + writer.Flush() - return nil + return nil } // Export current database to CSV func exportToCsv(fileName string) error { - var err error - var dataArray [][]string - var fh *os.File + var err error + var dataArray [][]string + var fh *os.File - err, dataArray = entriesToStringArray(false) + err, dataArray = entriesToStringArray(false) - if err != nil { - fmt.Printf("Error exporting entries to string array - \"%s\"\n", err.Error()) - return err - } + if err != nil { + fmt.Printf("Error exporting entries to string array - \"%s\"\n", err.Error()) + return err + } - fh, err = os.Create(fileName) - if err != nil { - fmt.Printf("Cannt open \"%s\" for writing - \"%s\"\n", fileName, err.Error()) - return err - } + fh, err = os.Create(fileName) + if err != nil { + fmt.Printf("Cannt open \"%s\" for writing - \"%s\"\n", fileName, err.Error()) + return err + } - writer := csv.NewWriter(fh) + writer := csv.NewWriter(fh) - // Write header - writer.Write([]string{"ID", "Title", "User", "URL", "Password", "Notes", "Modified"}) + // Write header + writer.Write([]string{"ID", "Title", "User", "URL", "Password", "Notes", "Modified"}) - for idx, record := range dataArray { - if err = writer.Write(record); err != nil { - fmt.Printf("Error writing record #%d to %s - \"%s\"\n", idx+1, fileName, err.Error()) - break - } - } + for idx, record := range dataArray { + if err = writer.Write(record); err != nil { + fmt.Printf("Error writing record #%d to %s - \"%s\"\n", idx+1, fileName, err.Error()) + break + } + } - writer.Flush() + writer.Flush() - if err != nil { - return err - } + if err != nil { + return err + } - os.Chmod(fileName, 0600) - fmt.Printf("!WARNING: Passwords are stored in plain-text!\n") - fmt.Printf("Exported %d records to %s .\n", len(dataArray), fileName) + os.Chmod(fileName, 0600) + fmt.Printf("!WARNING: Passwords are stored in plain-text!\n") + fmt.Printf("Exported %d records to %s .\n", len(dataArray), fileName) - return nil + return nil } From ba407ceeda46fb58f3c514817b4feafa411e058f Mon Sep 17 00:00:00 2001 From: Anand Date: Tue, 14 Dec 2021 10:35:23 +0530 Subject: [PATCH 09/27] ref issue #21 - Updated README.md for removal option --- README.md | 646 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 338 insertions(+), 308 deletions(-) diff --git a/README.md b/README.md index e30b918..970efa3 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Install If you are on a Debian or Debian derived system, you can directly download and install the latest version. Check out the [releases](https://github.com/pythonhacker/varuh/releases) page and use `dpkg` to install the binary. - $ sudo dpkg -i varuh-${VERSION}_amd64.deb + $ sudo dpkg -i varuh-${VERSION}_amd64.deb The binary will be installed in `/usr/bin` folder. @@ -44,24 +44,24 @@ You need the [Go compiler](https://golang.org/dl/) to build the code. (This can Install `make` by using your native package manager. Something like, - $ sudo apt install make -y + $ sudo apt install make -y should work. Then, - $ make - Building varuh - go: downloading github.com/akamensky/argparse v1.3.1 - go: downloading golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 - go: downloading github.com/atotto/clipboard v0.1.4 - go: downloading github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f - go: downloading github.com/pythonhacker/argparse v1.3.2 - go: downloading gorm.io/driver/sqlite v1.2.3 - ... + $ make + Building varuh + go: downloading github.com/akamensky/argparse v1.3.1 + go: downloading golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 + go: downloading github.com/atotto/clipboard v0.1.4 + go: downloading github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f + go: downloading github.com/pythonhacker/argparse v1.3.2 + go: downloading gorm.io/driver/sqlite v1.2.3 + ... - $ sudo make install - Installing varuh...done + $ sudo make install + Installing varuh...done The binary will be installed in `/usr/local/bin` folder. @@ -69,41 +69,42 @@ The binary will be installed in `/usr/local/bin` folder. Usage ===== - $ ./varuh -h - usage: varuh [-h|--help] [-I|--init ""] [-d|--decrypt ""] - [-C|--clone ""] [-R|--remove ""] [-U|--use-db - ""] [-f|--find ""] [-E|--edit ""] - [-l|--list-entry ""] [-x|--export ""] [-e|--encrypt] - [-A|--add] [-p|--path] [-a|--list-all] [-g|--genpass] [-s|--show] - [-c|--copy] [-v|--version] - - Password manager for the command line for Unix like operating - systems - - Options: - - -h --help Print help information - -I --init Initialize a new database - -d --decrypt Decrypt password database - -C --clone Clone an entry with - -R --remove Remove an entry with - -U --use-db Set as active database - -f --find Search entries with - -E --edit Edit entry by - -l --list-entry List entry by - -x --export Export all entries to - -e --encrypt Encrypt the current database - -A --add Add a new entry - -p --path Show current database path - -a --list-all List all entries in current database - -g --genpass Generate a strong password of length from 12 - 16 - -s --show Show passwords when listing entries - -c --copy Copy password to clipboard - -v --version Show version information and exit - - - AUTHORS - Copyright (C) 2021 Anand B Pillai + $ ./varuh -h + usage: varuh [-h|--help] [-I|--init ""] [-d|--decrypt ""] + [-C|--clone ""] [-R|--remove ""] [-U|--use-db + ""] [-f|--find ""] [-E|--edit ""] + [-l|--list-entry ""] [-x|--export ""] [-e|--encrypt] + [-A|--add] [-p|--path] [-a|--list-all] [-g|--genpass] [-s|--show] + [-c|--copy] [-y|--assume-yes] [-v|--version] + + Password manager for the command line for Unix like operating + systems + + Options: + + -h --help Print help information + -I --init Initialize a new database + -d --decrypt Decrypt password database + -C --clone Clone an entry with + -R --remove Remove an entry with or + -U --use-db Set as active database + -f --find Search entries with + -E --edit Edit entry by + -l --list-entry List entry by + -x --export Export all entries to + -e --encrypt Encrypt the current database + -A --add Add a new entry + -p --path Show current database path + -a --list-all List all entries in current database + -g --genpass Generate a strong password (length: 12 - 16) + -s --show Show passwords when listing entries + -c --copy Copy password to clipboard + -y --assume-yes Assume yes to actions requiring confirmation + -v --version Show version information and exit + + + AUTHORS + Copyright (C) 2021 Anand B Pillai Encryption and Security @@ -130,134 +131,134 @@ Databases ## Create a database - $ varuh -I mypasswds - Created new database - mypasswds - Updating active db path - /home/anand/mypasswds + $ varuh -I mypasswds + Created new database - mypasswds + Updating active db path - /home/anand/mypasswds - $ ls -lt mypasswds - -rw------- 1 anand anand 8192 Nov 9 23:06 mypasswds + $ ls -lt mypasswds + -rw------- 1 anand anand 8192 Nov 9 23:06 mypasswds The password database is created and is active now. You can start adding entries to it. ## Add an entry - $ varuh -A - Title: My Website Login - URL: mywebsite.name - Username: mememe - Password (enter to generate new): - Generating password ...done - Notes: Website uses Nginx auth - Do you want to add custom fields [y/N]: - Created new entry with id: 1 + $ varuh -A + Title: My Website Login + URL: mywebsite.name + Username: mememe + Password (enter to generate new): + Generating password ...done + Notes: Website uses Nginx auth + Do you want to add custom fields [y/N]: + Created new entry with id: 1 You can now list the entry with one of the list options. - $ varuh -l 1 - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - ID: 1 - Title: My Website Login - User: mememe - URL: http://mywebsite.name - Password: **************** - Notes: Website uses Nginx auth - Modified: 2021-21-09 23:12:35 - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + $ varuh -l 1 + +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + ID: 1 + Title: My Website Login + User: mememe + URL: http://mywebsite.name + Password: **************** + Notes: Website uses Nginx auth + Modified: 2021-21-09 23:12:35 + +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ## Add an entry with custom fields From version 0.3 onwards, custom fields are supported. - $ varuh -A - Title: Github token - URL: https://github.com/mydev/myproject - Username: mydev - Password (enter to generate new): ghp_ipQrStuVwxYz1a2b3cdEF10ghI689kLaMnOp - Notes: Never Expires - Do you want to add custom fields [y/N]: y - Field Name: Domain - Value for Domain: github.com - Field Name: Type - Value for Type: Auth Token - Field Name: - Created new entry with id: 6 - - $ varuh -l 6 - ID: 6 - Title: Github token - User: mydev - URL: https://github.com/mydev/myproject - Password: ghp_ipQrStuVwxYz1a2b3cdEF10ghI689kLaMnOp - Notes: Never Expires - Domain: github.com - Type: Auth Token - Modified: 2021-21-13 00:07:18 + $ varuh -A + Title: Github token + URL: https://github.com/mydev/myproject + Username: mydev + Password (enter to generate new): ghp_ipQrStuVwxYz1a2b3cdEF10ghI689kLaMnOp + Notes: Never Expires + Do you want to add custom fields [y/N]: y + Field Name: Domain + Value for Domain: github.com + Field Name: Type + Value for Type: Auth Token + Field Name: + Created new entry with id: 6 + + $ varuh -l 6 + ID: 6 + Title: Github token + User: mydev + URL: https://github.com/mydev/myproject + Password: ghp_ipQrStuVwxYz1a2b3cdEF10ghI689kLaMnOp + Notes: Never Expires + Domain: github.com + Type: Auth Token + Modified: 2021-21-13 00:07:18 For more on listing see the [Listing and Searching](#listing-and-searching) section below. ## Edit an entry - $ varuh -E 1 - Current Title: My Website Login - New Title: My Blog Login - Current URL: http://mywebsite.name - New URL: myblog.name - Current Username: mememe - New Username: meblog - Current Password: lTzC2z9kRppnYsYl - New Password ([y/Y] to generate new, enter will keep old one): - Current Notes: Website uses Nginx auth - New Notes: Website uses Apache - Do you want to add custom fields [y/N]: - Updated entry. - - $ varuh -l 1 -s - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - ID: 1 - Title: My Blog Login - User: meblog - URL: http://myblog.name - Password: myblog123 - Notes: Website uses Apache - Modified: 2021-21-09 23:15:29 - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + $ varuh -E 1 + Current Title: My Website Login + New Title: My Blog Login + Current URL: http://mywebsite.name + New URL: myblog.name + Current Username: mememe + New Username: meblog + Current Password: lTzC2z9kRppnYsYl + New Password ([y/Y] to generate new, enter will keep old one): + Current Notes: Website uses Nginx auth + New Notes: Website uses Apache + Do you want to add custom fields [y/N]: + Updated entry. + + $ varuh -l 1 -s + +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + ID: 1 + Title: My Blog Login + User: meblog + URL: http://myblog.name + Password: myblog123 + Notes: Website uses Apache + Modified: 2021-21-09 23:15:29 + +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ## Edit an entry with custom fields When you edit an entry with custom fields, you get the option to change the name of the fields or delete the fields entirely. - $ varuh -E 6 - Current Title: Github token - New Title: - Current URL: https://github.com/mydev/myproject - New URL: - Current Username: mydev - New Username: - Current Password: ghp_ipQrStuVwxYz1a2b3cdEF10ghI689kLaMnOp - New Password ([y/Y] to generate new, enter will keep old one): - Current Notes: Never Expires - New Notes: - Editing/deleting custom fields - Field Name: Domain - New Field Name (Enter to keep, "x" to delete): x - Field Name: Type - New Field Name (Enter to keep, "x" to delete): Token Type - Field Value: Auth Token + $ varuh -E 6 + Current Title: Github token + New Title: + Current URL: https://github.com/mydev/myproject + New URL: + Current Username: mydev + New Username: + Current Password: ghp_ipQrStuVwxYz1a2b3cdEF10ghI689kLaMnOp + New Password ([y/Y] to generate new, enter will keep old one): + Current Notes: Never Expires + New Notes: + Editing/deleting custom fields + Field Name: Domain + New Field Name (Enter to keep, "x" to delete): x + Field Name: Type + New Field Name (Enter to keep, "x" to delete): Token Type + Field Value: Auth Token New Field Value (Enter to keep): - Do you want to add custom fields [y/N]: - Created 1 custom entries for entry: 21. - Updated entry. - - $ varuh -l 6 -s - ID: 6 - Title: Github token - User: mydev - URL: https://github.com/mydev/myproject - Password: ghp_ipQrStuVwxYz1a2b3cdEF10ghI689kLaMnOp - Notes: Never Expires - Token Type: Auth Token - Modified: 2021-21-13 00:16:41 + Do you want to add custom fields [y/N]: + Created 1 custom entries for entry: 21. + Updated entry. + + $ varuh -l 6 -s + ID: 6 + Title: Github token + User: mydev + URL: https://github.com/mydev/myproject + Password: ghp_ipQrStuVwxYz1a2b3cdEF10ghI689kLaMnOp + Notes: Never Expires + Token Type: Auth Token + Modified: 2021-21-13 00:16:41 (*-s* turns on visible passwords) @@ -265,30 +266,59 @@ When you edit an entry with custom fields, you get the option to change the name To clone (copy) an entry, - $ $ varuh -C 1 - Cloned to new entry, id: 3 + $ $ varuh -C 1 + Cloned to new entry, id: 3 ## Remove an entry - $ varuh -R 1 - Entry with id 1 was removed from the database + $ varuh -R 1 + +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + Title: My Website Login + User: mememe + URL: https://mywebsite.name + Modified: 2021-21-09 23:12:35 + +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + Please confirm removal [Y/n]: + Entry with id 1 was removed from the database It is an error if the id does not exist. - $ varuh -R 4 - No entry with id 4 was found + $ varuh -R 4 + No entry with id 4 was found + +## Remove a range of entries + +You can remove a range of entry ids from id1-id2 using the following command. + + $ varuh -R id1-id2 + +Example: + + $ varuh -R 1-4 + +This will remove entries from 1 to 4 inclusive, asking for confirmation from the user every time. + +## Removal without confirmation + +If you are very sure, you can avoid the confirmation prompt by passing the `-y` flag which will remove the entry without confirmation. + + $ varuh -R 2 -y + ... + ... + ... + Entry with id 2 was removed from the database ## Switch to a new database Once a database is active, creating another one automatically encrypts the current one and makes the new one the active database. The automatic encryption happens only if the configuration flag `auto_encrypt` is turned on (See section [Configuration](#configuration) below). - $ varuh -I mysecrets - Encrytping current database - /home/anand/mypasswds - Password: - Password again: - Encryption complete. - Created new database - mysecrets - Updating active db path - /home/anand/mysecrets + $ varuh -I mysecrets + Encrytping current database - /home/anand/mypasswds + Password: + Password again: + Encryption complete. + Created new database - mysecrets + Updating active db path - /home/anand/mysecrets The previous database is now encrypted with the configured block cipher using the password. Please make sure you remember the password. @@ -296,48 +326,48 @@ The previous database is now encrypted with the configured block cipher using th If you want to switch back to a previous database, you can use the `-U` option. The same process is repeated with the current database getting encrypted and the older one getting decrypted. - $ varuh -U mypasswds - Encrypting current active database - /home/anand/mysecrets - Password: - Password again: - Encryption complete. - Database /home/anand/mypasswds is encrypted, decrypting it - Password: - Decryption complete. - Switched active database successfully. - + $ varuh -U mypasswds + Encrypting current active database - /home/anand/mysecrets + Password: + Password again: + Encryption complete. + Database /home/anand/mypasswds is encrypted, decrypting it + Password: + Decryption complete. + Switched active database successfully. + ## Manual encryption and decryption You can manually encrypt the current database using the `-e` option. - $ varuh -e - Password: - Password again: - Encryption complete. + $ varuh -e + Password: + Password again: + Encryption complete. Note that once you encrypt the active database, you cannot use the listings any more unless it is decrypted. - $ varuh -l 2 - No decrypted active database found. + $ varuh -l 2 + No decrypted active database found. Manually decrypt the database using `-d` option. - $ varuh -d mypasswds - Password: - Decryption complete. + $ varuh -d mypasswds + Password: + Decryption complete. Now the database is active again and you can see the listings. - $ varuh -l 3 - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - ID: 2 - Title: My Blog Login - User: myblog.name - URL: http://meblog - Password: ********* - Notes: Website uses Apache - Modified: 2021-21-09 23:21:32 - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + $ varuh -l 3 + +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + ID: 2 + Title: My Blog Login + User: myblog.name + URL: http://meblog + Password: ********* + Notes: Website uses Apache + Modified: 2021-21-09 23:21:32 + +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ## Always on encryption @@ -345,20 +375,20 @@ If the config param `encrypt_on` is set to `true` along with `auto_encrypt` (def ### Example - $ varuh -f my -s - Password: - Decryption complete. - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - ID: 2 - Title: MY LOCAL BANK - User: banklogin - URL: https://my.localbank.com - Password: bankpass123 - Notes: - Modified: 2021-21-18 12:44:10 - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - - Encryption complete. + $ varuh -f my -s + Password: + Decryption complete. + +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + ID: 2 + Title: MY LOCAL BANK + User: banklogin + URL: https://my.localbank.com + Password: bankpass123 + Notes: + Modified: 2021-21-18 12:44:10 + +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + Encryption complete. In this mode, your data is provided maximum safety as the database remains decrypted only for a short while on the disk while the data is being read and once done is encrypted back again. @@ -369,79 +399,79 @@ Listing and Searching To list an entry using its id, - $ varuh -l 8 - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - ID: 8 - Title: Google account - User: anandpillai@alumni.iitm.ac.in - URL: - Password: *********** - Notes: - Modified: 2021-21-25 15:02:50 - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + $ varuh -l 8 + +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + ID: 8 + Title: Google account + User: anandpillai@alumni.iitm.ac.in + URL: + Password: *********** + Notes: + Modified: 2021-21-25 15:02:50 + +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ## To search an entry An entry can be searched on its title, username, URL or notes. Search is case-insensitive. - $ varuh -f google - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - ID: 8 - Title: Google account - User: anandpillai@alumni.iitm.ac.in - URL: - Password: ********** - Notes: - Modified: 2021-21-25 15:02:50 - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - ID: 9 - Title: Google account - User: xyz@gmail.com - URL: - Password: ******** - Notes: - Modified: 2021-21-25 15:05:36 - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - ID: 10 - Title: Google account - User: somethingaboutme@gmail.com - URL: - Password: *********** - Notes: - Modified: 2021-21-25 15:09:51 - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + $ varuh -f google + +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + ID: 8 + Title: Google account + User: anandpillai@alumni.iitm.ac.in + URL: + Password: ********** + Notes: + Modified: 2021-21-25 15:02:50 + +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + ID: 9 + Title: Google account + User: xyz@gmail.com + URL: + Password: ******** + Notes: + Modified: 2021-21-25 15:05:36 + +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + ID: 10 + Title: Google account + User: somethingaboutme@gmail.com + URL: + Password: *********** + Notes: + Modified: 2021-21-25 15:09:51 + +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ## To list all entries To list all entries, use the option `-a`. - $ varuh -a - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - ID: 1 - Title: My Bank #1 - User: myusername1 - URL: https://mysuperbank1.com - Password: *********** - Notes: - Modified: 2021-21-15 15:40:29 - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - ID: 2 - Title: My Digital Locker #1 - User: mylockerusername - URL: https://mysuperlocker1.com - Password: ********** - Notes: - Modified: 2021-21-18 12:44:10 - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - ID: 3 - Title: My Bank Login #2 - User: mybankname2 - URL: https://myaveragebank.com - Password: ********** - Notes: - Modified: 2021-21-19 14:16:33 - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - ... + $ varuh -a + +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + ID: 1 + Title: My Bank #1 + User: myusername1 + URL: https://mysuperbank1.com + Password: *********** + Notes: + Modified: 2021-21-15 15:40:29 + +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + ID: 2 + Title: My Digital Locker #1 + User: mylockerusername + URL: https://mysuperlocker1.com + Password: ********** + Notes: + Modified: 2021-21-18 12:44:10 + +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + ID: 3 + Title: My Bank Login #2 + User: mybankname2 + URL: https://myaveragebank.com + Password: ********** + Notes: + Modified: 2021-21-19 14:16:33 + +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + ... By default the listing is in ascending ID order. This can be changed in the configuration (see below). @@ -457,8 +487,8 @@ This is useful to copy the password to a password input field in the browser for ## See current active database path - $ varuh -p - /home/anand/mypasswds + $ varuh -p + /home/anand/mypasswds Export ====== @@ -472,37 +502,37 @@ Export To export use the `-x` option. The type of file is automatically figured out from the filename extension. - $ varuh -x passwds.csv - !WARNING: Passwords are stored in plain-text! - Exported 14 records to passwds.csv . - Exported to passwds.csv. + $ varuh -x passwds.csv + !WARNING: Passwords are stored in plain-text! + Exported 14 records to passwds.csv . + Exported to passwds.csv. - $ varuh -x passwds.html - Exported to passwds.html. + $ varuh -x passwds.html + Exported to passwds.html. PDF export is supported if `pandoc` is installed along with the required `pdflatex` packages. The following command (on `Debian` and derived systems) should install the required dependencies. - $ sudo apt-get install pandoc texlive-latex-base texlive-fonts-recommended texlive-fonts-extra texlive-latex-extra texlive-xetex lmodern -y + $ sudo apt-get install pandoc texlive-latex-base texlive-fonts-recommended texlive-fonts-extra texlive-latex-extra texlive-xetex lmodern -y Then, - $ varuh -x passwds.pdf - pdftk not found, PDF won't be secure! + $ varuh -x passwds.pdf + pdftk not found, PDF won't be secure! - File passwds.pdf created without password. - Exported to passwds.pdf. + File passwds.pdf created without password. + Exported to passwds.pdf. PDF files are exported in landscape mode with 150 dpi and 600 columns. To avoid the data not fitting into one page the fields `Notes` and `URL` are not exported. If `pdftk` is installed, the PDF files will be encrypted with an (optional) password. - $ sudo apt-get install pdftk -y + $ sudo apt-get install pdftk -y - $ varuh -x passwds.pdf - PDF Encryption Password: ****** - File passwds.pdf created without password. - Added password to passwds.pdf. - Exported to passwds.pdf. + $ varuh -x passwds.pdf + PDF Encryption Password: ****** + File passwds.pdf created without password. + Added password to passwds.pdf. + Exported to passwds.pdf. Misc ==== @@ -513,14 +543,14 @@ Generate a strong password of length ranging from 12 - 16. A `strong` password is defined as a cryptographically secure string contaning at least one upper-case letter, one punctuation character and one number. - $ varuh -g - 7%zv/uzIgpqexJ + $ varuh -g + 7%zv/uzIgpqexJ - By passing the `-c` option, the password is also copied to the clipboard. + By passing the `-c` option, the password is also copied to the clipboard. - $ varuh -g -c - y6UpD$~uBI#8 - Password copied to clipboard + $ varuh -g -c + y6UpD$~uBI#8 + Password copied to clipboard Configuration @@ -530,19 +560,19 @@ Configuration The config file is named *config.json*. It looks as follows. - `{ - "active_db": "/home/anand/mypasswds", - "cipher": "aes", - "auto_encrypt": true, - "visible_passwords": false, - "encrypt_on": true, - "path": "/home/anand/.config/varuh/config.json", - "list_order": "id,asc", - "delimiter": "+", - "color": "default", - "bgcolor": "bgblack" - } - ` + `{ + "active_db": "/home/anand/mypasswds", + "cipher": "aes", + "auto_encrypt": true, + "visible_passwords": false, + "encrypt_on": true, + "path": "/home/anand/.config/varuh/config.json", + "list_order": "id,asc", + "delimiter": "+", + "color": "default", + "bgcolor": "bgblack" + } + ` You can modify the following variables. 1. `auto_encrypt` - Set this to true to enable automatic encryption/decryption when switching databases. Otherwise you have to do this manually. The default is `true`. @@ -555,7 +585,7 @@ You can modify the following variables. * `title` - Uses the `Title` field. * `username` - Uses the `User` field. - Always specify this configuration as `,`. Supported `` values are `asc` and `desc`. + Always specify this configuration as `,`. Supported `` values are `asc` and `desc`. 1. `delimiter` - This modifies the delimiter string when printing a listing. Only one character is allowed. 1. `color` - The foreground color of the text when printing listings. 1. `bgcolor` - The background color of the text when printing listings. From 1e5a00be5cafb42ec2ddba726c7d98ffcb9b6211 Mon Sep 17 00:00:00 2001 From: Anand Date: Tue, 14 Dec 2021 10:35:53 +0530 Subject: [PATCH 10/27] ref issue #21 - Added assume-yes option for removal --- actions.go | 1816 ++++++++++++++++++++++++++-------------------------- 1 file changed, 910 insertions(+), 906 deletions(-) diff --git a/actions.go b/actions.go index 9bec025..35c8ae2 100644 --- a/actions.go +++ b/actions.go @@ -2,1127 +2,1131 @@ package main import ( - "bufio" - "encoding/csv" - "errors" - "fmt" - "os" - "os/exec" - "os/signal" - "path/filepath" - "strconv" - "strings" - "syscall" + "bufio" + "encoding/csv" + "errors" + "fmt" + "os" + "os/exec" + "os/signal" + "path/filepath" + "strconv" + "strings" + "syscall" ) type CustomEntry struct { - fieldName string - fieldValue string + fieldName string + fieldValue string } // Wrappers (closures) for functions accepting strings as input for in/out encryption func WrapperMaxKryptStringFunc(fn actionFunc) actionFunc { - return func(inputStr string) error { - var maxKrypt bool - var defaultDB string - var encPasswd string - var err error + return func(inputStr string) error { + var maxKrypt bool + var defaultDB string + var encPasswd string + var err error - maxKrypt, defaultDB = isActiveDatabaseEncryptedAndMaxKryptOn() + maxKrypt, defaultDB = isActiveDatabaseEncryptedAndMaxKryptOn() - // If max krypt on - then autodecrypt on call and auto encrypt after call - if maxKrypt { - err, encPasswd = decryptDatabase(defaultDB) - if err != nil { - return err - } + // If max krypt on - then autodecrypt on call and auto encrypt after call + if maxKrypt { + err, encPasswd = decryptDatabase(defaultDB) + if err != nil { + return err + } - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) - go func() { - sig := <-sigChan - fmt.Println("Received signal", sig) - // Reencrypt - encryptDatabase(defaultDB, &encPasswd) - os.Exit(1) - }() - } + go func() { + sig := <-sigChan + fmt.Println("Received signal", sig) + // Reencrypt + encryptDatabase(defaultDB, &encPasswd) + os.Exit(1) + }() + } - err = fn(inputStr) + err = fn(inputStr) - // If max krypt on - then autodecrypt on call and auto encrypt after call - if maxKrypt { - encryptDatabase(defaultDB, &encPasswd) - } + // If max krypt on - then autodecrypt on call and auto encrypt after call + if maxKrypt { + encryptDatabase(defaultDB, &encPasswd) + } - return err - } + return err + } } // Wrappers (closures) for functions accepting no input for in/out encryption func WrapperMaxKryptVoidFunc(fn voidFunc) voidFunc { - return func() error { - var maxKrypt bool - var defaultDB string - var encPasswd string - var err error + return func() error { + var maxKrypt bool + var defaultDB string + var encPasswd string + var err error - maxKrypt, defaultDB = isActiveDatabaseEncryptedAndMaxKryptOn() + maxKrypt, defaultDB = isActiveDatabaseEncryptedAndMaxKryptOn() - // If max krypt on - then autodecrypt on call and auto encrypt after call - if maxKrypt { - err, encPasswd = decryptDatabase(defaultDB) - if err != nil { - return err - } + // If max krypt on - then autodecrypt on call and auto encrypt after call + if maxKrypt { + err, encPasswd = decryptDatabase(defaultDB) + if err != nil { + return err + } - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) - go func() { - sig := <-sigChan - fmt.Println("Received signal", sig) - // Reencrypt - encryptDatabase(defaultDB, &encPasswd) - os.Exit(1) - }() - } + go func() { + sig := <-sigChan + fmt.Println("Received signal", sig) + // Reencrypt + encryptDatabase(defaultDB, &encPasswd) + os.Exit(1) + }() + } - err = fn() + err = fn() - // If max krypt on - then autodecrypt on call and auto encrypt after call - if maxKrypt { - encryptDatabase(defaultDB, &encPasswd) - } + // If max krypt on - then autodecrypt on call and auto encrypt after call + if maxKrypt { + encryptDatabase(defaultDB, &encPasswd) + } - return err - } + return err + } } // Print the current active database path func showActiveDatabasePath() error { - err, settings := getOrCreateLocalConfig(APP) - - if err != nil { - fmt.Printf("Error parsing config - \"%s\"\n", err.Error()) - return err - } - - if settings != nil { - if settings.ActiveDB != "" { - fmt.Printf("%s\n", settings.ActiveDB) - } else { - fmt.Println("No active database") - } - return nil - } else { - fmt.Printf("Error - null config\n") - return errors.New("null config") - } + err, settings := getOrCreateLocalConfig(APP) + + if err != nil { + fmt.Printf("Error parsing config - \"%s\"\n", err.Error()) + return err + } + + if settings != nil { + if settings.ActiveDB != "" { + fmt.Printf("%s\n", settings.ActiveDB) + } else { + fmt.Println("No active database") + } + return nil + } else { + fmt.Printf("Error - null config\n") + return errors.New("null config") + } } // Set the current active database path func setActiveDatabasePath(dbPath string) error { - var fullPath string - var activeEncrypted bool - var newEncrypted bool - - err, settings := getOrCreateLocalConfig(APP) - - if err != nil { - fmt.Printf("Error parsing config - \"%s\"\n", err.Error()) - return err - } - - if settings != nil { - var flag bool - - if _, err = os.Stat(dbPath); os.IsNotExist(err) { - fmt.Printf("Error - path %s does not exist\n", dbPath) - return err - } - - fullPath, _ = filepath.Abs(dbPath) - - if fullPath == settings.ActiveDB { - fmt.Printf("Current database is \"%s\" - nothing to do\n", fullPath) - return nil - } - - if _, flag = isFileEncrypted(settings.ActiveDB); flag { - activeEncrypted = true - } - - if _, flag = isFileEncrypted(fullPath); flag { - newEncrypted = true - } - - // If autoencrypt is true - encrypt current DB automatically - if settings.AutoEncrypt { - if !activeEncrypted { - fmt.Printf("Encrypting current active database - %s\n", settings.ActiveDB) - err = encryptActiveDatabase() - if err == nil { - activeEncrypted = true - } - } - - if newEncrypted { - // Decrypt new database if it is encrypted - fmt.Printf("Database %s is encrypted, decrypting it\n", fullPath) - err, _ = decryptDatabase(fullPath) - if err != nil { - fmt.Printf("Decryption Error - \"%s\", not switching databases\n", err.Error()) - return err - } else { - newEncrypted = false - } - } - } - - if !activeEncrypted { - // Use should manually encrypt before switching - fmt.Println("Auto-encrypt disabled, encrypt existing database before switching to new.") - return nil - } - - if newEncrypted { - // Use should manually decrypt before switching - fmt.Println("Auto-encrypt disabled, decrypt new database manually before switching.") - return nil - } - - settings.ActiveDB = fullPath - err = updateSettings(settings, settings.ConfigPath) - if err == nil { - fmt.Println("Switched active database successfully.") - } else { - fmt.Printf("Error updating settings - \"%s\"\n", err.Error()) - } - - return err - - } else { - fmt.Printf("Error - null config\n") - return errors.New("null config") - } + var fullPath string + var activeEncrypted bool + var newEncrypted bool + + err, settings := getOrCreateLocalConfig(APP) + + if err != nil { + fmt.Printf("Error parsing config - \"%s\"\n", err.Error()) + return err + } + + if settings != nil { + var flag bool + + if _, err = os.Stat(dbPath); os.IsNotExist(err) { + fmt.Printf("Error - path %s does not exist\n", dbPath) + return err + } + + fullPath, _ = filepath.Abs(dbPath) + + if fullPath == settings.ActiveDB { + fmt.Printf("Current database is \"%s\" - nothing to do\n", fullPath) + return nil + } + + if _, flag = isFileEncrypted(settings.ActiveDB); flag { + activeEncrypted = true + } + + if _, flag = isFileEncrypted(fullPath); flag { + newEncrypted = true + } + + // If autoencrypt is true - encrypt current DB automatically + if settings.AutoEncrypt { + if !activeEncrypted { + fmt.Printf("Encrypting current active database - %s\n", settings.ActiveDB) + err = encryptActiveDatabase() + if err == nil { + activeEncrypted = true + } + } + + if newEncrypted { + // Decrypt new database if it is encrypted + fmt.Printf("Database %s is encrypted, decrypting it\n", fullPath) + err, _ = decryptDatabase(fullPath) + if err != nil { + fmt.Printf("Decryption Error - \"%s\", not switching databases\n", err.Error()) + return err + } else { + newEncrypted = false + } + } + } + + if !activeEncrypted { + // Use should manually encrypt before switching + fmt.Println("Auto-encrypt disabled, encrypt existing database before switching to new.") + return nil + } + + if newEncrypted { + // Use should manually decrypt before switching + fmt.Println("Auto-encrypt disabled, decrypt new database manually before switching.") + return nil + } + + settings.ActiveDB = fullPath + err = updateSettings(settings, settings.ConfigPath) + if err == nil { + fmt.Println("Switched active database successfully.") + } else { + fmt.Printf("Error updating settings - \"%s\"\n", err.Error()) + } + + return err + + } else { + fmt.Printf("Error - null config\n") + return errors.New("null config") + } } // Text menu driven function to add a new entry func addNewEntry() error { - var userName string - var title string - var url string - var notes string - var passwd string - var err error - var customEntries []CustomEntry - - if err = checkActiveDatabase(); err != nil { - return err - } - - reader := bufio.NewReader(os.Stdin) - title = readInput(reader, "Title") - url = readInput(reader, "URL") - - if len(url) > 0 && !strings.HasPrefix(strings.ToLower(url), "http://") && !strings.HasPrefix(strings.ToLower(url), "https://") { - url = "http://" + url - } - - userName = readInput(reader, "Username") - - fmt.Printf("Password (enter to generate new): ") - err, passwd = readPassword() - - if len(passwd) == 0 { - fmt.Printf("\nGenerating password ...") - err, passwd = generateStrongPassword() - fmt.Printf("done") - } - // fmt.Printf("Password => %s\n", passwd) - - notes = readInput(reader, "\nNotes") - - // Title and username/password are mandatory - if len(title) == 0 { - fmt.Printf("Error - valid Title required\n") - return errors.New("invalid input") - } - if len(userName) == 0 { - fmt.Printf("Error - valid Username required\n") - return errors.New("invalid input") - } - if len(passwd) == 0 { - fmt.Printf("Error - valid Password required\n") - return errors.New("invalid input") - } - - customEntries = addCustomFields(reader) - - // Trim spaces - err = addNewDatabaseEntry(title, userName, url, passwd, notes, customEntries) - - if err != nil { - fmt.Printf("Error adding entry - \"%s\"\n", err.Error()) - } - - return err + var userName string + var title string + var url string + var notes string + var passwd string + var err error + var customEntries []CustomEntry + + if err = checkActiveDatabase(); err != nil { + return err + } + + reader := bufio.NewReader(os.Stdin) + title = readInput(reader, "Title") + url = readInput(reader, "URL") + + if len(url) > 0 && !strings.HasPrefix(strings.ToLower(url), "http://") && !strings.HasPrefix(strings.ToLower(url), "https://") { + url = "http://" + url + } + + userName = readInput(reader, "Username") + + fmt.Printf("Password (enter to generate new): ") + err, passwd = readPassword() + + if len(passwd) == 0 { + fmt.Printf("\nGenerating password ...") + err, passwd = generateStrongPassword() + fmt.Printf("done") + } + // fmt.Printf("Password => %s\n", passwd) + + notes = readInput(reader, "\nNotes") + + // Title and username/password are mandatory + if len(title) == 0 { + fmt.Printf("Error - valid Title required\n") + return errors.New("invalid input") + } + if len(userName) == 0 { + fmt.Printf("Error - valid Username required\n") + return errors.New("invalid input") + } + if len(passwd) == 0 { + fmt.Printf("Error - valid Password required\n") + return errors.New("invalid input") + } + + customEntries = addCustomFields(reader) + + // Trim spaces + err = addNewDatabaseEntry(title, userName, url, passwd, notes, customEntries) + + if err != nil { + fmt.Printf("Error adding entry - \"%s\"\n", err.Error()) + } + + return err } // Function to update existing custom entries and add new ones // The bool part of the return value indicates whether to take action func addOrUpdateCustomFields(reader *bufio.Reader, entry *Entry) ([]CustomEntry, bool) { - var customEntries []ExtendedEntry - var editedCustomEntries []CustomEntry - var newCustomEntries []CustomEntry - var flag bool + var customEntries []ExtendedEntry + var editedCustomEntries []CustomEntry + var newCustomEntries []CustomEntry + var flag bool - customEntries = getExtendedEntries(entry) + customEntries = getExtendedEntries(entry) - if len(customEntries) > 0 { + if len(customEntries) > 0 { - fmt.Println("Editing/deleting custom fields") - for _, customEntry := range customEntries { - var fieldName string - var fieldValue string + fmt.Println("Editing/deleting custom fields") + for _, customEntry := range customEntries { + var fieldName string + var fieldValue string - fmt.Println("Field Name: " + customEntry.FieldName) - fieldName = readInput(reader, "\tNew Field Name (Enter to keep, \"x\" to delete)") - if strings.ToLower(strings.TrimSpace(fieldName)) == "x" { - fmt.Println("Deleting field " + fieldName) - } else { - if strings.TrimSpace(fieldName) == "" { - fieldName = customEntry.FieldName - } + fmt.Println("Field Name: " + customEntry.FieldName) + fieldName = readInput(reader, "\tNew Field Name (Enter to keep, \"x\" to delete)") + if strings.ToLower(strings.TrimSpace(fieldName)) == "x" { + fmt.Println("Deleting field " + fieldName) + } else { + if strings.TrimSpace(fieldName) == "" { + fieldName = customEntry.FieldName + } - fmt.Println("Field Value: " + customEntry.FieldValue) - fieldValue = readInput(reader, "\tNew Field Value (Enter to keep)") - if strings.TrimSpace(fieldValue) == "" { - fieldValue = customEntry.FieldValue - } + fmt.Println("Field Value: " + customEntry.FieldValue) + fieldValue = readInput(reader, "\tNew Field Value (Enter to keep)") + if strings.TrimSpace(fieldValue) == "" { + fieldValue = customEntry.FieldValue + } - editedCustomEntries = append(editedCustomEntries, CustomEntry{fieldName, fieldValue}) - } - } - } + editedCustomEntries = append(editedCustomEntries, CustomEntry{fieldName, fieldValue}) + } + } + } - newCustomEntries = addCustomFields(reader) + newCustomEntries = addCustomFields(reader) - editedCustomEntries = append(editedCustomEntries, newCustomEntries...) + editedCustomEntries = append(editedCustomEntries, newCustomEntries...) - // Cases where length == 0 - // 1. Existing entries - all deleted - flag = len(customEntries) > 0 || len(editedCustomEntries) > 0 + // Cases where length == 0 + // 1. Existing entries - all deleted + flag = len(customEntries) > 0 || len(editedCustomEntries) > 0 - return editedCustomEntries, flag + return editedCustomEntries, flag } // Function to add custom fields to an entry func addCustomFields(reader *bufio.Reader) []CustomEntry { - // Custom fields - var custom string - var customEntries []CustomEntry + // Custom fields + var custom string + var customEntries []CustomEntry - custom = readInput(reader, "Do you want to add custom fields [y/N]") - if strings.ToLower(custom) == "y" { + custom = readInput(reader, "Do you want to add custom fields [y/N]") + if strings.ToLower(custom) == "y" { - fmt.Println("Keep entering custom field name followed by the value. Press return with no input once done.") - for true { - var customFieldName string - var customFieldValue string + fmt.Println("Keep entering custom field name followed by the value. Press return with no input once done.") + for true { + var customFieldName string + var customFieldValue string - customFieldName = strings.TrimSpace(readInput(reader, "Field Name")) - if customFieldName != "" { - customFieldValue = strings.TrimSpace(readInput(reader, "Value for "+customFieldName)) - } + customFieldName = strings.TrimSpace(readInput(reader, "Field Name")) + if customFieldName != "" { + customFieldValue = strings.TrimSpace(readInput(reader, "Value for "+customFieldName)) + } - if customFieldName == "" && customFieldValue == "" { - break - } + if customFieldName == "" && customFieldValue == "" { + break + } - customEntries = append(customEntries, CustomEntry{customFieldName, customFieldValue}) - } - } + customEntries = append(customEntries, CustomEntry{customFieldName, customFieldValue}) + } + } - return customEntries + return customEntries } // Edit a current entry by id func editCurrentEntry(idString string) error { - var userName string - var title string - var url string - var notes string - var passwd string - var err error - var entry *Entry - var id int + var userName string + var title string + var url string + var notes string + var passwd string + var err error + var entry *Entry + var id int - if err = checkActiveDatabase(); err != nil { - return err - } + if err = checkActiveDatabase(); err != nil { + return err + } - id, _ = strconv.Atoi(idString) + id, _ = strconv.Atoi(idString) - err, entry = getEntryById(id) - if err != nil || entry == nil { - fmt.Printf("No entry found for id %d\n", id) - return err - } + err, entry = getEntryById(id) + if err != nil || entry == nil { + fmt.Printf("No entry found for id %d\n", id) + return err + } - reader := bufio.NewReader(os.Stdin) + reader := bufio.NewReader(os.Stdin) - fmt.Printf("Current Title: %s\n", entry.Title) - title = readInput(reader, "New Title") + fmt.Printf("Current Title: %s\n", entry.Title) + title = readInput(reader, "New Title") - fmt.Printf("Current URL: %s\n", entry.Url) - url = readInput(reader, "New URL") + fmt.Printf("Current URL: %s\n", entry.Url) + url = readInput(reader, "New URL") - if len(url) > 0 && !strings.HasPrefix(strings.ToLower(url), "http://") && !strings.HasPrefix(strings.ToLower(url), "https://") { - url = "http://" + url - } + if len(url) > 0 && !strings.HasPrefix(strings.ToLower(url), "http://") && !strings.HasPrefix(strings.ToLower(url), "https://") { + url = "http://" + url + } - fmt.Printf("Current Username: %s\n", entry.User) - userName = readInput(reader, "New Username") + fmt.Printf("Current Username: %s\n", entry.User) + userName = readInput(reader, "New Username") - fmt.Printf("Current Password: %s\n", entry.Password) - fmt.Printf("New Password ([y/Y] to generate new, enter will keep old one): ") - err, passwd = readPassword() + fmt.Printf("Current Password: %s\n", entry.Password) + fmt.Printf("New Password ([y/Y] to generate new, enter will keep old one): ") + err, passwd = readPassword() - if strings.ToLower(passwd) == "y" { - fmt.Printf("\nGenerating new password ...") - err, passwd = generateStrongPassword() - } - // fmt.Printf("Password => %s\n", passwd) + if strings.ToLower(passwd) == "y" { + fmt.Printf("\nGenerating new password ...") + err, passwd = generateStrongPassword() + } + // fmt.Printf("Password => %s\n", passwd) - fmt.Printf("\nCurrent Notes: %s\n", entry.Notes) - notes = readInput(reader, "New Notes") + fmt.Printf("\nCurrent Notes: %s\n", entry.Notes) + notes = readInput(reader, "New Notes") - customEntries, flag := addOrUpdateCustomFields(reader, entry) + customEntries, flag := addOrUpdateCustomFields(reader, entry) - // Update - err = updateDatabaseEntry(entry, title, userName, url, passwd, notes, customEntries, flag) - if err != nil { - fmt.Printf("Error updating entry - \"%s\"\n", err.Error()) - } + // Update + err = updateDatabaseEntry(entry, title, userName, url, passwd, notes, customEntries, flag) + if err != nil { + fmt.Printf("Error updating entry - \"%s\"\n", err.Error()) + } - return err + return err } // List current entry by id func listCurrentEntry(idString string) error { - var id int - var err error - var entry *Entry + var id int + var err error + var entry *Entry - if err = checkActiveDatabase(); err != nil { - return err - } + if err = checkActiveDatabase(); err != nil { + return err + } - id, _ = strconv.Atoi(idString) + id, _ = strconv.Atoi(idString) - // fmt.Printf("Listing current entry - %d\n", id) - err, entry = getEntryById(id) - if err != nil || entry == nil { - fmt.Printf("No entry found for id %d\n", id) - return err - } + // fmt.Printf("Listing current entry - %d\n", id) + err, entry = getEntryById(id) + if err != nil || entry == nil { + fmt.Printf("No entry found for id %d\n", id) + return err + } - err = printEntry(entry, true) + err = printEntry(entry, true) - if err == nil && settingsRider.CopyPassword { - // fmt.Printf("Copying password " + entry.Password + " to clipboard\n") - copyPasswordToClipboard(entry.Password) - } + if err == nil && settingsRider.CopyPassword { + // fmt.Printf("Copying password " + entry.Password + " to clipboard\n") + copyPasswordToClipboard(entry.Password) + } - return err + return err } // List all entries func listAllEntries() error { - var err error - var maxKrypt bool - var defaultDB string - var passwd string - - maxKrypt, defaultDB = isActiveDatabaseEncryptedAndMaxKryptOn() - - // If max krypt on - then autodecrypt on call and auto encrypt after call - if maxKrypt { - err, passwd = decryptDatabase(defaultDB) - if err != nil { - return err - } - } - - if err = checkActiveDatabase(); err != nil { - return err - } - - err, settings := getOrCreateLocalConfig(APP) - - if err != nil { - fmt.Printf("Error parsing config - \"%s\"\n", err.Error()) - return err - } - - orderKeys := strings.Split(settings.ListOrder, ",") - err, entries := iterateEntries(orderKeys[0], orderKeys[1]) - - if err == nil { - if len(entries) > 0 { - fmt.Printf("%s", getColor(strings.ToLower(settings.Color))) - printDelim(settings.Delim, settings.Color) - for _, entry := range entries { - printEntry(&entry, false) - } - } else { - fmt.Println("No entries.") - } - } else { - fmt.Printf("Error fetching entries: \"%s\"\n", err.Error()) - return err - } - - // If max krypt on - then autodecrypt on call and auto encrypt after call - if maxKrypt { - err = encryptDatabase(defaultDB, &passwd) - } - - return err + var err error + var maxKrypt bool + var defaultDB string + var passwd string + + maxKrypt, defaultDB = isActiveDatabaseEncryptedAndMaxKryptOn() + + // If max krypt on - then autodecrypt on call and auto encrypt after call + if maxKrypt { + err, passwd = decryptDatabase(defaultDB) + if err != nil { + return err + } + } + + if err = checkActiveDatabase(); err != nil { + return err + } + + err, settings := getOrCreateLocalConfig(APP) + + if err != nil { + fmt.Printf("Error parsing config - \"%s\"\n", err.Error()) + return err + } + + orderKeys := strings.Split(settings.ListOrder, ",") + err, entries := iterateEntries(orderKeys[0], orderKeys[1]) + + if err == nil { + if len(entries) > 0 { + fmt.Printf("%s", getColor(strings.ToLower(settings.Color))) + printDelim(settings.Delim, settings.Color) + for _, entry := range entries { + printEntry(&entry, false) + } + } else { + fmt.Println("No entries.") + } + } else { + fmt.Printf("Error fetching entries: \"%s\"\n", err.Error()) + return err + } + + // If max krypt on - then autodecrypt on call and auto encrypt after call + if maxKrypt { + err = encryptDatabase(defaultDB, &passwd) + } + + return err } // Find current entry by term - prints all matches func findCurrentEntry(term string) error { - var err error - var entries []Entry - - if err = checkActiveDatabase(); err != nil { - return err - } - - err, entries = searchDatabaseEntry(term) - if err != nil || len(entries) == 0 { - fmt.Printf("Entry for query \"%s\" not found\n", term) - return err - } else { - var delim bool - var pcopy bool - - if len(entries) == 1 { - delim = true - pcopy = true - // Single entry means copy password can be enabled - } else { - _, settings := getOrCreateLocalConfig(APP) - fmt.Printf("%s", getColor(strings.ToLower(settings.Color))) - printDelim(settings.Delim, settings.Color) - } - - for _, entry := range entries { - printEntry(&entry, delim) - } - - if pcopy && settingsRider.CopyPassword { - // Single entry - copyPasswordToClipboard(entries[0].Password) - } - } - - return err + var err error + var entries []Entry + + if err = checkActiveDatabase(); err != nil { + return err + } + + err, entries = searchDatabaseEntry(term) + if err != nil || len(entries) == 0 { + fmt.Printf("Entry for query \"%s\" not found\n", term) + return err + } else { + var delim bool + var pcopy bool + + if len(entries) == 1 { + delim = true + pcopy = true + // Single entry means copy password can be enabled + } else { + _, settings := getOrCreateLocalConfig(APP) + fmt.Printf("%s", getColor(strings.ToLower(settings.Color))) + printDelim(settings.Delim, settings.Color) + } + + for _, entry := range entries { + printEntry(&entry, delim) + } + + if pcopy && settingsRider.CopyPassword { + // Single entry + copyPasswordToClipboard(entries[0].Password) + } + } + + return err } // Remove a range of entries - say 10-14 func removeMultipleEntries(idRangeEntry string) error { - var err error - var idRange []string - var id1, id2 int + var err error + var idRange []string + var id1, id2 int - idRange = strings.Split(idRangeEntry, "-") + idRange = strings.Split(idRangeEntry, "-") - if len(idRange) != 2 { - fmt.Println("Invalid id range - " + idRangeEntry) - return errors.New("Invalid id range - " + idRangeEntry) - } + if len(idRange) != 2 { + fmt.Println("Invalid id range - " + idRangeEntry) + return errors.New("Invalid id range - " + idRangeEntry) + } - id1, _ = strconv.Atoi(idRange[0]) - id2, _ = strconv.Atoi(idRange[1]) + id1, _ = strconv.Atoi(idRange[0]) + id2, _ = strconv.Atoi(idRange[1]) - if id1 >= id2 { - fmt.Println("Invalid id range - " + idRangeEntry) - return errors.New("Invalid id range - " + idRangeEntry) - } + if id1 >= id2 { + fmt.Println("Invalid id range - " + idRangeEntry) + return errors.New("Invalid id range - " + idRangeEntry) + } - for idNum := id1; idNum <= id2; idNum++ { - err = removeCurrentEntry(fmt.Sprintf("%d", idNum)) - } + for idNum := id1; idNum <= id2; idNum++ { + err = removeCurrentEntry(fmt.Sprintf("%d", idNum)) + } - return err + return err } // Remove current entry by id func removeCurrentEntry(idString string) error { - var err error - var entry *Entry - var id int - var response string - - if err = checkActiveDatabase(); err != nil { - return err - } - - if strings.Contains(idString, "-") { - return removeMultipleEntries(idString) - } - - id, _ = strconv.Atoi(idString) - - err, entry = getEntryById(id) - if err != nil || entry == nil { - fmt.Printf("No entry with id %d was found\n", id) - return err - } - - printEntryMinimal(entry, true) - - response = readInput(bufio.NewReader(os.Stdin), "Please confirm removal [Y/n]: ") - - if strings.ToLower(response) != "n" { - // Drop from the database - err = removeDatabaseEntry(entry) - if err == nil { - fmt.Printf("Entry with id %d was removed from the database\n", id) - } - } else { - fmt.Println("Removal of entry cancelled by user.") - } - - return err + var err error + var entry *Entry + var id int + var response string + + if err = checkActiveDatabase(); err != nil { + return err + } + + if strings.Contains(idString, "-") { + return removeMultipleEntries(idString) + } + + id, _ = strconv.Atoi(idString) + + err, entry = getEntryById(id) + if err != nil || entry == nil { + fmt.Printf("No entry with id %d was found\n", id) + return err + } + + printEntryMinimal(entry, true) + + if !settingsRider.AssumeYes { + response = readInput(bufio.NewReader(os.Stdin), "Please confirm removal [Y/n]: ") + } else { + response = "y" + } + + if strings.ToLower(response) != "n" { + // Drop from the database + err = removeDatabaseEntry(entry) + if err == nil { + fmt.Printf("Entry with id %d was removed from the database\n", id) + } + } else { + fmt.Println("Removal of entry cancelled by user.") + } + + return err } // Copy current entry by id into new entry func copyCurrentEntry(idString string) error { - var err error - var entry *Entry - var id int + var err error + var entry *Entry + var id int - if err = checkActiveDatabase(); err != nil { - return err - } + if err = checkActiveDatabase(); err != nil { + return err + } - id, _ = strconv.Atoi(idString) + id, _ = strconv.Atoi(idString) - err, entry = getEntryById(id) - if err != nil || entry == nil { - fmt.Printf("No entry with id %d was found\n", id) - return err - } + err, entry = getEntryById(id) + if err != nil || entry == nil { + fmt.Printf("No entry with id %d was found\n", id) + return err + } - err, _ = cloneEntry(entry) - if err != nil { - fmt.Printf("Error cloning entry: \"%s\"\n", err.Error()) - return err - } + err, _ = cloneEntry(entry) + if err != nil { + fmt.Printf("Error cloning entry: \"%s\"\n", err.Error()) + return err + } - return err + return err } // Encrypt the active database func encryptActiveDatabase() error { - var err error - var dbPath string + var err error + var dbPath string - if err = checkActiveDatabase(); err != nil { - return err - } + if err = checkActiveDatabase(); err != nil { + return err + } - err, dbPath = getActiveDatabase() - if err != nil { - fmt.Printf("Error getting active database path - \"%s\"\n", err.Error()) - return err - } + err, dbPath = getActiveDatabase() + if err != nil { + fmt.Printf("Error getting active database path - \"%s\"\n", err.Error()) + return err + } - return encryptDatabase(dbPath, nil) + return encryptDatabase(dbPath, nil) } // Encrypt the database using AES func encryptDatabase(dbPath string, givenPasswd *string) error { - var err error - var passwd string - var passwd2 string - - // If password is given, use it - if givenPasswd != nil { - passwd = *givenPasswd - } - - if len(passwd) == 0 { - fmt.Printf("Password: ") - err, passwd = readPassword() - - if err == nil { - fmt.Printf("\nPassword again: ") - err, passwd2 = readPassword() - if err == nil { - if passwd != passwd2 { - fmt.Println("\nPassword mismatch.") - return errors.New("mismatched passwords") - } - } - } - - if err != nil { - fmt.Printf("Error reading password - \"%s\"\n", err.Error()) - return err - } - } - - // err = encryptFileAES(dbPath, passwd) - _, settings := getOrCreateLocalConfig(APP) - - switch settings.Cipher { - case "aes": - err = encryptFileAES(dbPath, passwd) - case "xchacha", "chacha", "xchachapoly": - err = encryptFileXChachaPoly(dbPath, passwd) - default: - fmt.Println("No cipher set, defaulting to AES") - err = encryptFileAES(dbPath, passwd) - } - - if err == nil { - fmt.Println("\nEncryption complete.") - } - - return err + var err error + var passwd string + var passwd2 string + + // If password is given, use it + if givenPasswd != nil { + passwd = *givenPasswd + } + + if len(passwd) == 0 { + fmt.Printf("Password: ") + err, passwd = readPassword() + + if err == nil { + fmt.Printf("\nPassword again: ") + err, passwd2 = readPassword() + if err == nil { + if passwd != passwd2 { + fmt.Println("\nPassword mismatch.") + return errors.New("mismatched passwords") + } + } + } + + if err != nil { + fmt.Printf("Error reading password - \"%s\"\n", err.Error()) + return err + } + } + + // err = encryptFileAES(dbPath, passwd) + _, settings := getOrCreateLocalConfig(APP) + + switch settings.Cipher { + case "aes": + err = encryptFileAES(dbPath, passwd) + case "xchacha", "chacha", "xchachapoly": + err = encryptFileXChachaPoly(dbPath, passwd) + default: + fmt.Println("No cipher set, defaulting to AES") + err = encryptFileAES(dbPath, passwd) + } + + if err == nil { + fmt.Println("\nEncryption complete.") + } + + return err } // Decrypt an encrypted database func decryptDatabase(dbPath string) (error, string) { - var err error - var passwd string - var flag bool + var err error + var passwd string + var flag bool - if err, flag = isFileEncrypted(dbPath); !flag { - fmt.Println(err.Error()) - return err, "" - } + if err, flag = isFileEncrypted(dbPath); !flag { + fmt.Println(err.Error()) + return err, "" + } - fmt.Printf("Password: ") - err, passwd = readPassword() + fmt.Printf("Password: ") + err, passwd = readPassword() - if err != nil { - fmt.Printf("\nError reading password - \"%s\"\n", err.Error()) - return err, "" - } + if err != nil { + fmt.Printf("\nError reading password - \"%s\"\n", err.Error()) + return err, "" + } - _, settings := getOrCreateLocalConfig(APP) + _, settings := getOrCreateLocalConfig(APP) - switch settings.Cipher { - case "aes": - err = decryptFileAES(dbPath, passwd) - case "xchacha", "chacha", "xchachapoly": - err = decryptFileXChachaPoly(dbPath, passwd) - default: - fmt.Println("No cipher set, defaulting to AES") - err = decryptFileAES(dbPath, passwd) - } + switch settings.Cipher { + case "aes": + err = decryptFileAES(dbPath, passwd) + case "xchacha", "chacha", "xchachapoly": + err = decryptFileXChachaPoly(dbPath, passwd) + default: + fmt.Println("No cipher set, defaulting to AES") + err = decryptFileAES(dbPath, passwd) + } - if err == nil { - fmt.Println("\nDecryption complete.") - } + if err == nil { + fmt.Println("\nDecryption complete.") + } - return err, passwd + return err, passwd } // Export data to a varity of file types func exportToFile(fileName string) error { - var err error - var maxKrypt bool - var defaultDB string - var passwd string - - ext := strings.ToLower(filepath.Ext(fileName)) - - maxKrypt, defaultDB = isActiveDatabaseEncryptedAndMaxKryptOn() - - if ext == ".csv" || ext == ".md" || ext == ".html" || ext == ".pdf" { - // If max krypt on - then autodecrypt on call and auto encrypt after call - if maxKrypt { - err, passwd = decryptDatabase(defaultDB) - if err != nil { - return err - } - } - } - - switch ext { - case ".csv": - err = exportToCsv(fileName) - case ".md": - err = exportToMarkdown(fileName) - case ".html": - err = exportToHTML(fileName) - case ".pdf": - err = exportToPDF(fileName) - default: - fmt.Printf("Error - extn %s not supported\n", ext) - return fmt.Errorf("format %s not supported", ext) - } - - if err != nil { - fmt.Printf("Error exporting to \"%s\" - \"%s\"\n", fileName, err.Error()) - return err - } else { - if _, err = os.Stat(fileName); err == nil { - fmt.Printf("Exported to %s.\n", fileName) - // Chmod 600 - os.Chmod(fileName, 0600) - - // If max krypt on - then autodecrypt on call and auto encrypt after call - if maxKrypt { - err = encryptDatabase(defaultDB, &passwd) - } - - return err - } - } - - return err + var err error + var maxKrypt bool + var defaultDB string + var passwd string + + ext := strings.ToLower(filepath.Ext(fileName)) + + maxKrypt, defaultDB = isActiveDatabaseEncryptedAndMaxKryptOn() + + if ext == ".csv" || ext == ".md" || ext == ".html" || ext == ".pdf" { + // If max krypt on - then autodecrypt on call and auto encrypt after call + if maxKrypt { + err, passwd = decryptDatabase(defaultDB) + if err != nil { + return err + } + } + } + + switch ext { + case ".csv": + err = exportToCsv(fileName) + case ".md": + err = exportToMarkdown(fileName) + case ".html": + err = exportToHTML(fileName) + case ".pdf": + err = exportToPDF(fileName) + default: + fmt.Printf("Error - extn %s not supported\n", ext) + return fmt.Errorf("format %s not supported", ext) + } + + if err != nil { + fmt.Printf("Error exporting to \"%s\" - \"%s\"\n", fileName, err.Error()) + return err + } else { + if _, err = os.Stat(fileName); err == nil { + fmt.Printf("Exported to %s.\n", fileName) + // Chmod 600 + os.Chmod(fileName, 0600) + + // If max krypt on - then autodecrypt on call and auto encrypt after call + if maxKrypt { + err = encryptDatabase(defaultDB, &passwd) + } + + return err + } + } + + return err } // Export current database to markdown func exportToMarkdown(fileName string) error { - var err error - var dataArray [][]string - var fh *os.File - var maxLengths [7]int - var headers []string = []string{" ID ", " Title ", " User ", " URL ", " Password ", " Notes ", " Modified "} - - err, dataArray = entriesToStringArray(false) - - if err != nil { - fmt.Printf("Error exporting entries to string array - \"%s\"\n", err.Error()) - return err - } - - for _, record := range dataArray { - for idx, field := range record { - - if len(field) > maxLengths[idx] { - maxLengths[idx] = len(field) - } - } - } - - // fmt.Printf("%+v\n", maxLengths) - fh, err = os.Create(fileName) - if err != nil { - fmt.Printf("Cannt open \"%s\" for writing - \"%s\"\n", fileName, err.Error()) - return err - } - - defer fh.Close() - - writer := bufio.NewWriter(fh) - - // Write markdown header - for idx, length := range maxLengths { - delta := length - len(headers[idx]) - // fmt.Printf("%d\n", delta) - if delta > 0 { - for i := 0; i < delta+2; i++ { - headers[idx] += " " - } - } - } - - writer.WriteString(" |" + strings.Join(headers, "|") + "|\n") - - // Write line separator - writer.WriteString(" | ") - for _, length := range maxLengths { - - for i := 0; i < length; i++ { - writer.WriteString("-") - } - writer.WriteString(" | ") - } - writer.WriteString("\n") - - // Write records - for _, record := range dataArray { - writer.WriteString(" | ") - for _, field := range record { - writer.WriteString(field + " | ") - } - writer.WriteString("\n") - } - - writer.Flush() - - return nil + var err error + var dataArray [][]string + var fh *os.File + var maxLengths [7]int + var headers []string = []string{" ID ", " Title ", " User ", " URL ", " Password ", " Notes ", " Modified "} + + err, dataArray = entriesToStringArray(false) + + if err != nil { + fmt.Printf("Error exporting entries to string array - \"%s\"\n", err.Error()) + return err + } + + for _, record := range dataArray { + for idx, field := range record { + + if len(field) > maxLengths[idx] { + maxLengths[idx] = len(field) + } + } + } + + // fmt.Printf("%+v\n", maxLengths) + fh, err = os.Create(fileName) + if err != nil { + fmt.Printf("Cannt open \"%s\" for writing - \"%s\"\n", fileName, err.Error()) + return err + } + + defer fh.Close() + + writer := bufio.NewWriter(fh) + + // Write markdown header + for idx, length := range maxLengths { + delta := length - len(headers[idx]) + // fmt.Printf("%d\n", delta) + if delta > 0 { + for i := 0; i < delta+2; i++ { + headers[idx] += " " + } + } + } + + writer.WriteString(" |" + strings.Join(headers, "|") + "|\n") + + // Write line separator + writer.WriteString(" | ") + for _, length := range maxLengths { + + for i := 0; i < length; i++ { + writer.WriteString("-") + } + writer.WriteString(" | ") + } + writer.WriteString("\n") + + // Write records + for _, record := range dataArray { + writer.WriteString(" | ") + for _, field := range record { + writer.WriteString(field + " | ") + } + writer.WriteString("\n") + } + + writer.Flush() + + return nil } // This needs pandoc and pdflatex support func exportToPDF(fileName string) error { - var err error - var tmpFile string - var passwd string - var pdfTkFound bool - - cmd := exec.Command("which", "pandoc") - if _, err = cmd.Output(); err != nil { - return errors.New("pandoc not found") - } - - cmd = exec.Command("which", "pdftk") - if _, err = cmd.Output(); err != nil { - fmt.Printf("pdftk not found, PDF won't be secure!\n") - } else { - pdfTkFound = true - } - - if pdfTkFound { - fmt.Printf("PDF Encryption Password: ") - err, passwd = readPassword() - } - - tmpFile = randomFileName(os.TempDir(), ".tmp") - // fmt.Printf("Temp file => %s\n", tmpFile) - err = exportToMarkdownLimited(tmpFile) - - if err == nil { - var args []string = []string{"-o", fileName, "-f", "markdown", "-V", "geometry:landscape", "--columns=600", "--pdf-engine", "xelatex", "--dpi=150", tmpFile} - - cmd = exec.Command("pandoc", args...) - _, err = cmd.Output() - // Remove tmpfile - os.Remove(tmpFile) - - // If the file is generated, encrypt it if pdfTkFound - if _, err = os.Stat(fileName); err == nil { - fmt.Printf("\nFile %s created without password.\n", fileName) - - if pdfTkFound && len(passwd) > 0 { - tmpFile = randomFileName(".", ".pdf") - // fmt.Printf("pdf file => %s\n", tmpFile) - args = []string{fileName, "output", tmpFile, "user_pw", passwd} - cmd = exec.Command("pdftk", args...) - _, err = cmd.Output() - - if err == nil { - // Copy over - fmt.Printf("Added password to %s.\n", fileName) - os.Remove(fileName) - err = os.Rename(tmpFile, fileName) - } else { - fmt.Printf("Error adding password to pdf - \"%s\"\n", err.Error()) - } - } - } - } - - return err + var err error + var tmpFile string + var passwd string + var pdfTkFound bool + + cmd := exec.Command("which", "pandoc") + if _, err = cmd.Output(); err != nil { + return errors.New("pandoc not found") + } + + cmd = exec.Command("which", "pdftk") + if _, err = cmd.Output(); err != nil { + fmt.Printf("pdftk not found, PDF won't be secure!\n") + } else { + pdfTkFound = true + } + + if pdfTkFound { + fmt.Printf("PDF Encryption Password: ") + err, passwd = readPassword() + } + + tmpFile = randomFileName(os.TempDir(), ".tmp") + // fmt.Printf("Temp file => %s\n", tmpFile) + err = exportToMarkdownLimited(tmpFile) + + if err == nil { + var args []string = []string{"-o", fileName, "-f", "markdown", "-V", "geometry:landscape", "--columns=600", "--pdf-engine", "xelatex", "--dpi=150", tmpFile} + + cmd = exec.Command("pandoc", args...) + _, err = cmd.Output() + // Remove tmpfile + os.Remove(tmpFile) + + // If the file is generated, encrypt it if pdfTkFound + if _, err = os.Stat(fileName); err == nil { + fmt.Printf("\nFile %s created without password.\n", fileName) + + if pdfTkFound && len(passwd) > 0 { + tmpFile = randomFileName(".", ".pdf") + // fmt.Printf("pdf file => %s\n", tmpFile) + args = []string{fileName, "output", tmpFile, "user_pw", passwd} + cmd = exec.Command("pdftk", args...) + _, err = cmd.Output() + + if err == nil { + // Copy over + fmt.Printf("Added password to %s.\n", fileName) + os.Remove(fileName) + err = os.Rename(tmpFile, fileName) + } else { + fmt.Printf("Error adding password to pdf - \"%s\"\n", err.Error()) + } + } + } + } + + return err } // Export current database to markdown minus the long fields func exportToMarkdownLimited(fileName string) error { - var err error - var dataArray [][]string - var fh *os.File - var maxLengths [5]int - var headers []string = []string{" ID ", " Title ", " User ", " Password ", " Modified "} - - err, dataArray = entriesToStringArray(true) - - if err != nil { - fmt.Printf("Error exporting entries to string array - \"%s\"\n", err.Error()) - return err - } - - for _, record := range dataArray { - for idx, field := range record { - - if len(field) > maxLengths[idx] { - maxLengths[idx] = len(field) - } - } - } - - // fmt.Printf("%+v\n", maxLengths) - fh, err = os.Create(fileName) - if err != nil { - fmt.Printf("Cannt open \"%s\" for writing - \"%s\"\n", fileName, err.Error()) - return err - } - - defer fh.Close() - - writer := bufio.NewWriter(fh) - - // Write markdown header - for idx, length := range maxLengths { - delta := length - len(headers[idx]) - // fmt.Printf("%d\n", delta) - if delta > 0 { - for i := 0; i < delta+2; i++ { - headers[idx] += " " - } - } - } - - writer.WriteString(" |" + strings.Join(headers, "|") + "|\n") - - // Write line separator - writer.WriteString(" | ") - for _, length := range maxLengths { - - for i := 0; i < length; i++ { - writer.WriteString("-") - } - writer.WriteString(" | ") - } - writer.WriteString("\n") - - // Write records - for _, record := range dataArray { - writer.WriteString(" | ") - for _, field := range record { - writer.WriteString(field + " | ") - } - writer.WriteString("\n") - } - - writer.Flush() - - return nil + var err error + var dataArray [][]string + var fh *os.File + var maxLengths [5]int + var headers []string = []string{" ID ", " Title ", " User ", " Password ", " Modified "} + + err, dataArray = entriesToStringArray(true) + + if err != nil { + fmt.Printf("Error exporting entries to string array - \"%s\"\n", err.Error()) + return err + } + + for _, record := range dataArray { + for idx, field := range record { + + if len(field) > maxLengths[idx] { + maxLengths[idx] = len(field) + } + } + } + + // fmt.Printf("%+v\n", maxLengths) + fh, err = os.Create(fileName) + if err != nil { + fmt.Printf("Cannt open \"%s\" for writing - \"%s\"\n", fileName, err.Error()) + return err + } + + defer fh.Close() + + writer := bufio.NewWriter(fh) + + // Write markdown header + for idx, length := range maxLengths { + delta := length - len(headers[idx]) + // fmt.Printf("%d\n", delta) + if delta > 0 { + for i := 0; i < delta+2; i++ { + headers[idx] += " " + } + } + } + + writer.WriteString(" |" + strings.Join(headers, "|") + "|\n") + + // Write line separator + writer.WriteString(" | ") + for _, length := range maxLengths { + + for i := 0; i < length; i++ { + writer.WriteString("-") + } + writer.WriteString(" | ") + } + writer.WriteString("\n") + + // Write records + for _, record := range dataArray { + writer.WriteString(" | ") + for _, field := range record { + writer.WriteString(field + " | ") + } + writer.WriteString("\n") + } + + writer.Flush() + + return nil } // Export current database to html func exportToHTML(fileName string) error { - var err error - var dataArray [][]string - var fh *os.File - var headers []string = []string{" ID ", " Title ", " User ", " URL ", " Password ", " Notes ", " Modified "} + var err error + var dataArray [][]string + var fh *os.File + var headers []string = []string{" ID ", " Title ", " User ", " URL ", " Password ", " Notes ", " Modified "} - err, dataArray = entriesToStringArray(false) + err, dataArray = entriesToStringArray(false) - if err != nil { - fmt.Printf("Error exporting entries to string array - \"%s\"\n", err.Error()) - return err - } + if err != nil { + fmt.Printf("Error exporting entries to string array - \"%s\"\n", err.Error()) + return err + } - // fmt.Printf("%+v\n", maxLengths) - fh, err = os.Create(fileName) - if err != nil { - fmt.Printf("Cannt open \"%s\" for writing - \"%s\"\n", fileName, err.Error()) - return err - } + // fmt.Printf("%+v\n", maxLengths) + fh, err = os.Create(fileName) + if err != nil { + fmt.Printf("Cannt open \"%s\" for writing - \"%s\"\n", fileName, err.Error()) + return err + } - defer fh.Close() + defer fh.Close() - writer := bufio.NewWriter(fh) + writer := bufio.NewWriter(fh) - writer.WriteString("\n") - writer.WriteString("\n") - writer.WriteString("\n") + writer.WriteString("\n") + writer.WriteString("
\n") + writer.WriteString("\n") - for _, h := range headers { - writer.WriteString(fmt.Sprintf("", h)) - } - writer.WriteString("\n") - writer.WriteString("\n") + for _, h := range headers { + writer.WriteString(fmt.Sprintf("", h)) + } + writer.WriteString("\n") + writer.WriteString("\n") - // Write records - for _, record := range dataArray { - writer.WriteString("") - for _, field := range record { - writer.WriteString(fmt.Sprintf("", field)) - } - writer.WriteString("\n") - } - writer.WriteString("\n") - writer.WriteString("
%s
%s
%s
\n") + // Write records + for _, record := range dataArray { + writer.WriteString("") + for _, field := range record { + writer.WriteString(fmt.Sprintf("%s", field)) + } + writer.WriteString("\n") + } + writer.WriteString("\n") + writer.WriteString("\n") - writer.WriteString("\n") + writer.WriteString("\n") - writer.Flush() + writer.Flush() - return nil + return nil } // Export current database to CSV func exportToCsv(fileName string) error { - var err error - var dataArray [][]string - var fh *os.File + var err error + var dataArray [][]string + var fh *os.File - err, dataArray = entriesToStringArray(false) + err, dataArray = entriesToStringArray(false) - if err != nil { - fmt.Printf("Error exporting entries to string array - \"%s\"\n", err.Error()) - return err - } + if err != nil { + fmt.Printf("Error exporting entries to string array - \"%s\"\n", err.Error()) + return err + } - fh, err = os.Create(fileName) - if err != nil { - fmt.Printf("Cannt open \"%s\" for writing - \"%s\"\n", fileName, err.Error()) - return err - } + fh, err = os.Create(fileName) + if err != nil { + fmt.Printf("Cannt open \"%s\" for writing - \"%s\"\n", fileName, err.Error()) + return err + } - writer := csv.NewWriter(fh) + writer := csv.NewWriter(fh) - // Write header - writer.Write([]string{"ID", "Title", "User", "URL", "Password", "Notes", "Modified"}) + // Write header + writer.Write([]string{"ID", "Title", "User", "URL", "Password", "Notes", "Modified"}) - for idx, record := range dataArray { - if err = writer.Write(record); err != nil { - fmt.Printf("Error writing record #%d to %s - \"%s\"\n", idx+1, fileName, err.Error()) - break - } - } + for idx, record := range dataArray { + if err = writer.Write(record); err != nil { + fmt.Printf("Error writing record #%d to %s - \"%s\"\n", idx+1, fileName, err.Error()) + break + } + } - writer.Flush() + writer.Flush() - if err != nil { - return err - } + if err != nil { + return err + } - os.Chmod(fileName, 0600) - fmt.Printf("!WARNING: Passwords are stored in plain-text!\n") - fmt.Printf("Exported %d records to %s .\n", len(dataArray), fileName) + os.Chmod(fileName, 0600) + fmt.Printf("!WARNING: Passwords are stored in plain-text!\n") + fmt.Printf("Exported %d records to %s .\n", len(dataArray), fileName) - return nil + return nil } From d5a4b6033205dfacbc82daab623a6163451f30e0 Mon Sep 17 00:00:00 2001 From: Anand Date: Tue, 14 Dec 2021 10:36:02 +0530 Subject: [PATCH 11/27] ref issue #21 - Added assume-yes option for removal --- main.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/main.go b/main.go index 0df1d8f..a182f13 100644 --- a/main.go +++ b/main.go @@ -32,7 +32,7 @@ type CmdOption struct { // Print the program's usage string and exit func printUsage() error { - // getopt.Usage() + // getopt.Usage() os.Exit(0) return nil @@ -102,8 +102,9 @@ func performAction(optMap map[string]interface{}) { } flagsActionsMap := map[string]voidFunc{ - "show": setShowPasswords, - "copy": setCopyPasswordToClipboard, + "show": setShowPasswords, + "copy": setCopyPasswordToClipboard, + "assume-yes": setAssumeYes, } // Flag actions - always done @@ -168,7 +169,7 @@ func initializeCmdLine(parser *argparse.Parser) map[string]interface{} { {"I", "init", "Initialize a new database", "", ""}, {"d", "decrypt", "Decrypt password database", "", ""}, {"C", "clone", "Clone an entry with ", "", ""}, - {"R", "remove", "Remove an entry with ", "", ""}, + {"R", "remove", "Remove an entry with or ", "", ""}, {"U", "use-db", "Set as active database", "", ""}, {"f", "find", "Search entries with ", "", ""}, {"E", "edit", "Edit entry by ", "", ""}, @@ -188,6 +189,7 @@ func initializeCmdLine(parser *argparse.Parser) map[string]interface{} { {"g", "genpass", "Generate a strong password (length: 12 - 16)", "", ""}, {"s", "show", "Show passwords when listing entries", "", ""}, {"c", "copy", "Copy password to clipboard", "", ""}, + {"y", "assume-yes", "Assume yes to actions requiring confirmation", "", ""}, {"v", "version", "Show version information and exit", "", ""}, {"h", "help", "Print this help message and exit", "", ""}, } From 479a806ee92377a107af4a699ee5387b8991a9b4 Mon Sep 17 00:00:00 2001 From: Anand Date: Tue, 14 Dec 2021 10:36:16 +0530 Subject: [PATCH 12/27] ref issue #21 - Added assume-yes option for removal --- utils.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/utils.go b/utils.go index abd84f7..774b783 100644 --- a/utils.go +++ b/utils.go @@ -22,6 +22,7 @@ const DELIMSIZE int = 69 type SettingsOverride struct { ShowPasswords bool CopyPassword bool + AssumeYes bool } // Settings structure for local config @@ -444,6 +445,11 @@ func setCopyPasswordToClipboard() error { return nil } +func setAssumeYes() error { + settingsRider.AssumeYes = true + return nil +} + func copyPasswordToClipboard(passwd string) { clipboard.WriteAll(passwd) } From e9e697f0a563e9e0da4a22fa1c9c04fa6a5f79af Mon Sep 17 00:00:00 2001 From: Anand Date: Tue, 14 Dec 2021 15:38:55 +0530 Subject: [PATCH 13/27] ref issue #10 - Search multiple terms using AND operator --- actions.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/actions.go b/actions.go index 35c8ae2..14cf150 100644 --- a/actions.go +++ b/actions.go @@ -511,12 +511,15 @@ func findCurrentEntry(term string) error { var err error var entries []Entry + var terms []string if err = checkActiveDatabase(); err != nil { return err } - err, entries = searchDatabaseEntry(term) + terms = strings.Split(term, " ") + + err, entries = searchDatabaseEntries(terms, "AND") if err != nil || len(entries) == 0 { fmt.Printf("Entry for query \"%s\" not found\n", term) return err From 03e5ef01ef476c3a4e32897adfc89f2146f9cd29 Mon Sep 17 00:00:00 2001 From: Anand Date: Tue, 14 Dec 2021 15:39:21 +0530 Subject: [PATCH 14/27] ref issue #10 - Search multiple terms using AND operator --- db.go | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/db.go b/db.go index 423085f..410e1a0 100644 --- a/db.go +++ b/db.go @@ -229,7 +229,7 @@ func addNewDatabaseEntry(title, userName, url, passwd, notes string, customEntri err, db = openActiveDatabase() if err == nil && db != nil { - // result := db.Debug().Create(&entry) + // result := db.Debug().Create(&entry) result := db.Create(&entry) if result.Error == nil && result.RowsAffected == 1 { // Add custom fields if given @@ -341,6 +341,73 @@ func searchDatabaseEntry(term string) (error, []Entry) { } +// Union of two entry arrays +func union(entry1 []Entry, entry2 []Entry) []Entry { + + m := make(map[int]bool) + + for _, item := range entry1 { + m[item.ID] = true + } + + for _, item := range entry2 { + if _, ok := m[item.ID]; !ok { + entry1 = append(entry1, item) + } + } + + return entry1 +} + +// Intersection of two entry arrays +func intersection(entry1 []Entry, entry2 []Entry) []Entry { + + var common []Entry + + m := make(map[int]bool) + + for _, item := range entry1 { + m[item.ID] = true + } + + for _, item := range entry2 { + if _, ok := m[item.ID]; ok { + common = append(common, item) + } + } + + return common +} + +// Search database for the given terms and returns matches according to operator +func searchDatabaseEntries(terms []string, operator string) (error, []Entry) { + + var err error + var finalEntries []Entry + + for idx, term := range terms { + var entries []Entry + + err, entries = searchDatabaseEntry(term) + if err != nil { + fmt.Printf("Error searching for term: %s - \"%s\"\n", term, err.Error()) + return err, entries + } + + if idx == 0 { + finalEntries = entries + } else { + if operator == "AND" { + finalEntries = intersection(finalEntries, entries) + } else if operator == "OR" { + finalEntries = union(finalEntries, entries) + } + } + } + + return nil, finalEntries +} + // Remove a given database entry func removeDatabaseEntry(entry *Entry) error { From c3bbb258c126e59942c8b3d0cca5e8f8dbf80674 Mon Sep 17 00:00:00 2001 From: Anand Date: Tue, 14 Dec 2021 15:39:47 +0530 Subject: [PATCH 15/27] ref issue #10 - Search multiple terms using AND operator --- main.go | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index a182f13..026de6f 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/pythonhacker/argparse" "os" + "strings" ) const VERSION = 0.3 @@ -86,13 +87,16 @@ func performAction(optMap map[string]interface{}) { "edit": WrapperMaxKryptStringFunc(editCurrentEntry), "init": initNewDatabase, "list-entry": WrapperMaxKryptStringFunc(listCurrentEntry), - "find": WrapperMaxKryptStringFunc(findCurrentEntry), "remove": WrapperMaxKryptStringFunc(removeCurrentEntry), "clone": WrapperMaxKryptStringFunc(copyCurrentEntry), "use-db": setActiveDatabasePath, "export": exportToFile, } + stringListActionsMap := map[string]actionFunc{ + "find": WrapperMaxKryptStringFunc(findCurrentEntry), + } + stringActions2Map := map[string]actionFunc2{ "decrypt": decryptDatabase, } @@ -146,6 +150,18 @@ func performAction(optMap map[string]interface{}) { } } + for key, mappedFunc := range stringListActionsMap { + if len(*optMap[key].(*[]string)) > 0 { + + var vals = *(optMap[key].(*[]string)) + // Convert to single string + var singleVal = strings.Join(vals, " ") + mappedFunc(singleVal) + flag = true + break + } + } + if flag { return } @@ -171,7 +187,6 @@ func initializeCmdLine(parser *argparse.Parser) map[string]interface{} { {"C", "clone", "Clone an entry with ", "", ""}, {"R", "remove", "Remove an entry with or ", "", ""}, {"U", "use-db", "Set as active database", "", ""}, - {"f", "find", "Search entries with ", "", ""}, {"E", "edit", "Edit entry by ", "", ""}, {"l", "list-entry", "List entry by ", "", ""}, {"x", "export", "Export all entries to ", "", ""}, @@ -181,6 +196,14 @@ func initializeCmdLine(parser *argparse.Parser) map[string]interface{} { optMap[opt.Long] = parser.String(opt.Short, opt.Long, &argparse.Options{Help: opt.Help, Path: opt.Path}) } + stringListOptions := []CmdOption{ + {"f", "find", "Search entries with terms", " ...", ""}, + } + + for _, opt := range stringListOptions { + optMap[opt.Long] = parser.StringList(opt.Short, opt.Long, &argparse.Options{Help: opt.Help, Path: opt.Path}) + } + boolOptions := []CmdOption{ {"e", "encrypt", "Encrypt the current database", "", ""}, {"A", "add", "Add a new entry", "", ""}, From 3624934f5e437b3e1c5c25142b1ad52363f70ef4 Mon Sep 17 00:00:00 2001 From: Anand Date: Tue, 14 Dec 2021 15:43:30 +0530 Subject: [PATCH 16/27] ref issue #10 - Added readme entry for searching with multiple terms --- README.md | 68 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 970efa3..512d257 100644 --- a/README.md +++ b/README.md @@ -69,38 +69,39 @@ The binary will be installed in `/usr/local/bin` folder. Usage ===== - $ ./varuh -h + $ varuh -h usage: varuh [-h|--help] [-I|--init ""] [-d|--decrypt ""] [-C|--clone ""] [-R|--remove ""] [-U|--use-db - ""] [-f|--find ""] [-E|--edit ""] - [-l|--list-entry ""] [-x|--export ""] [-e|--encrypt] - [-A|--add] [-p|--path] [-a|--list-all] [-g|--genpass] [-s|--show] - [-c|--copy] [-y|--assume-yes] [-v|--version] + ""] [-E|--edit ""] [-l|--list-entry ""] + [-x|--export ""] [-f|--find "" [-f|--find "" + ...]] [-e|--encrypt] [-A|--add] [-p|--path] [-a|--list-all] + [-g|--genpass] [-s|--show] [-c|--copy] [-y|--assume-yes] + [-v|--version] Password manager for the command line for Unix like operating systems Options: - -h --help Print help information - -I --init Initialize a new database - -d --decrypt Decrypt password database - -C --clone Clone an entry with - -R --remove Remove an entry with or - -U --use-db Set as active database - -f --find Search entries with - -E --edit Edit entry by - -l --list-entry List entry by - -x --export Export all entries to - -e --encrypt Encrypt the current database - -A --add Add a new entry - -p --path Show current database path - -a --list-all List all entries in current database - -g --genpass Generate a strong password (length: 12 - 16) - -s --show Show passwords when listing entries - -c --copy Copy password to clipboard - -y --assume-yes Assume yes to actions requiring confirmation - -v --version Show version information and exit + -h --help Print help information + -I --init Initialize a new database + -d --decrypt Decrypt password database + -C --clone Clone an entry with + -R --remove Remove an entry with or + -U --use-db Set as active database + -E --edit Edit entry by + -l --list-entry List entry by + -x --export Export all entries to + -f --find ... Search entries with terms + -e --encrypt Encrypt the current database + -A --add Add a new entry + -p --path Show current database path + -a --list-all List all entries in current database + -g --genpass Generate a strong password (length: 12 - 16) + -s --show Show passwords when listing entries + -c --copy Copy password to clipboard + -y --assume-yes Assume yes to actions requiring confirmation + -v --version Show version information and exit AUTHORS @@ -441,6 +442,25 @@ An entry can be searched on its title, username, URL or notes. Search is case-in Modified: 2021-21-25 15:09:51 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +## To search using multiple terms + +The `-f` option supports multiple terms, so you can specify this more than one time to narrow a search down to a specific entry. + + $ varuh -f google -f anand + +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + ID: 8 + Title: Google account + User: anandpillai@alumni.iitm.ac.in + URL: + Password: ********** + Notes: + Modified: 2021-21-25 15:02:50 + +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + $ varuh -f google -f priya + Entry for "google priya" not found + ## To list all entries To list all entries, use the option `-a`. From e0ec7ac47a2c44a0eb5df1d05e552622d4124102 Mon Sep 17 00:00:00 2001 From: Anand Date: Sun, 2 Jan 2022 13:17:36 +0530 Subject: [PATCH 17/27] fixed bugs issue #32 #33 #34 - added migrate option and tag as a regular field - issues #30 #31 --- db.go | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 4 deletions(-) diff --git a/db.go b/db.go index 410e1a0..fa2f75a 100644 --- a/db.go +++ b/db.go @@ -22,6 +22,7 @@ type Entry struct { Url string `gorm:"column:url"` Password string `gorm:"column:password"` Notes string `gorm:"column:notes"` + Tags string `gorm:"column:tags"` Timestamp time.Time `gorm:"type:timestamp;default:(datetime('now','localtime'))"` // sqlite3 } @@ -56,6 +57,16 @@ func (e1 *Entry) Copy(e2 *Entry) { } } +// Clone an entry +func (e1 *ExtendedEntry) Copy(e2 *ExtendedEntry) { + + if e2 != nil { + e1.FieldName = e2.FieldName + e1.FieldValue = e2.FieldValue + e1.EntryID = e2.EntryID + } +} + // Create a new database func openDatabase(filePath string) (error, *gorm.DB) { @@ -219,13 +230,15 @@ func replaceCustomEntries(db *gorm.DB, entry *Entry, updatedEntries []CustomEntr } // Add a new entry to current database -func addNewDatabaseEntry(title, userName, url, passwd, notes string, customEntries []CustomEntry) error { +func addNewDatabaseEntry(title, userName, url, passwd, tags string, + notes string, customEntries []CustomEntry) error { var entry Entry var err error var db *gorm.DB - entry = Entry{Title: title, User: userName, Url: url, Password: passwd, Notes: notes} + entry = Entry{Title: title, User: userName, Url: url, Password: passwd, Tags: strings.TrimSpace(tags), + Notes: notes} err, db = openActiveDatabase() if err == nil && db != nil { @@ -247,13 +260,20 @@ func addNewDatabaseEntry(title, userName, url, passwd, notes string, customEntri } // Update current database entry with new values -func updateDatabaseEntry(entry *Entry, title, userName, url, passwd, notes string, customEntries []CustomEntry, flag bool) error { +func updateDatabaseEntry(entry *Entry, title, userName, url, passwd, tags string, + notes string, customEntries []CustomEntry, flag bool) error { var updateMap map[string]interface{} updateMap = make(map[string]interface{}) - keyValMap := map[string]string{"title": title, "user": userName, "url": url, "password": passwd, "notes": notes} + keyValMap := map[string]string{ + "title": title, + "user": userName, + "url": url, + "password": passwd, + "notes": notes, + "tags": tags} for key, val := range keyValMap { if len(val) > 0 { @@ -416,11 +436,22 @@ func removeDatabaseEntry(entry *Entry) error { err, db = openActiveDatabase() if err == nil && db != nil { + var exEntries []ExtendedEntry + res := db.Delete(entry) if res.Error != nil { return res.Error } + // Delete extended entries if any + exEntries = getExtendedEntries(entry) + if len(exEntries) > 0 { + res = db.Delete(exEntries) + if res.Error != nil { + return res.Error + } + } + return nil } @@ -450,6 +481,31 @@ func cloneEntry(entry *Entry) (error, *Entry) { return err, nil } +// Clone extended entries for an entry and return error code +func cloneExtendedEntries(entry *Entry, exEntries []ExtendedEntry) error { + + var err error + var db *gorm.DB + + err, db = openActiveDatabase() + if err == nil && db != nil { + for _, exEntry := range exEntries { + var exEntryNew ExtendedEntry + + exEntryNew.Copy(&exEntry) + // Update the ID! + exEntryNew.EntryID = entry.ID + + result := db.Create(&exEntryNew) + if result.Error != nil { + return result.Error + } + } + } + + return err +} + // Return an iterator over all entries using the given order query keys func iterateEntries(orderKey string, order string) (error, []Entry) { From 90787455d8e777b52bd393f18557fd4ee200e9de Mon Sep 17 00:00:00 2001 From: Anand Date: Sun, 2 Jan 2022 13:17:47 +0530 Subject: [PATCH 18/27] fixed bugs issue #32 #33 #34 - added migrate option and tag as a regular field - issues #30 #31 --- actions.go | 114 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 97 insertions(+), 17 deletions(-) diff --git a/actions.go b/actions.go index 14cf150..09c94ca 100644 --- a/actions.go +++ b/actions.go @@ -6,6 +6,7 @@ import ( "encoding/csv" "errors" "fmt" + "gorm.io/gorm" "os" "os/exec" "os/signal" @@ -175,14 +176,19 @@ func setActiveDatabasePath(dbPath string) error { } if newEncrypted { - // Decrypt new database if it is encrypted - fmt.Printf("Database %s is encrypted, decrypting it\n", fullPath) - err, _ = decryptDatabase(fullPath) - if err != nil { - fmt.Printf("Decryption Error - \"%s\", not switching databases\n", err.Error()) - return err + if !settings.AutoEncrypt { + // Decrypt new database if it is encrypted + fmt.Printf("Database %s is encrypted, decrypting it\n", fullPath) + err, _ = decryptDatabase(fullPath) + if err != nil { + fmt.Printf("Decryption Error - \"%s\", not switching databases\n", err.Error()) + return err + } else { + newEncrypted = false + } } else { - newEncrypted = false + // New database is encrypted and autoencrypt is set - so keep it like that + // fmt.Printf("Database %s is already encrypted, nothing to do\n", fullPath) } } } @@ -193,7 +199,7 @@ func setActiveDatabasePath(dbPath string) error { return nil } - if newEncrypted { + if newEncrypted && !settings.AutoEncrypt { // Use should manually decrypt before switching fmt.Println("Auto-encrypt disabled, decrypt new database manually before switching.") return nil @@ -223,6 +229,7 @@ func addNewEntry() error { var url string var notes string var passwd string + var tags string var err error var customEntries []CustomEntry @@ -250,7 +257,8 @@ func addNewEntry() error { } // fmt.Printf("Password => %s\n", passwd) - notes = readInput(reader, "\nNotes") + tags = readInput(reader, "\nTags (separated by space): ") + notes = readInput(reader, "Notes") // Title and username/password are mandatory if len(title) == 0 { @@ -269,7 +277,7 @@ func addNewEntry() error { customEntries = addCustomFields(reader) // Trim spaces - err = addNewDatabaseEntry(title, userName, url, passwd, notes, customEntries) + err = addNewDatabaseEntry(title, userName, url, passwd, tags, notes, customEntries) if err != nil { fmt.Printf("Error adding entry - \"%s\"\n", err.Error()) @@ -299,7 +307,7 @@ func addOrUpdateCustomFields(reader *bufio.Reader, entry *Entry) ([]CustomEntry, fmt.Println("Field Name: " + customEntry.FieldName) fieldName = readInput(reader, "\tNew Field Name (Enter to keep, \"x\" to delete)") if strings.ToLower(strings.TrimSpace(fieldName)) == "x" { - fmt.Println("Deleting field " + fieldName) + fmt.Println("Deleting field: " + customEntry.FieldName) } else { if strings.TrimSpace(fieldName) == "" { fieldName = customEntry.FieldName @@ -365,6 +373,7 @@ func editCurrentEntry(idString string) error { var title string var url string var notes string + var tags string var passwd string var err error var entry *Entry @@ -407,13 +416,16 @@ func editCurrentEntry(idString string) error { } // fmt.Printf("Password => %s\n", passwd) + fmt.Printf("\nCurrent Tags: %s\n", entry.Tags) + tags = readInput(reader, "New Tags") + fmt.Printf("\nCurrent Notes: %s\n", entry.Notes) notes = readInput(reader, "New Notes") customEntries, flag := addOrUpdateCustomFields(reader, entry) // Update - err = updateDatabaseEntry(entry, title, userName, url, passwd, notes, customEntries, flag) + err = updateDatabaseEntry(entry, title, userName, url, passwd, tags, notes, customEntries, flag) if err != nil { fmt.Printf("Error updating entry - \"%s\"\n", err.Error()) } @@ -629,6 +641,9 @@ func copyCurrentEntry(idString string) error { var err error var entry *Entry + var entryNew *Entry + var exEntries []ExtendedEntry + var id int if err = checkActiveDatabase(); err != nil { @@ -643,12 +658,24 @@ func copyCurrentEntry(idString string) error { return err } - err, _ = cloneEntry(entry) + err, entryNew = cloneEntry(entry) if err != nil { fmt.Printf("Error cloning entry: \"%s\"\n", err.Error()) return err } + exEntries = getExtendedEntries(entry) + + if len(exEntries) > 0 { + fmt.Printf("%d extended entries found\n", len(exEntries)) + + err = cloneExtendedEntries(entryNew, exEntries) + if err != nil { + fmt.Printf("Error cloning extended entries: \"%s\"\n", err.Error()) + return err + } + } + return err } @@ -684,11 +711,11 @@ func encryptDatabase(dbPath string, givenPasswd *string) error { } if len(passwd) == 0 { - fmt.Printf("Password: ") + fmt.Printf("Encryption Password: ") err, passwd = readPassword() if err == nil { - fmt.Printf("\nPassword again: ") + fmt.Printf("\nEncryption Password again: ") err, passwd2 = readPassword() if err == nil { if passwd != passwd2 { @@ -736,7 +763,7 @@ func decryptDatabase(dbPath string) (error, string) { return err, "" } - fmt.Printf("Password: ") + fmt.Printf("Decryption Password: ") err, passwd = readPassword() if err != nil { @@ -757,12 +784,65 @@ func decryptDatabase(dbPath string) (error, string) { } if err == nil { - fmt.Println("\nDecryption complete.") + fmt.Println("...decryption complete.") } return err, passwd } +// Migrate an existing database to the new schema +func migrateDatabase(dbPath string) error { + + var err error + var flag bool + var passwd string + var db *gorm.DB + + if _, err = os.Stat(dbPath); os.IsNotExist(err) { + fmt.Printf("Error - path %s does not exist\n", dbPath) + return err + } + + if err, flag = isFileEncrypted(dbPath); flag { + err, passwd = decryptDatabase(dbPath) + } + + if err != nil { + return err + } + + err, db = openDatabase(dbPath) + + if err != nil { + fmt.Printf("Error opening database path - %s: %s\n", dbPath, err.Error()) + return err + } + + fmt.Println("Migrating tables ...") + err = db.AutoMigrate(&Entry{}) + + if err != nil { + fmt.Printf("Error migrating table \"entries\" - %s: %s\n", dbPath, err.Error()) + return err + } + + err = db.AutoMigrate(&ExtendedEntry{}) + + if err != nil { + fmt.Printf("Error migrating table \"exentries\" - %s: %s\n", dbPath, err.Error()) + return err + } + + if flag { + // File was encrypted - encrypt it again + encryptDatabase(dbPath, &passwd) + } + + fmt.Println("Migration successful.") + + return nil +} + // Export data to a varity of file types func exportToFile(fileName string) error { From 6eee3225f68d7f516a25245a9e58106a35d8c88b Mon Sep 17 00:00:00 2001 From: Anand Date: Sun, 2 Jan 2022 13:17:58 +0530 Subject: [PATCH 19/27] fixed bugs issue #32 #33 #34 - added migrate option and tag as a regular field - issues #30 #31 --- utils.go | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/utils.go b/utils.go index 774b783..57c3ac8 100644 --- a/utils.go +++ b/utils.go @@ -213,6 +213,22 @@ func rewriteBaseFile(path string, contents []byte, mode fs.FileMode) (error, str return err, origFile } +// Rewrite the contents of the file with the new contents +func rewriteFile(path string, contents []byte, mode fs.FileMode) (error, string) { + + var err error + + // Overwrite it + err = os.WriteFile(path, contents, 0644) + + if err == nil { + // Chmod it + os.Chmod(path, mode) + } + + return err, path +} + // Get color codes for console colors func getColor(code string) string { @@ -317,7 +333,13 @@ func printEntry(entry *Entry, delim bool) error { } fmt.Printf("Password: %s\n", strings.Join(asterisks, "")) } - fmt.Printf("Notes: %s\n", entry.Notes) + + if len(entry.Tags) > 0 { + fmt.Printf("Tags: %s\n", entry.Tags) + } + if len(entry.Notes) > 0 { + fmt.Printf("Notes: %s\n", entry.Notes) + } // Query extended entries customEntries = getExtendedEntries(entry) From 6c2130749338e938784c4cf1a66b69f1004e5738 Mon Sep 17 00:00:00 2001 From: Anand Date: Sun, 2 Jan 2022 13:18:20 +0530 Subject: [PATCH 20/27] fixed bugs issue #32 #33 #34 - added migrate option and tag as a regular field - issues #30 #31 --- crypto.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crypto.go b/crypto.go index 2c9ac69..c1744b7 100644 --- a/crypto.go +++ b/crypto.go @@ -278,7 +278,7 @@ func decryptFileAES(encDbPath string, password string) error { return err } - err, origFile = rewriteBaseFile(encDbPath, plainText, 0600) + err, origFile = rewriteFile(encDbPath, plainText, 0600) if err != nil { fmt.Printf("Error writing decrypted data to %s - \"%s\"\n", origFile, err.Error()) @@ -425,8 +425,8 @@ func decryptFileXChachaPoly(encDbPath string, password string) error { return err } - // err = os.WriteFile("test.sqlite3", plainText, 0600) - err, origFile = rewriteBaseFile(encDbPath, plainText, 0600) + // err = os.WriteFile("test.sqlite3", oplainText, 0600) + err, origFile = rewriteFile(encDbPath, plainText, 0600) if err != nil { fmt.Printf("Error writing decrypted data to %s - \"%s\"\n", origFile, err.Error()) From 8fdf80354f89a70ed85e368ea7abe7c40e227591 Mon Sep 17 00:00:00 2001 From: Anand Date: Sun, 2 Jan 2022 13:19:02 +0530 Subject: [PATCH 21/27] ref issue #30 #31 - added new options to migrate existing database and to add tag as regular field --- main.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/main.go b/main.go index 026de6f..4557b28 100644 --- a/main.go +++ b/main.go @@ -91,6 +91,7 @@ func performAction(optMap map[string]interface{}) { "clone": WrapperMaxKryptStringFunc(copyCurrentEntry), "use-db": setActiveDatabasePath, "export": exportToFile, + "migrate": migrateDatabase, } stringListActionsMap := map[string]actionFunc{ @@ -190,6 +191,7 @@ func initializeCmdLine(parser *argparse.Parser) map[string]interface{} { {"E", "edit", "Edit entry by ", "", ""}, {"l", "list-entry", "List entry by ", "", ""}, {"x", "export", "Export all entries to ", "", ""}, + {"m", "migrate", "Migrate a database to latest schema", "", ""}, } for _, opt := range stringOptions { From f5bc4a80d83832941916aabd949c9e6ef482f032 Mon Sep 17 00:00:00 2001 From: Anand Date: Sun, 2 Jan 2022 13:19:50 +0530 Subject: [PATCH 22/27] ref issue #30 #31 #32 #33 #34 - upped version to 0.4 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 4557b28..1cee5e4 100644 --- a/main.go +++ b/main.go @@ -9,7 +9,7 @@ import ( "strings" ) -const VERSION = 0.3 +const VERSION = 0.4 const APP = "varuh" const AUTHOR_INFO = ` From 78c17d5e73fe82c3210d5b17a798e51eb0e74df4 Mon Sep 17 00:00:00 2001 From: Anand Date: Sun, 2 Jan 2022 13:25:59 +0530 Subject: [PATCH 23/27] ref i- updated copyright --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 1cee5e4..c1ce900 100644 --- a/main.go +++ b/main.go @@ -14,7 +14,7 @@ const APP = "varuh" const AUTHOR_INFO = ` AUTHORS - Copyright (C) 2021 Anand B Pillai + Copyright (C) 2022 Anand B Pillai ` type actionFunc func(string) error From 791ba801356fdfdd4519b7a779b78c1e7e8fe104 Mon Sep 17 00:00:00 2001 From: Anand Date: Sun, 2 Jan 2022 13:40:04 +0530 Subject: [PATCH 24/27] updated default delim to '>' --- utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils.go b/utils.go index 57c3ac8..8c57d62 100644 --- a/utils.go +++ b/utils.go @@ -123,7 +123,7 @@ func getOrCreateLocalConfig(app string) (error, *Settings) { } else { // fmt.Printf("Creating default configuration ...") - settings = Settings{"", "aes", true, true, false, configFile, "id,asc", "+", "default", "bgblack"} + settings = Settings{"", "aes", true, true, false, configFile, "id,asc", ">", "default", "bgblack"} if err = writeSettings(&settings, configFile); err == nil { // fmt.Println(" ...done") From f04a6ea29957bac7aa467fe5ec363f5904609737 Mon Sep 17 00:00:00 2001 From: Anand Date: Sun, 2 Jan 2022 13:44:41 +0530 Subject: [PATCH 25/27] ref issue #30 - Updated README with tags and migration and a few other things --- README.md | 113 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 71 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 512d257..118e7f7 100644 --- a/README.md +++ b/README.md @@ -69,43 +69,44 @@ The binary will be installed in `/usr/local/bin` folder. Usage ===== - $ varuh -h - usage: varuh [-h|--help] [-I|--init ""] [-d|--decrypt ""] - [-C|--clone ""] [-R|--remove ""] [-U|--use-db - ""] [-E|--edit ""] [-l|--list-entry ""] - [-x|--export ""] [-f|--find "" [-f|--find "" - ...]] [-e|--encrypt] [-A|--add] [-p|--path] [-a|--list-all] - [-g|--genpass] [-s|--show] [-c|--copy] [-y|--assume-yes] - [-v|--version] - - Password manager for the command line for Unix like operating - systems - - Options: - - -h --help Print help information - -I --init Initialize a new database - -d --decrypt Decrypt password database - -C --clone Clone an entry with - -R --remove Remove an entry with or - -U --use-db Set as active database - -E --edit Edit entry by - -l --list-entry List entry by - -x --export Export all entries to - -f --find ... Search entries with terms - -e --encrypt Encrypt the current database - -A --add Add a new entry - -p --path Show current database path - -a --list-all List all entries in current database - -g --genpass Generate a strong password (length: 12 - 16) - -s --show Show passwords when listing entries - -c --copy Copy password to clipboard - -y --assume-yes Assume yes to actions requiring confirmation - -v --version Show version information and exit - - - AUTHORS - Copyright (C) 2021 Anand B Pillai + $ varuh -h + usage: varuh [-h|--help] [-I|--init ""] [-d|--decrypt ""] + [-C|--clone ""] [-R|--remove ""] [-U|--use-db + ""] [-E|--edit ""] [-l|--list-entry ""] + [-x|--export ""] [-m|--migrate ""] [-f|--find + "" [-f|--find "" ...]] [-e|--encrypt] [-A|--add] + [-p|--path] [-a|--list-all] [-g|--genpass] [-s|--show] [-c|--copy] + [-y|--assume-yes] [-v|--version] + + Password manager for the command line for Unix like operating + systems + + Options: + + -h --help Print help information + -I --init Initialize a new database + -d --decrypt Decrypt password database + -C --clone Clone an entry with + -R --remove Remove an entry with or + -U --use-db Set as active database + -E --edit Edit entry by + -l --list-entry List entry by + -x --export Export all entries to + -m --migrate Migrate a database to latest schema + -f --find ... Search entries with terms + -e --encrypt Encrypt the current database + -A --add Add a new entry + -p --path Show current database path + -a --list-all List all entries in current database + -g --genpass Generate a strong password (length: 12 - 16) + -s --show Show passwords when listing entries + -c --copy Copy password to clipboard + -y --assume-yes Assume yes to actions requiring confirmation + -v --version Show version information and exit + + + AUTHORS + Copyright (C) 2022 Anand B Pillai Encryption and Security @@ -149,6 +150,7 @@ The password database is created and is active now. You can start adding entries Username: mememe Password (enter to generate new): Generating password ...done + Tags (separated by space): testing test website Notes: Website uses Nginx auth Do you want to add custom fields [y/N]: Created new entry with id: 1 @@ -162,6 +164,7 @@ You can now list the entry with one of the list options. User: mememe URL: http://mywebsite.name Password: **************** + Tags: testing test website Notes: Website uses Nginx auth Modified: 2021-21-09 23:12:35 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -175,6 +178,7 @@ From version 0.3 onwards, custom fields are supported. URL: https://github.com/mydev/myproject Username: mydev Password (enter to generate new): ghp_ipQrStuVwxYz1a2b3cdEF10ghI689kLaMnOp + Tags (separated by space): token github Notes: Never Expires Do you want to add custom fields [y/N]: y Field Name: Domain @@ -190,12 +194,12 @@ From version 0.3 onwards, custom fields are supported. User: mydev URL: https://github.com/mydev/myproject Password: ghp_ipQrStuVwxYz1a2b3cdEF10ghI689kLaMnOp + Tags: token github Notes: Never Expires Domain: github.com Type: Auth Token Modified: 2021-21-13 00:07:18 - For more on listing see the [Listing and Searching](#listing-and-searching) section below. ## Edit an entry @@ -208,7 +212,9 @@ For more on listing see the [Listing and Searching](#listing-and-searching) sect Current Username: mememe New Username: meblog Current Password: lTzC2z9kRppnYsYl - New Password ([y/Y] to generate new, enter will keep old one): + New Password ([y/Y] to generate new, enter will keep old one): + Current Tags: testing test website + New Tags: Current Notes: Website uses Nginx auth New Notes: Website uses Apache Do you want to add custom fields [y/N]: @@ -221,6 +227,7 @@ For more on listing see the [Listing and Searching](#listing-and-searching) sect User: meblog URL: http://myblog.name Password: myblog123 + Tags: testing test website Notes: Website uses Apache Modified: 2021-21-09 23:15:29 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -242,7 +249,8 @@ When you edit an entry with custom fields, you get the option to change the name New Notes: Editing/deleting custom fields Field Name: Domain - New Field Name (Enter to keep, "x" to delete): x + New Field Name (Enter to keep, "x" to delete): x + Deleting field: Domain Field Name: Type New Field Name (Enter to keep, "x" to delete): Token Type Field Value: Auth Token @@ -257,6 +265,7 @@ When you edit an entry with custom fields, you get the option to change the name User: mydev URL: https://github.com/mydev/myproject Password: ghp_ipQrStuVwxYz1a2b3cdEF10ghI689kLaMnOp + Tags: token github Notes: Never Expires Token Type: Auth Token Modified: 2021-21-13 00:16:41 @@ -336,7 +345,25 @@ If you want to switch back to a previous database, you can use the `-U` option. Password: Decryption complete. Switched active database successfully. - + +## Database Migration + +(New in version 0.4) + +When new features are added - sometimes new fields would be required to be added in the database schema. To make sure your old databases work with the new features in such cases, the `--migrate` option can be used to migrate your existing databases. + + $ ./varuh -m /home/anand/mypasswds + Password: + Decryption complete. + Migrating tables ... + + Encryption complete. + Migration successful. + +For migration you need to provide the database path - even for the active database. Once migrated, you can continue to use your database as before. + +NOTE: It is suggested to make a backup copy of your current active database before migration. + ## Manual encryption and decryption You can manually encrypt the current database using the `-e` option. @@ -366,6 +393,7 @@ Now the database is active again and you can see the listings. User: myblog.name URL: http://meblog Password: ********* + Tags: test testing website Notes: Website uses Apache Modified: 2021-21-09 23:21:32 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -385,6 +413,7 @@ If the config param `encrypt_on` is set to `true` along with `auto_encrypt` (def User: banklogin URL: https://my.localbank.com Password: bankpass123 + Tags: bank banking finance Notes: Modified: 2021-21-18 12:44:10 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -588,7 +617,7 @@ The config file is named *config.json*. It looks as follows. "encrypt_on": true, "path": "/home/anand/.config/varuh/config.json", "list_order": "id,asc", - "delimiter": "+", + "delimiter": ">", "color": "default", "bgcolor": "bgblack" } From f81a7ee0ac44f2b43c2a8d93480e48eb0697eeca Mon Sep 17 00:00:00 2001 From: Anand Date: Sun, 2 Jan 2022 13:45:49 +0530 Subject: [PATCH 26/27] ref issue #30 - Updated separator string --- README.md | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 118e7f7..49d22e9 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ The password database is created and is active now. You can start adding entries You can now list the entry with one of the list options. $ varuh -l 1 - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ID: 1 Title: My Website Login User: mememe @@ -167,7 +167,7 @@ You can now list the entry with one of the list options. Tags: testing test website Notes: Website uses Nginx auth Modified: 2021-21-09 23:12:35 - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ## Add an entry with custom fields @@ -221,7 +221,7 @@ For more on listing see the [Listing and Searching](#listing-and-searching) sect Updated entry. $ varuh -l 1 -s - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ID: 1 Title: My Blog Login User: meblog @@ -230,7 +230,7 @@ For more on listing see the [Listing and Searching](#listing-and-searching) sect Tags: testing test website Notes: Website uses Apache Modified: 2021-21-09 23:15:29 - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ## Edit an entry with custom fields @@ -282,12 +282,12 @@ To clone (copy) an entry, ## Remove an entry $ varuh -R 1 - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Title: My Website Login User: mememe URL: https://mywebsite.name Modified: 2021-21-09 23:12:35 - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Please confirm removal [Y/n]: Entry with id 1 was removed from the database @@ -387,7 +387,7 @@ Manually decrypt the database using `-d` option. Now the database is active again and you can see the listings. $ varuh -l 3 - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ID: 2 Title: My Blog Login User: myblog.name @@ -396,7 +396,7 @@ Now the database is active again and you can see the listings. Tags: test testing website Notes: Website uses Apache Modified: 2021-21-09 23:21:32 - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ## Always on encryption @@ -407,7 +407,7 @@ If the config param `encrypt_on` is set to `true` along with `auto_encrypt` (def $ varuh -f my -s Password: Decryption complete. - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ID: 2 Title: MY LOCAL BANK User: banklogin @@ -416,7 +416,7 @@ If the config param `encrypt_on` is set to `true` along with `auto_encrypt` (def Tags: bank banking finance Notes: Modified: 2021-21-18 12:44:10 - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Encryption complete. @@ -430,7 +430,7 @@ Listing and Searching To list an entry using its id, $ varuh -l 8 - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ID: 8 Title: Google account User: anandpillai@alumni.iitm.ac.in @@ -438,14 +438,14 @@ To list an entry using its id, Password: *********** Notes: Modified: 2021-21-25 15:02:50 - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ## To search an entry An entry can be searched on its title, username, URL or notes. Search is case-insensitive. $ varuh -f google - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ID: 8 Title: Google account User: anandpillai@alumni.iitm.ac.in @@ -453,7 +453,7 @@ An entry can be searched on its title, username, URL or notes. Search is case-in Password: ********** Notes: Modified: 2021-21-25 15:02:50 - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ID: 9 Title: Google account User: xyz@gmail.com @@ -461,7 +461,7 @@ An entry can be searched on its title, username, URL or notes. Search is case-in Password: ******** Notes: Modified: 2021-21-25 15:05:36 - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ID: 10 Title: Google account User: somethingaboutme@gmail.com @@ -469,7 +469,7 @@ An entry can be searched on its title, username, URL or notes. Search is case-in Password: *********** Notes: Modified: 2021-21-25 15:09:51 - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ## To search using multiple terms @@ -477,7 +477,7 @@ An entry can be searched on its title, username, URL or notes. Search is case-in The `-f` option supports multiple terms, so you can specify this more than one time to narrow a search down to a specific entry. $ varuh -f google -f anand - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ID: 8 Title: Google account User: anandpillai@alumni.iitm.ac.in @@ -485,7 +485,7 @@ The `-f` option supports multiple terms, so you can specify this more than one t Password: ********** Notes: Modified: 2021-21-25 15:02:50 - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> $ varuh -f google -f priya Entry for "google priya" not found @@ -495,7 +495,7 @@ The `-f` option supports multiple terms, so you can specify this more than one t To list all entries, use the option `-a`. $ varuh -a - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ID: 1 Title: My Bank #1 User: myusername1 @@ -503,7 +503,7 @@ To list all entries, use the option `-a`. Password: *********** Notes: Modified: 2021-21-15 15:40:29 - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ID: 2 Title: My Digital Locker #1 User: mylockerusername @@ -511,7 +511,7 @@ To list all entries, use the option `-a`. Password: ********** Notes: Modified: 2021-21-18 12:44:10 - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ID: 3 Title: My Bank Login #2 User: mybankname2 @@ -519,7 +519,7 @@ To list all entries, use the option `-a`. Password: ********** Notes: Modified: 2021-21-19 14:16:33 - +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ... By default the listing is in ascending ID order. This can be changed in the configuration (see below). From 397eea14328af055248149871e97718c2693802e Mon Sep 17 00:00:00 2001 From: Anand Date: Sun, 2 Jan 2022 13:49:02 +0530 Subject: [PATCH 27/27] doc fixes --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 49d22e9..01631aa 100644 --- a/README.md +++ b/README.md @@ -244,7 +244,9 @@ When you edit an entry with custom fields, you get the option to change the name Current Username: mydev New Username: Current Password: ghp_ipQrStuVwxYz1a2b3cdEF10ghI689kLaMnOp - New Password ([y/Y] to generate new, enter will keep old one): + New Password ([y/Y] to generate new, enter will keep old one): + Current Tags: token github + New Tags: Current Notes: Never Expires New Notes: Editing/deleting custom fields @@ -350,7 +352,7 @@ If you want to switch back to a previous database, you can use the `-U` option. (New in version 0.4) -When new features are added - sometimes new fields would be required to be added in the database schema. To make sure your old databases work with the new features in such cases, the `--migrate` option can be used to migrate your existing databases. +When new features are added - sometimes new fields would be required to be added in the database schema. To make sure your old databases work with the new features in such cases, the `-m/--migrate` option can be used to migrate your existing databases. $ ./varuh -m /home/anand/mypasswds Password: