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 { 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()) 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) { diff --git a/main.go b/main.go index 026de6f..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 = ` @@ -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 { 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)