From 668258f076a76e7c797f5d9f9e07364590640f92 Mon Sep 17 00:00:00 2001 From: Anand Date: Mon, 3 Jan 2022 18:41:58 +0530 Subject: [PATCH 01/11] ref issue #28 - Enhanced schema for entries table plus new address table for new types --- db.go | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 5 deletions(-) diff --git a/db.go b/db.go index fa2f75a..3bb5b23 100644 --- a/db.go +++ b/db.go @@ -17,7 +17,23 @@ import ( // Structure representing an entry in the db type Entry struct { ID int `gorm:"column:id;autoIncrement;primaryKey"` + Type string `gorm:"column:type"` // Type of entry - password (default), card, identity etc Title string `gorm:"column:title"` + Name string `gorm:"column:name"` // Card holder name/ID card name - for types cards/identity + Company string `gorm:"column:company"` // Company name of person - for type identity and + // Credit card company for type CC + Number string `gorm:"column:number"` // Number type - CC number for credit cards + // ID card number for identity types + SecurityCode string `gorm:"security_code"` // CVV number/security code for CC type + ExpiryMonth string `gorm:"expiry_month"` // CC or Identity document expiry month + ExpiryDay string `gorm:"expiry_day"` // Identity document expiry day + ExpiryYear string `gorm:"expiry_year"` // CC or Identity document expiry year + FirstName string `gorm:"column:first_name"` // first name - for ID card types + MiddleName string `gorm:"column:middle_name"` // middle name - for ID card types + LastName string `gorm:"column:last_name"` // last name - for ID card types + Email string `gorm:"email"` // Email - for ID card types + PhoneNumber string `gorm:"phone_number"` // Phone number - for ID card types + User string `gorm:"column:user"` Url string `gorm:"column:url"` Password string `gorm:"column:password"` @@ -45,15 +61,68 @@ func (ex *ExtendedEntry) TableName() string { return "exentries" } +type Address struct { + ID int `gorm:"column:id;autoIncrement;primaryKey"` + Number string `gorm:"column:number"` // Flat or building number + Building string `gorm:"column:building"` // Apartment or building or society name + Street string `gorm:"column:street"` // Street address + Locality string `gorm:"column:locality"` // Name of the locality e.g: Whitefield + Area string `gorm:"column:area"` // Name of the larger area e.g: East Bangalore + City string `gorm:"column:city"` // Name of the city e.g: Bangalore + State string `gorm:"column:state"` // Name of the state e.g: Karnataka + Country string `gorm:"column:country"` // Name of the country e.g: India + + Landmark string `gorm:"column:landmark"` // Name of landmark if any + ZipCode string `gorm:"column:zipcode"` // PIN/ZIP code + Type string `gorm:"column:type"` // Type of address: Home/Work/Business + + Entry Entry `gorm:"foreignKey:EntryID"` + EntryID int +} + +func (ad *Address) TableName() string { + return "address" +} + // Clone an entry func (e1 *Entry) Copy(e2 *Entry) { if e2 != nil { - e1.Title = e2.Title - e1.User = e2.User - e1.Url = e2.Url - e1.Password = e2.Password - e1.Notes = e2.Notes + switch (e2.Type) { + case "password": + e1.Title = e2.Title + e1.User = e2.User + e1.Url = e2.Url + e1.Password = e2.Password + e1.Notes = e2.Notes + e1.Tags = e2.Tags + e1.Type = e2.Type + case "card": + e1.Title = e2.Title + e1.Name = e2.Name // card holder name + e1.Company = e2.Company + e1.Number = e2.Number + e1.SecurityCode = e2.SecurityCode + e1.ExpiryMonth = e2.ExpiryMonth + e1.ExpiryYear = e2.ExpiryYear + e1.Tags = e2.Tags + e1.Notes = e2.Notes + e1.Type = e2.Type + case "identity": + e1.Title = e2.Title + e1.Name = e2.Name + e1.Company = e2.Company + e1.FirstName = e2.FirstName + e1.LastName = e2.LastName + e1.MiddleName = e2.MiddleName + e1.User = e2.User + e1.Email = e2.Email + e1.PhoneNumber = e2.PhoneNumber + e1.Number = e2.Number + e1.Notes = e2.Notes + e1.Tags = e2.Tags + e1.Type = e2.Type + } } } From f391ae917013b826b9f48031a40d9c0d8e68f82f Mon Sep 17 00:00:00 2001 From: Anand Date: Mon, 3 Jan 2022 18:43:52 +0530 Subject: [PATCH 02/11] ref issue #28 - added active field --- db.go | 1 + 1 file changed, 1 insertion(+) diff --git a/db.go b/db.go index 3bb5b23..62359e6 100644 --- a/db.go +++ b/db.go @@ -34,6 +34,7 @@ type Entry struct { Email string `gorm:"email"` // Email - for ID card types PhoneNumber string `gorm:"phone_number"` // Phone number - for ID card types + Active bool `gorm:"active;default:true"` // Is the id card/CC active ? User string `gorm:"column:user"` Url string `gorm:"column:url"` Password string `gorm:"column:password"` From 7fd1032bc6f93b863a8c64facc12cc50f37cee2b Mon Sep 17 00:00:00 2001 From: Anand Balachandran Pillai Date: Sun, 14 Aug 2022 11:02:24 +0530 Subject: [PATCH 03/11] ref issue #42 - search on tags as well --- db.go | 805 +++++++++++++++++++++++++++++----------------------------- 1 file changed, 401 insertions(+), 404 deletions(-) diff --git a/db.go b/db.go index fa2f75a..2d73af3 100644 --- a/db.go +++ b/db.go @@ -2,586 +2,583 @@ package main import ( - "database/sql" - "fmt" - "gorm.io/driver/sqlite" - "gorm.io/gorm" - "gorm.io/gorm/logger" - "os" - "path/filepath" - "strconv" - "strings" - "time" + "database/sql" + "fmt" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" + "os" + "path/filepath" + "strconv" + "strings" + "time" ) // Structure representing an entry in the db type Entry struct { - ID int `gorm:"column:id;autoIncrement;primaryKey"` - Title string `gorm:"column:title"` - User string `gorm:"column:user"` - 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 + ID int `gorm:"column:id;autoIncrement;primaryKey"` + Title string `gorm:"column:title"` + User string `gorm:"column:user"` + 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 } func (e *Entry) TableName() string { - return "entries" + 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 + 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 + Entry Entry `gorm:"foreignKey:EntryID"` + EntryID int } func (ex *ExtendedEntry) TableName() string { - return "exentries" + return "exentries" } // Clone an entry func (e1 *Entry) Copy(e2 *Entry) { - if e2 != nil { - e1.Title = e2.Title - e1.User = e2.User - e1.Url = e2.Url - e1.Password = e2.Password - e1.Notes = e2.Notes - } + if e2 != nil { + e1.Title = e2.Title + e1.User = e2.User + e1.Url = e2.Url + e1.Password = e2.Password + e1.Notes = e2.Notes + } } // Clone an entry func (e1 *ExtendedEntry) Copy(e2 *ExtendedEntry) { - if e2 != nil { - e1.FieldName = e2.FieldName - e1.FieldValue = e2.FieldValue - e1.EntryID = e2.EntryID - } + 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) { - db, err := gorm.Open(sqlite.Open(filePath), &gorm.Config{ - Logger: logger.Default.LogMode(logger.Silent), - }) - return err, db + db, err := gorm.Open(sqlite.Open(filePath), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Silent), + }) + return err, db } // Create a new table for Entries in the database func createNewEntry(db *gorm.DB) error { - return db.AutoMigrate(&Entry{}) + return db.AutoMigrate(&Entry{}) } // Create a new table for Extended Entries in the database func createNewExEntry(db *gorm.DB) error { - return db.AutoMigrate(&ExtendedEntry{}) + return db.AutoMigrate(&ExtendedEntry{}) } // Init new database including tables func initNewDatabase(dbPath string) error { - var err error - var db *gorm.DB - var absPath string - - if hasActiveDatabase() { - // Has an active database - encrypt it before creating new one - _, activeDbPath := getActiveDatabase() - absPath, _ = filepath.Abs(dbPath) - - if absPath == activeDbPath { - fmt.Printf("Database already exists and is active - %s\n", dbPath) - return nil - } else { - // TBD - fmt.Printf("Encrytping current database - %s\n", activeDbPath) - encryptDatabase(activeDbPath, nil) - } - } - - if _, err = os.Stat(dbPath); err == nil { - // filePath exists, remove it - os.Remove(dbPath) - } - - err, db = openDatabase(dbPath) - if err != nil { - fmt.Printf("Error creating new database - \"%s\"\n", err.Error()) - return err - } - - err = createNewEntry(db) - if err != nil { - fmt.Printf("Error creating schema - \"%s\"\n", err.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 - absPath, err = filepath.Abs(dbPath) - // Chmod it - os.Chmod(absPath, 0600) - - if err == nil { - fmt.Printf("Updating active db path - %s\n", absPath) - updateActiveDbPath(absPath) - } else { - fmt.Printf("Error - %s\n", err.Error()) - return err - } - - return nil + var err error + var db *gorm.DB + var absPath string + + if hasActiveDatabase() { + // Has an active database - encrypt it before creating new one + _, activeDbPath := getActiveDatabase() + absPath, _ = filepath.Abs(dbPath) + + if absPath == activeDbPath { + fmt.Printf("Database already exists and is active - %s\n", dbPath) + return nil + } else { + // TBD + fmt.Printf("Encrytping current database - %s\n", activeDbPath) + encryptDatabase(activeDbPath, nil) + } + } + + if _, err = os.Stat(dbPath); err == nil { + // filePath exists, remove it + os.Remove(dbPath) + } + + err, db = openDatabase(dbPath) + if err != nil { + fmt.Printf("Error creating new database - \"%s\"\n", err.Error()) + return err + } + + err = createNewEntry(db) + if err != nil { + fmt.Printf("Error creating schema - \"%s\"\n", err.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 + absPath, err = filepath.Abs(dbPath) + // Chmod it + os.Chmod(absPath, 0600) + + if err == nil { + fmt.Printf("Updating active db path - %s\n", absPath) + updateActiveDbPath(absPath) + } else { + fmt.Printf("Error - %s\n", err.Error()) + return err + } + + return nil } // Open currently active database func openActiveDatabase() (error, *gorm.DB) { - var dbPath string - var err error + var dbPath string + var err error - err, dbPath = getActiveDatabase() - if err != nil { - fmt.Printf("Error getting active database path - %s\n", err.Error()) - return err, nil - } + err, dbPath = getActiveDatabase() + if err != nil { + fmt.Printf("Error getting active database path - %s\n", err.Error()) + return err, nil + } - err, db := openDatabase(dbPath) - if err != nil { - fmt.Printf("Error opening active database path - %s: %s\n", dbPath, err.Error()) - return err, nil - } + err, db := openDatabase(dbPath) + if err != nil { + fmt.Printf("Error opening active database path - %s: %s\n", dbPath, err.Error()) + return err, nil + } - return nil, 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 + var count int + var err error - err = createNewExEntry(db) - if err != nil { - fmt.Printf("Error creating schema - \"%s\"\n", err.Error()) - return err - } + err = createNewExEntry(db) + if err != nil { + fmt.Printf("Error creating schema - \"%s\"\n", err.Error()) + return err + } - for _, customEntry := range customEntries { - var exEntry ExtendedEntry + for _, customEntry := range customEntries { + var exEntry ExtendedEntry - exEntry = ExtendedEntry{FieldName: customEntry.fieldName, FieldValue: customEntry.fieldValue, - EntryID: entry.ID} + exEntry = ExtendedEntry{FieldName: customEntry.fieldName, FieldValue: customEntry.fieldValue, + EntryID: entry.ID} - resultEx := db.Create(&exEntry) - if resultEx.Error == nil && resultEx.RowsAffected == 1 { - count += 1 - } - } + 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 + 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 + 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 - } + 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) + db.Where("entry_id = ?", entry.ID).Delete(&customEntries) - for _, customEntry := range updatedEntries { - var exEntry ExtendedEntry + for _, customEntry := range updatedEntries { + var exEntry ExtendedEntry - exEntry = ExtendedEntry{FieldName: customEntry.fieldName, FieldValue: customEntry.fieldValue, - EntryID: entry.ID} + exEntry = ExtendedEntry{FieldName: customEntry.fieldName, FieldValue: customEntry.fieldValue, + EntryID: entry.ID} - resultEx := db.Create(&exEntry) - if resultEx.Error == nil && resultEx.RowsAffected == 1 { - count += 1 - } - } + 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 + 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, 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, Tags: strings.TrimSpace(tags), - Notes: notes} - - err, db = openActiveDatabase() - if err == nil && db != nil { - // 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 - } - } - - return err + 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, Tags: strings.TrimSpace(tags), + Notes: notes} + + err, db = openActiveDatabase() + if err == nil && db != nil { + // 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 + } + } + + return err } // Update current database entry with new values 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, - "tags": tags} - - for key, val := range keyValMap { - if len(val) > 0 { - updateMap[key] = val - } - } - - if len(updateMap) == 0 && !flag { - fmt.Printf("Nothing to update\n") - return nil - } - - // Update timestamp also - updateMap["timestamp"] = time.Now() - - err, db := openActiveDatabase() - - if err == nil && db != nil { - result := db.Model(entry).Updates(updateMap) - if result.Error != nil { - return result.Error - } - - if flag { - replaceCustomEntries(db, entry, customEntries) - } - fmt.Println("Updated entry.") - return nil - } - - return err + 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, + "tags": tags} + + for key, val := range keyValMap { + if len(val) > 0 { + updateMap[key] = val + } + } + + if len(updateMap) == 0 && !flag { + fmt.Printf("Nothing to update\n") + return nil + } + + // Update timestamp also + updateMap["timestamp"] = time.Now() + + err, db := openActiveDatabase() + + if err == nil && db != nil { + result := db.Model(entry).Updates(updateMap) + if result.Error != nil { + return result.Error + } + + if flag { + replaceCustomEntries(db, entry, customEntries) + } + fmt.Println("Updated entry.") + return nil + } + + return err } // Find entry given the id func getEntryById(id int) (error, *Entry) { - var entry Entry - var err error - var db *gorm.DB - - err, db = openActiveDatabase() - if err == nil && db != nil { - result := db.First(&entry, id) - if result.Error == nil { - return nil, &entry - } else { - return result.Error, nil - } - } - - return err, nil + var entry Entry + var err error + var db *gorm.DB + + err, db = openActiveDatabase() + if err == nil && db != nil { + result := db.First(&entry, id) + if result.Error == nil { + return nil, &entry + } else { + return result.Error, nil + } + } + + return err, nil } // Search database for the given string and return all matches func searchDatabaseEntry(term string) (error, []Entry) { - var entries []Entry - var err error - var db *gorm.DB - var searchTerm string + var entries []Entry + var err error + var db *gorm.DB + var searchTerm string - err, db = openActiveDatabase() - if err == nil && db != nil { - var conditions []string - var condition string + err, db = openActiveDatabase() + if err == nil && db != nil { + searchTerm = fmt.Sprintf("%%%s%%", term) + // Search on fields title, user, url and notes and tags. + query := db.Debug().Where(fmt.Sprintf("title like \"%s\"", searchTerm)) - searchTerm = fmt.Sprintf("%%%s%%", term) - // Search on fields title, user, url and notes - for _, field := range []string{"title", "user", "url", "notes"} { - conditions = append(conditions, field+" like ?") - } + for _, field := range[]string{"user", "url", "notes", "tags"} { + query = query.Or(fmt.Sprintf("%s like \"%s\"", field, searchTerm)) + } + + res := query.Find(&entries) - condition = strings.Join(conditions, " OR ") - query := db.Where(condition, searchTerm, searchTerm, searchTerm, searchTerm) - res := query.Find(&entries) + if res.Error != nil { + return res.Error, nil + } - if res.Error != nil { - return res.Error, nil - } + return nil, entries + } - return nil, entries - } - - return err, entries + return err, entries } // Union of two entry arrays func union(entry1 []Entry, entry2 []Entry) []Entry { - m := make(map[int]bool) + m := make(map[int]bool) - for _, item := range entry1 { - m[item.ID] = true - } + for _, item := range entry1 { + m[item.ID] = true + } - for _, item := range entry2 { - if _, ok := m[item.ID]; !ok { - entry1 = append(entry1, item) - } - } + for _, item := range entry2 { + if _, ok := m[item.ID]; !ok { + entry1 = append(entry1, item) + } + } - return entry1 + return entry1 } // Intersection of two entry arrays func intersection(entry1 []Entry, entry2 []Entry) []Entry { - var common []Entry + var common []Entry - m := make(map[int]bool) + m := make(map[int]bool) - for _, item := range entry1 { - m[item.ID] = true - } + for _, item := range entry1 { + m[item.ID] = true + } - for _, item := range entry2 { - if _, ok := m[item.ID]; ok { - common = append(common, item) - } - } + for _, item := range entry2 { + if _, ok := m[item.ID]; ok { + common = append(common, item) + } + } - return common + 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 + 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 { - var err error - var db *gorm.DB + var err error + var db *gorm.DB - err, db = openActiveDatabase() - if err == nil && db != nil { - var exEntries []ExtendedEntry + err, db = openActiveDatabase() + if err == nil && db != nil { + var exEntries []ExtendedEntry - res := db.Delete(entry) - if res.Error != nil { - return res.Error - } + 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 - } - } + // 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 - } + return nil + } - return err + return err } // Clone an entry and return cloned entry func cloneEntry(entry *Entry) (error, *Entry) { - var entryNew Entry - var err error - var db *gorm.DB + var entryNew Entry + var err error + var db *gorm.DB - err, db = openActiveDatabase() - if err == nil && db != nil { - entryNew.Copy(entry) + err, db = openActiveDatabase() + if err == nil && db != nil { + entryNew.Copy(entry) - result := db.Create(&entryNew) - if result.Error == nil && result.RowsAffected == 1 { - fmt.Printf("Cloned to new entry, id: %d.\n", entryNew.ID) - return nil, &entryNew - } else if result.Error != nil { - return result.Error, nil - } - } + result := db.Create(&entryNew) + if result.Error == nil && result.RowsAffected == 1 { + fmt.Printf("Cloned to new entry, id: %d.\n", entryNew.ID) + return nil, &entryNew + } else if result.Error != nil { + return result.Error, nil + } + } - return err, nil + 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 + var err error + var db *gorm.DB - err, db = openActiveDatabase() - if err == nil && db != nil { - for _, exEntry := range exEntries { - var exEntryNew ExtendedEntry + 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 + exEntryNew.Copy(&exEntry) + // Update the ID! + exEntryNew.EntryID = entry.ID - result := db.Create(&exEntryNew) - if result.Error != nil { - return result.Error - } - } - } + result := db.Create(&exEntryNew) + if result.Error != nil { + return result.Error + } + } + } - return err + return err } // Return an iterator over all entries using the given order query keys func iterateEntries(orderKey string, order string) (error, []Entry) { - var err error - var db *gorm.DB - var entries []Entry + var err error + var db *gorm.DB + var entries []Entry - err, db = openActiveDatabase() + err, db = openActiveDatabase() - if err == nil && db != nil { - var rows *sql.Rows + if err == nil && db != nil { + var rows *sql.Rows - rows, err = db.Model(&Entry{}).Order(fmt.Sprintf("%s %s", orderKey, order)).Rows() - for rows.Next() { - var entry Entry + rows, err = db.Model(&Entry{}).Order(fmt.Sprintf("%s %s", orderKey, order)).Rows() + for rows.Next() { + var entry Entry - db.ScanRows(rows, &entry) - entries = append(entries, entry) - } + db.ScanRows(rows, &entry) + entries = append(entries, entry) + } - return nil, entries - } + return nil, entries + } - return err, nil + return err, nil } // Export all entries to string array func entriesToStringArray(skipLongFields bool) (error, [][]string) { - var err error - var db *gorm.DB - var dataArray [][]string + var err error + var db *gorm.DB + var dataArray [][]string - err, db = openActiveDatabase() + err, db = openActiveDatabase() - if err == nil && db != nil { - var rows *sql.Rows - var count int64 + if err == nil && db != nil { + var rows *sql.Rows + var count int64 - db.Model(&Entry{}).Count(&count) + db.Model(&Entry{}).Count(&count) - dataArray = make([][]string, 0, count) + dataArray = make([][]string, 0, count) - rows, err = db.Model(&Entry{}).Order("id asc").Rows() - for rows.Next() { - var entry Entry - var entryData []string + rows, err = db.Model(&Entry{}).Order("id asc").Rows() + for rows.Next() { + var entry Entry + var entryData []string - db.ScanRows(rows, &entry) + db.ScanRows(rows, &entry) - if skipLongFields { - // Skip Notes - entryData = []string{strconv.Itoa(entry.ID), entry.Title, entry.User, entry.Password, entry.Timestamp.Format("2006-06-02 15:04:05")} - } else { - entryData = []string{strconv.Itoa(entry.ID), entry.Title, entry.User, entry.Url, entry.Password, entry.Notes, entry.Timestamp.Format("2006-06-02 15:04:05")} - } + if skipLongFields { + // Skip Notes + entryData = []string{strconv.Itoa(entry.ID), entry.Title, entry.User, entry.Password, entry.Timestamp.Format("2006-06-02 15:04:05")} + } else { + entryData = []string{strconv.Itoa(entry.ID), entry.Title, entry.User, entry.Url, entry.Password, entry.Notes, entry.Timestamp.Format("2006-06-02 15:04:05")} + } - dataArray = append(dataArray, entryData) - } - } + dataArray = append(dataArray, entryData) + } + } - return err, dataArray + 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 + var err error + var db *gorm.DB + var customEntries []ExtendedEntry - err, db = openActiveDatabase() + err, db = openActiveDatabase() - if err == nil && db != nil { - db.Where("entry_id = ?", entry.ID).Find(&customEntries) - } + if err == nil && db != nil { + db.Where("entry_id = ?", entry.ID).Find(&customEntries) + } - return customEntries + return customEntries } From a3c1420942e888b2b4701cef5a96b4e4b58ddb04 Mon Sep 17 00:00:00 2001 From: Anand Balachandran Pillai Date: Sun, 14 Aug 2022 11:02:47 +0530 Subject: [PATCH 04/11] ref issue #42 - turn off debug --- db.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db.go b/db.go index 2d73af3..a444b71 100644 --- a/db.go +++ b/db.go @@ -339,7 +339,7 @@ func searchDatabaseEntry(term string) (error, []Entry) { if err == nil && db != nil { searchTerm = fmt.Sprintf("%%%s%%", term) // Search on fields title, user, url and notes and tags. - query := db.Debug().Where(fmt.Sprintf("title like \"%s\"", searchTerm)) + query := db.Where(fmt.Sprintf("title like \"%s\"", searchTerm)) for _, field := range[]string{"user", "url", "notes", "tags"} { query = query.Or(fmt.Sprintf("%s like \"%s\"", field, searchTerm)) From 067c24de309a4db8ca6fef10b83d7e1bdd8e4310 Mon Sep 17 00:00:00 2001 From: Anand Balachandran Pillai Date: Sun, 14 Aug 2022 11:11:09 +0530 Subject: [PATCH 05/11] Adding github actions --- .github/workflows/build.yaml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/build.yaml diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..20e8343 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,21 @@ +name: build +on: [push] +jobs: + varuh build + runs-on: ubuntu-latest + steps: + - run: echo "\U0001f389 The job was automatically triggered by a ${{ github.event_name }} event." + - run: echo "\U0001f427 This job is now running on a ${{ runner.os }} server hosted by GitHub!" + - run: echo "\U0001f50e The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}." + - name: Check out repository code + uses: actions/checkout@v3 + - run: echo "\U0001f4a1 The ${{ github.repository }} repository has been cloned to the runner." + - run: echo "\U0001f5a5\ufe0f The workflow is now ready to test your code on the runner." + - name: List files in the repository + run: | + ls ${{ github.workspace }} + - run: echo "\U0001f34f This job's status is ${{ job.status }}." + - name: Build code + run: | + make + From 0383a2dfd64d232e1bf0372881d9b6fbfb35a8e3 Mon Sep 17 00:00:00 2001 From: Anand Balachandran Pillai Date: Sun, 14 Aug 2022 11:14:07 +0530 Subject: [PATCH 06/11] Fixing actions --- .github/workflows/build.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 20e8343..53bb578 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,7 +1,7 @@ name: build on: [push] jobs: - varuh build + varuh-build: runs-on: ubuntu-latest steps: - run: echo "\U0001f389 The job was automatically triggered by a ${{ github.event_name }} event." @@ -17,5 +17,4 @@ jobs: - run: echo "\U0001f34f This job's status is ${{ job.status }}." - name: Build code run: | - make - + make From 936c8a070682c34de389166bf810c948ba706c51 Mon Sep 17 00:00:00 2001 From: Anand Balachandran Pillai Date: Sun, 14 Aug 2022 11:17:46 +0530 Subject: [PATCH 07/11] Fixing build --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 53bb578..d25d263 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -17,4 +17,4 @@ jobs: - run: echo "\U0001f34f This job's status is ${{ job.status }}." - name: Build code run: | - make + cd ${{ github.workspace }} && make From c3da6109fb135887b3356aa3792c4b2061be1a57 Mon Sep 17 00:00:00 2001 From: Anand Balachandran Pillai Date: Mon, 15 Aug 2022 00:35:38 +0530 Subject: [PATCH 08/11] ref issue 28 - added support for card type data --- actions.go | 2027 +++++++++++++++++++++++++++------------------------- db.go | 56 +- go.mod | 2 +- go.sum | 6 +- main.go | 394 +++++----- utils.go | 819 ++++++++++++--------- 6 files changed, 1816 insertions(+), 1488 deletions(-) diff --git a/actions.go b/actions.go index 09c94ca..f149966 100644 --- a/actions.go +++ b/actions.go @@ -2,1214 +2,1299 @@ package main import ( - "bufio" - "encoding/csv" - "errors" - "fmt" - "gorm.io/gorm" - "os" - "os/exec" - "os/signal" - "path/filepath" - "strconv" - "strings" - "syscall" + "bufio" + "encoding/csv" + "errors" + "fmt" + "gorm.io/gorm" + "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 { - 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 { - // 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) - } - } - } - - if !activeEncrypted { - // Use should manually encrypt before switching - fmt.Println("Auto-encrypt disabled, encrypt existing database before switching to new.") - return nil - } - - if newEncrypted && !settings.AutoEncrypt { - // 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 { + 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 { + // 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) + } + } + } + + if !activeEncrypted { + // Use should manually encrypt before switching + fmt.Println("Auto-encrypt disabled, encrypt existing database before switching to new.") + return nil + } + + if newEncrypted && !settings.AutoEncrypt { + // 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 for a card type +func addNewCardEntry() error { + + var cardHolder string + var cardName string + var cardNumber string + var cardCvv string + var cardPin string + var cardIssuer string + var cardClass string + var cardExpiry string + + var notes string + var tags string + var err error + var customEntries []CustomEntry + + if err = checkActiveDatabase(); err != nil { + return err + } + + reader := bufio.NewReader(os.Stdin) + cardNumber = readInput(reader, "Card Number") + cardClass, err = detectCardType(cardNumber) + + if err != nil { + fmt.Printf("Error - %s\n", err.Error()) + return err + } else { + fmt.Printf("\n", cardClass) + } + + cardHolder = readInput(reader, "Name on the Card") + cardExpiry = readInput(reader, "Expiry Date as mm/dd") + + // expiry has to be in the form of / + if !checkValidExpiry(cardExpiry) { + return errors.New("Invalid Expiry Date") + } + + fmt.Printf("CVV: ") + err, cardCvv = readPassword() + + if !validateCvv(cardCvv, cardClass) { + fmt.Printf("\nError - Invalid CVV for %s\n", cardClass) + return errors.New(fmt.Sprintf("Error - Invalid CVV for %s\n", cardClass)) + } + + fmt.Printf("\nCard PIN: ") + err, cardPin = readPassword() + + if !validateCardPin(cardPin) { + fmt.Printf("\nError - Invalid PIN") + return errors.New("Error - Invalid PIN") + } + + cardIssuer = readInput(reader, "\nIssuing Bank") + cardName = readInput(reader, "A name for this Card") + // Name cant be blank + if len(cardName) == 0 { + fmt.Printf("Error - name cant be blank") + return errors.New("Empty card name") + } + + tags = readInput(reader, "\nTags (separated by space): ") + notes = readInput(reader, "Notes") + + customEntries = addCustomFields(reader) + + err = addNewDatabaseCardEntry(cardName, cardNumber, cardHolder, cardIssuer, + cardClass, cardCvv, cardPin, cardExpiry, notes, tags, customEntries) + + if err != nil { + fmt.Printf("Error adding entry - \"%s\"\n", err.Error()) + } + + return err +} + + // 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 tags 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) - - tags = readInput(reader, "\nTags (separated by space): ") - notes = readInput(reader, "Notes") - - // 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, tags, 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 tags string + var err error + var customEntries []CustomEntry + + if err = checkActiveDatabase(); err != nil { + return err + } + + if settingsRider.Type == "card" { + return addNewCardEntry() + } + + 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) + + tags = readInput(reader, "\nTags (separated by space): ") + notes = readInput(reader, "Notes") + + // 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, tags, 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: " + customEntry.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: " + customEntry.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 tags 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 tags 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 Tags: %s\n", entry.Tags) - tags = readInput(reader, "New Tags") + 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") + 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, tags, notes, customEntries, flag) - if err != nil { - fmt.Printf("Error updating entry - \"%s\"\n", err.Error()) - } + // Update + err = updateDatabaseEntry(entry, title, userName, url, passwd, tags, 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 - var terms []string - - if err = checkActiveDatabase(); err != nil { - return err - } - - 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 - } 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 + var terms []string + + if err = checkActiveDatabase(); err != nil { + return err + } + + 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 + } 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) - - 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 + 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 entryNew *Entry - var exEntries []ExtendedEntry + var err error + var entry *Entry + var entryNew *Entry + var exEntries []ExtendedEntry - var id int + 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, entryNew = cloneEntry(entry) - if err != nil { - fmt.Printf("Error cloning entry: \"%s\"\n", err.Error()) - return err - } + err, entryNew = cloneEntry(entry) + if err != nil { + fmt.Printf("Error cloning entry: \"%s\"\n", err.Error()) + return err + } - exEntries = getExtendedEntries(entry) + exEntries = getExtendedEntries(entry) - if len(exEntries) > 0 { - fmt.Printf("%d extended entries found\n", len(exEntries)) + 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 - } - } + err = cloneExtendedEntries(entryNew, exEntries) + if err != nil { + fmt.Printf("Error cloning extended entries: \"%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("Encryption Password: ") - err, passwd = readPassword() - - if err == nil { - fmt.Printf("\nEncryption Password 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("Encryption Password: ") + err, passwd = readPassword() + + if err == nil { + fmt.Printf("\nEncryption Password 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("Decryption Password: ") - err, passwd = readPassword() + fmt.Printf("Decryption 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("...decryption complete.") - } + if err == nil { + fmt.Println("...decryption complete.") + } - return err, passwd + 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 + 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 = 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, flag = isFileEncrypted(dbPath); flag { + err, passwd = decryptDatabase(dbPath) + if err != nil { + fmt.Printf("Error decrypting - %s: %s\n", dbPath, err.Error()) + return err + } + } - if err != nil { - return err - } - err, db = openDatabase(dbPath) + err, db = openDatabase(dbPath) - if err != nil { - fmt.Printf("Error opening database path - %s: %s\n", dbPath, err.Error()) - return err - } + 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{}) + 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 - } + if err != nil { + fmt.Printf("Error migrating table \"entries\" - %s: %s\n", dbPath, err.Error()) + return err + } - err = db.AutoMigrate(&ExtendedEntry{}) + err = db.AutoMigrate(&ExtendedEntry{}) - if err != nil { - fmt.Printf("Error migrating table \"exentries\" - %s: %s\n", dbPath, err.Error()) - return err - } + 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) - } + if flag { + // File was encrypted - encrypt it again + encryptDatabase(dbPath, &passwd) + } - fmt.Println("Migration successful.") + fmt.Println("Migration successful.") - return nil + return nil } // 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 } diff --git a/db.go b/db.go index a444b71..2b62c57 100644 --- a/db.go +++ b/db.go @@ -17,15 +17,22 @@ import ( // Structure representing an entry in the db type Entry struct { ID int `gorm:"column:id;autoIncrement;primaryKey"` - Title string `gorm:"column:title"` - User string `gorm:"column:user"` - Url string `gorm:"column:url"` - Password string `gorm:"column:password"` + Title string `gorm:"column:title"` // For card type this -> Card Name + User string `gorm:"column:user"` // For card type this -> Card Holder Name + Url string `gorm:"column:url"` // For card type this -> Card Number + Password string `gorm:"column:password"` // For card type this -> CVV number + Pin string `gorm:"column:pin"` // For card type this -> card pin + ExpiryDate string `gorm:"colum:expiry_date"` // For card type this -> Card expiry date + Issuer string `gorm:"column:issuer"` // For card type this -> Issuing bank + Class string `gorm:"column:class"` // For card type this -> visa/mastercard/amex etc + Notes string `gorm:"column:notes"` Tags string `gorm:"column:tags"` + Type string `gorm:"column:type"` // Entry type, default/card/ID Timestamp time.Time `gorm:"type:timestamp;default:(datetime('now','localtime'))"` // sqlite3 } + func (e *Entry) TableName() string { return "entries" } @@ -259,6 +266,47 @@ func addNewDatabaseEntry(title, userName, url, passwd, tags string, return err } +// Add a new card entry to current database +func addNewDatabaseCardEntry(cardName, cardNumber, cardHolder, cardIssuer, cardClass, + cardCvv, cardPin, cardExpiry, notes, tags string, customEntries []CustomEntry) error { + + var entry Entry + var err error + var db *gorm.DB + + entry = Entry{ + Title: cardName, + User: cardHolder, + Url: cardNumber, + Password: cardCvv, + Pin: cardPin, + Issuer: cardIssuer, + Class: cardClass, + ExpiryDate: cardExpiry, + Type: "card", + Tags: strings.TrimSpace(tags), + Notes: notes} + + err, db = openActiveDatabase() + if err == nil && db != nil { + // 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 + } + } + + return err +} + + // Update current database entry with new values func updateDatabaseEntry(entry *Entry, title, userName, url, passwd, tags string, notes string, customEntries []CustomEntry, flag bool) error { diff --git a/go.mod b/go.mod index f320c0a..7417d0e 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,9 @@ module varuh go 1.16 require ( - github.com/akamensky/argparse v1.3.1 github.com/atotto/clipboard v0.1.4 github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f + github.com/polyglothacker/creditcard v0.0.0-20220814132008-214952378026 github.com/pythonhacker/argparse v1.3.2 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 gorm.io/driver/sqlite v1.2.3 diff --git a/go.sum b/go.sum index b6161ad..8e58640 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/akamensky/argparse v1.3.1 h1:kP6+OyvR0fuBH6UhbE6yh/nskrDEIQgEA1SUXDPjx4g= -github.com/akamensky/argparse v1.3.1/go.mod h1:S5kwC7IuDcEr5VeXtGPRVZ5o/FdhcMlQz4IZQuw64xA= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -15,9 +13,12 @@ github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/K github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/polyglothacker/creditcard v0.0.0-20220814132008-214952378026 h1:UGQ0EYOPlnXlhGGTlRXIqGhKViXU7Ro+EIl+S+Ui8AY= +github.com/polyglothacker/creditcard v0.0.0-20220814132008-214952378026/go.mod h1:F7aq1XexOpEd3ipbid3ZwJkijRyBf5p1EBVU5MycFb8= github.com/pythonhacker/argparse v1.3.2 h1:JOojnYFHk7oap+MQiFgiPAHlzvhJfqukErLneWaHR/M= github.com/pythonhacker/argparse v1.3.2/go.mod h1:gdUstTr/g1ojhRwrF9gKFOVLwsNfwarBg8aCQRjtvo8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= @@ -31,6 +32,7 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/sqlite v1.2.3 h1:OwKm0xRAnsZMWAl5BtXJ9BsXAZHIt802DOTVMQuzWN8= diff --git a/main.go b/main.go index c1ce900..2f878b5 100644 --- a/main.go +++ b/main.go @@ -3,10 +3,10 @@ package main import ( - "fmt" - "github.com/pythonhacker/argparse" - "os" - "strings" + "fmt" + "github.com/pythonhacker/argparse" + "os" + "strings" ) const VERSION = 0.4 @@ -21,231 +21,245 @@ type actionFunc func(string) error type actionFunc2 func(string) (error, string) type voidFunc func() error type voidFunc2 func() (error, string) +type settingFunc func(string) // Structure to keep the options data type CmdOption struct { - Short string - Long string - Help string - Path string - Default string + Short string + Long string + Help string + Path string + Default string } // Print the program's usage string and exit func printUsage() error { - // getopt.Usage() - os.Exit(0) + // getopt.Usage() + os.Exit(0) - return nil + return nil } // Print the program's version info and exit func printVersionInfo() error { - fmt.Printf("%s version %.2f\n", APP, VERSION) - os.Exit(0) + fmt.Printf("%s version %.2f\n", APP, VERSION) + os.Exit(0) - return nil + return nil } // Command-line wrapper to generateRandomPassword func genPass() (error, string) { - var err error - var passwd string + var err error + var passwd string - err, passwd = generateStrongPassword() + err, passwd = generateStrongPassword() - if err != nil { - fmt.Printf("Error generating password - \"%s\"\n", err.Error()) - return err, "" - } + if err != nil { + fmt.Printf("Error generating password - \"%s\"\n", err.Error()) + return err, "" + } - fmt.Println(passwd) + fmt.Println(passwd) - if settingsRider.CopyPassword { - copyPasswordToClipboard(passwd) - fmt.Println("Password copied to clipboard") - } + if settingsRider.CopyPassword { + copyPasswordToClipboard(passwd) + fmt.Println("Password copied to clipboard") + } - return nil, passwd + return nil, passwd } // // Perform an action by using the command line options map func performAction(optMap map[string]interface{}) { - var flag bool - - boolActionsMap := map[string]voidFunc{ - "add": WrapperMaxKryptVoidFunc(addNewEntry), - "version": printVersionInfo, - "help": printUsage, - "path": showActiveDatabasePath, - "list-all": WrapperMaxKryptVoidFunc(listAllEntries), - "encrypt": encryptActiveDatabase, - } - - stringActionsMap := map[string]actionFunc{ - "edit": WrapperMaxKryptStringFunc(editCurrentEntry), - "init": initNewDatabase, - "list-entry": WrapperMaxKryptStringFunc(listCurrentEntry), - "remove": WrapperMaxKryptStringFunc(removeCurrentEntry), - "clone": WrapperMaxKryptStringFunc(copyCurrentEntry), - "use-db": setActiveDatabasePath, - "export": exportToFile, - "migrate": migrateDatabase, - } - - stringListActionsMap := map[string]actionFunc{ - "find": WrapperMaxKryptStringFunc(findCurrentEntry), - } - - stringActions2Map := map[string]actionFunc2{ - "decrypt": decryptDatabase, - } - - flagsActions2Map := map[string]voidFunc2{ - "genpass": genPass, - } - - flagsActionsMap := map[string]voidFunc{ - "show": setShowPasswords, - "copy": setCopyPasswordToClipboard, - "assume-yes": setAssumeYes, - } - - // Flag actions - always done - for key, mappedFunc := range flagsActionsMap { - if *optMap[key].(*bool) { - mappedFunc() - } - } - - // Flag 2 actions - for key, mappedFunc := range flagsActions2Map { - if *optMap[key].(*bool) { - mappedFunc() - flag = true - break - } - } - - // One of bool or string actions - for key, mappedFunc := range boolActionsMap { - if *optMap[key].(*bool) { - mappedFunc() - flag = true - break - } - } - - if flag { - return - } - - for key, mappedFunc := range stringActionsMap { - if *optMap[key].(*string) != "" { - - var val = *(optMap[key].(*string)) - mappedFunc(val) - flag = true - break - } - } - - 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 - } - - for key, mappedFunc := range stringActions2Map { - if *optMap[key].(*string) != "" { - var val = *(optMap[key].(*string)) - mappedFunc(val) - break - } - } + var flag bool + + boolActionsMap := map[string]voidFunc{ + "add": WrapperMaxKryptVoidFunc(addNewEntry), + "version": printVersionInfo, + "help": printUsage, + "path": showActiveDatabasePath, + "list-all": WrapperMaxKryptVoidFunc(listAllEntries), + "encrypt": encryptActiveDatabase, + } + + stringActionsMap := map[string]actionFunc{ + "edit": WrapperMaxKryptStringFunc(editCurrentEntry), + "init": initNewDatabase, + "list-entry": WrapperMaxKryptStringFunc(listCurrentEntry), + "remove": WrapperMaxKryptStringFunc(removeCurrentEntry), + "clone": WrapperMaxKryptStringFunc(copyCurrentEntry), + "use-db": setActiveDatabasePath, + "export": exportToFile, + "migrate": migrateDatabase, + } + + stringListActionsMap := map[string]actionFunc{ + "find": WrapperMaxKryptStringFunc(findCurrentEntry), + } + + stringActions2Map := map[string]actionFunc2{ + "decrypt": decryptDatabase, + } + + flagsActions2Map := map[string]voidFunc2{ + "genpass": genPass, + } + + flagsActionsMap := map[string]voidFunc{ + "show": setShowPasswords, + "copy": setCopyPasswordToClipboard, + "assume-yes": setAssumeYes, + } + + flagsSettingsMap := map[string]settingFunc{ + "type": setType, + } + + // Flag actions - always done + for key, mappedFunc := range flagsActionsMap { + if *optMap[key].(*bool) { + mappedFunc() + } + } + + // Flag 2 actions + for key, mappedFunc := range flagsActions2Map { + if *optMap[key].(*bool) { + mappedFunc() + flag = true + break + } + } + + // Settings + for key, mappedFunc := range flagsSettingsMap { + if *optMap[key].(*string) != ""{ + var val = *(optMap[key].(*string)) + mappedFunc(val) + } + } + + // One of bool or string actions + for key, mappedFunc := range boolActionsMap { + if *optMap[key].(*bool) { + mappedFunc() + flag = true + break + } + } + + if flag { + return + } + + for key, mappedFunc := range stringActionsMap { + if *optMap[key].(*string) != "" { + + var val = *(optMap[key].(*string)) + mappedFunc(val) + flag = true + break + } + } + + 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 + } + + for key, mappedFunc := range stringActions2Map { + if *optMap[key].(*string) != "" { + var val = *(optMap[key].(*string)) + mappedFunc(val) + break + } + } } func initializeCmdLine(parser *argparse.Parser) map[string]interface{} { - var optMap map[string]interface{} - - optMap = make(map[string]interface{}) - - stringOptions := []CmdOption{ - {"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", "", ""}, - } - - for _, opt := range stringOptions { - 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", "", ""}, - {"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 this help message and exit", "", ""}, - } - - for _, opt := range boolOptions { - optMap[opt.Long] = parser.Flag(string(opt.Short), opt.Long, &argparse.Options{Help: opt.Help}) - } - - return optMap + var optMap map[string]interface{} + + optMap = make(map[string]interface{}) + + stringOptions := []CmdOption{ + {"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", "", ""}, + {"t", "type", "Specify type when adding a new entry", "", ""}, + } + + for _, opt := range stringOptions { + 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", "", ""}, + {"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 this help message and exit", "", ""}, + } + + for _, opt := range boolOptions { + optMap[opt.Long] = parser.Flag(string(opt.Short), opt.Long, &argparse.Options{Help: opt.Help}) + } + + return optMap } // Main routine func main() { - if len(os.Args) == 1 { - os.Args = append(os.Args, "-h") - } + if len(os.Args) == 1 { + os.Args = append(os.Args, "-h") + } - parser := argparse.NewParser("varuh", - "Password manager for the command line for Unix like operating systems", - AUTHOR_INFO, - ) + parser := argparse.NewParser("varuh", + "Password manager for the command line for Unix like operating systems", + AUTHOR_INFO, + ) - optMap := initializeCmdLine(parser) + optMap := initializeCmdLine(parser) - err := parser.Parse(os.Args) + err := parser.Parse(os.Args) - if err != nil { - fmt.Println(parser.Usage(err)) - } + if err != nil { + fmt.Println(parser.Usage(err)) + } - getOrCreateLocalConfig(APP) + getOrCreateLocalConfig(APP) - performAction(optMap) + performAction(optMap) } diff --git a/utils.go b/utils.go index 8c57d62..7adca13 100644 --- a/utils.go +++ b/utils.go @@ -2,47 +2,52 @@ package main import ( - "bufio" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "github.com/atotto/clipboard" - "github.com/kirsle/configdir" - "golang.org/x/crypto/ssh/terminal" - "io/fs" - "os" - "path/filepath" - "strings" + "bufio" + "time" + "regexp" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "github.com/atotto/clipboard" + "github.com/kirsle/configdir" + "golang.org/x/crypto/ssh/terminal" + "io/fs" + "os" + "path/filepath" + "strings" + "strconv" + "github.com/polyglothacker/creditcard" ) const DELIMSIZE int = 69 // Over-ride settings via cmd line type SettingsOverride struct { - ShowPasswords bool - CopyPassword bool - AssumeYes bool + ShowPasswords bool + CopyPassword bool + AssumeYes bool + Type string // Type of entity to add } // Settings structure for local config type Settings struct { - ActiveDB string `json:"active_db"` - Cipher string `json:"cipher"` - AutoEncrypt bool `json:"auto_encrypt"` - KeepEncrypted bool `json:"encrypt_on"` - ShowPasswords bool `json:"visible_passwords"` - ConfigPath string `json:"path"` - // Key to order listings when using -a option - // Valid values are - // 1. timestamp,{desc,asc} - // 2. title,{desc,asc} - // 3. username, {desc,asc} - // 4. id, {desc,asc{ - ListOrder string `json:"list_order"` - Delim string `json:"delimiter"` - Color string `json:"color"` // fg color to print - BgColor string `json:"bgcolor"` // bg color to print + ActiveDB string `json:"active_db"` + Cipher string `json:"cipher"` + AutoEncrypt bool `json:"auto_encrypt"` + KeepEncrypted bool `json:"encrypt_on"` + ShowPasswords bool `json:"visible_passwords"` + ConfigPath string `json:"path"` + // Key to order listings when using -a option + // Valid values are + // 1. timestamp,{desc,asc} + // 2. title,{desc,asc} + // 3. username, {desc,asc} + // 4. id, {desc,asc{ + ListOrder string `json:"list_order"` + Delim string `json:"delimiter"` + Color string `json:"color"` // fg color to print + BgColor string `json:"bgcolor"` // bg color to print } // Global settings override @@ -51,434 +56,608 @@ var settingsRider SettingsOverride // Write settings to disk func writeSettings(settings *Settings, configFile string) error { - fh, err := os.Create(configFile) - if err != nil { - fmt.Printf("Error generating configuration file %s - \"%s\"\n", configFile, err.Error()) - return err - } + fh, err := os.Create(configFile) + if err != nil { + fmt.Printf("Error generating configuration file %s - \"%s\"\n", configFile, err.Error()) + return err + } - defer fh.Close() + defer fh.Close() - encoder := json.NewEncoder(fh) - encoder.SetIndent("", "\t") - err = encoder.Encode(&settings) + encoder := json.NewEncoder(fh) + encoder.SetIndent("", "\t") + err = encoder.Encode(&settings) - return err + return err } // Write updated settings to disk func updateSettings(settings *Settings, configFile string) error { - fh, err := os.OpenFile(configFile, os.O_RDWR, 0644) - if err != nil { - fmt.Printf("Error opening config file %s - \"%s\"\n", configFile, err.Error()) - return err - } + fh, err := os.OpenFile(configFile, os.O_RDWR, 0644) + if err != nil { + fmt.Printf("Error opening config file %s - \"%s\"\n", configFile, err.Error()) + return err + } - defer fh.Close() + defer fh.Close() - encoder := json.NewEncoder(fh) - encoder.SetIndent("", "\t") - err = encoder.Encode(&settings) + encoder := json.NewEncoder(fh) + encoder.SetIndent("", "\t") + err = encoder.Encode(&settings) - if err != nil { - fmt.Printf("Error updating config %s - \"%s\"\n", configFile, err.Error()) - return err - } + if err != nil { + fmt.Printf("Error updating config %s - \"%s\"\n", configFile, err.Error()) + return err + } - return err + return err } // Make the per-user configuration folder and return local settings func getOrCreateLocalConfig(app string) (error, *Settings) { - var settings Settings - var configPath string - var configFile string - var err error - var fh *os.File - - configPath = configdir.LocalConfig(app) - err = configdir.MakePath(configPath) // Ensure it exists. - if err != nil { - return err, nil - } - - configFile = filepath.Join(configPath, "config.json") - // fmt.Printf("Config file, path => %s %s\n", configFile, configPath) - - if _, err = os.Stat(configFile); err == nil { - fh, err = os.Open(configFile) - if err != nil { - return err, nil - } - - defer fh.Close() - - decoder := json.NewDecoder(fh) - err = decoder.Decode(&settings) - if err != nil { - return err, nil - } - - } else { - // fmt.Printf("Creating default configuration ...") - settings = Settings{"", "aes", true, true, false, configFile, "id,asc", ">", "default", "bgblack"} - - if err = writeSettings(&settings, configFile); err == nil { - // fmt.Println(" ...done") - } else { - return err, nil - } - } - - return nil, &settings + var settings Settings + var configPath string + var configFile string + var err error + var fh *os.File + + configPath = configdir.LocalConfig(app) + err = configdir.MakePath(configPath) // Ensure it exists. + if err != nil { + return err, nil + } + + configFile = filepath.Join(configPath, "config.json") + // fmt.Printf("Config file, path => %s %s\n", configFile, configPath) + + if _, err = os.Stat(configFile); err == nil { + fh, err = os.Open(configFile) + if err != nil { + return err, nil + } + + defer fh.Close() + + decoder := json.NewDecoder(fh) + err = decoder.Decode(&settings) + if err != nil { + return err, nil + } + + } else { + // fmt.Printf("Creating default configuration ...") + settings = Settings{"", "aes", true, true, false, configFile, "id,asc", ">", "default", "bgblack"} + + if err = writeSettings(&settings, configFile); err == nil { + // fmt.Println(" ...done") + } else { + return err, nil + } + } + + return nil, &settings } // Return if there is an active, decrypted database func hasActiveDatabase() bool { - err, settings := getOrCreateLocalConfig(APP) - if err == nil && settings.ActiveDB != "" { - if _, err := os.Stat(settings.ActiveDB); err == nil { - if _, flag := isFileEncrypted(settings.ActiveDB); !flag { - return true - } - return false - } - } - - if err != nil { - fmt.Printf("Error parsing local config - \"%s\"\n", err.Error()) - } - - return false + err, settings := getOrCreateLocalConfig(APP) + if err == nil && settings.ActiveDB != "" { + if _, err := os.Stat(settings.ActiveDB); err == nil { + if _, flag := isFileEncrypted(settings.ActiveDB); !flag { + return true + } + return false + } + } + + if err != nil { + fmt.Printf("Error parsing local config - \"%s\"\n", err.Error()) + } + + return false } // Get the current active database func getActiveDatabase() (error, string) { - err, settings := getOrCreateLocalConfig(APP) - if err == nil && settings.ActiveDB != "" { - if _, err := os.Stat(settings.ActiveDB); err == nil { - return nil, settings.ActiveDB - } - } + err, settings := getOrCreateLocalConfig(APP) + if err == nil && settings.ActiveDB != "" { + if _, err := os.Stat(settings.ActiveDB); err == nil { + return nil, settings.ActiveDB + } + } - if err != nil { - fmt.Printf("Error parsing local config - \"%s\"\n", err.Error()) - } + if err != nil { + fmt.Printf("Error parsing local config - \"%s\"\n", err.Error()) + } - return err, "" + return err, "" } // Update the active db path func updateActiveDbPath(dbPath string) error { - _, settings := getOrCreateLocalConfig(APP) + _, settings := getOrCreateLocalConfig(APP) - if settings != nil { - settings.ActiveDB = dbPath - } + if settings != nil { + settings.ActiveDB = dbPath + } - return updateSettings(settings, settings.ConfigPath) + return updateSettings(settings, settings.ConfigPath) } // Read the password from console without echoing func readPassword() (error, string) { - var passwd []byte - var err error + var passwd []byte + var err error - passwd, err = terminal.ReadPassword(int(os.Stdin.Fd())) - return err, string(passwd) + passwd, err = terminal.ReadPassword(int(os.Stdin.Fd())) + return err, string(passwd) } // Rewrite the contents of the base file (path minus extension) with the new contents func rewriteBaseFile(path string, contents []byte, mode fs.FileMode) (error, string) { - var err error - var origFile string + var err error + var origFile string - origFile = strings.TrimSuffix(path, filepath.Ext(path)) - // Overwrite it - err = os.WriteFile(origFile, contents, 0644) + origFile = strings.TrimSuffix(path, filepath.Ext(path)) + // Overwrite it + err = os.WriteFile(origFile, contents, 0644) - if err == nil { - // Chmod it - os.Chmod(origFile, mode) - } + if err == nil { + // Chmod it + os.Chmod(origFile, mode) + } - return err, origFile + 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 + var err error - // Overwrite it - err = os.WriteFile(path, contents, 0644) + // Overwrite it + err = os.WriteFile(path, contents, 0644) - if err == nil { - // Chmod it - os.Chmod(path, mode) - } + if err == nil { + // Chmod it + os.Chmod(path, mode) + } - return err, path + return err, path } // Get color codes for console colors func getColor(code string) string { - colors := map[string]string{ - "black": "\x1b[30m", - "blue": "\x1B[34m", - "red": "\x1B[31m", - "green": "\x1B[32m", - "yellow": "\x1B[33m", - "magenta": "\x1B[35m", - "cyan": "\x1B[36m", - "white": "\x1B[37m", - - // From https://gist.github.com/abritinthebay/d80eb99b2726c83feb0d97eab95206c4 - // esoteric options - "bright": "\x1b[1m", - "dim": "\x1b[2m", - "underscore": "\x1b[4m", - "blink": "\x1b[5m", - "reverse": "\x1b[7m", - "hidden": "\x1b[8m", - - // background color options - "bgblack": "\x1b[40m", - "bgred": "\x1b[41m", - "bggreen": "\x1b[42m", - "bgyellow": "\x1b[43m", - "bgblue": "\x1b[44m", - "bgmagenta": "\x1b[45m", - "bgcyan": "\x1b[46m", - "bgwhite": "\x1b[47m", - - // reset color code - "reset": "\x1B[0m", - "default": "\x1B[0m", - } - - if color, ok := colors[code]; ok { - return color - } else { - return colors["default"] - } + colors := map[string]string{ + "black": "\x1b[30m", + "blue": "\x1B[34m", + "red": "\x1B[31m", + "green": "\x1B[32m", + "yellow": "\x1B[33m", + "magenta": "\x1B[35m", + "cyan": "\x1B[36m", + "white": "\x1B[37m", + + // From https://gist.github.com/abritinthebay/d80eb99b2726c83feb0d97eab95206c4 + // esoteric options + "bright": "\x1b[1m", + "dim": "\x1b[2m", + "underscore": "\x1b[4m", + "blink": "\x1b[5m", + "reverse": "\x1b[7m", + "hidden": "\x1b[8m", + + // background color options + "bgblack": "\x1b[40m", + "bgred": "\x1b[41m", + "bggreen": "\x1b[42m", + "bgyellow": "\x1b[43m", + "bgblue": "\x1b[44m", + "bgmagenta": "\x1b[45m", + "bgcyan": "\x1b[46m", + "bgwhite": "\x1b[47m", + + // reset color code + "reset": "\x1B[0m", + "default": "\x1B[0m", + } + + if color, ok := colors[code]; ok { + return color + } else { + return colors["default"] + } } // Print the delimiter line for listings func printDelim(delimChar string, color string) { - var delims []string + var delims []string - if color == "underscore" { - // Override delimieter to space - delimChar = " " - } + if color == "underscore" { + // Override delimieter to space + delimChar = " " + } - if len(delimChar) > 1 { - // slice it - take only the first - delimChar = string(delimChar[0]) - } - for i := 0; i < DELIMSIZE; i++ { - delims = append(delims, delimChar) - } + if len(delimChar) > 1 { + // slice it - take only the first + delimChar = string(delimChar[0]) + } + for i := 0; i < DELIMSIZE; i++ { + delims = append(delims, delimChar) + } - fmt.Println(strings.Join(delims, "")) + fmt.Println(strings.Join(delims, "")) } -// Print an entry to the console -func printEntry(entry *Entry, delim bool) error { - - var err error - var settings *Settings - var customEntries []ExtendedEntry - - 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) - } +// Print a card entry to the console +func printCardEntry(entry *Entry, settings* Settings, delim bool) error { + + var customEntries []ExtendedEntry + + 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("ID: %d\n", entry.ID) + fmt.Printf("Card Name: %s\n", entry.Title) + fmt.Printf("Card Holder: %s\n", entry.User) + fmt.Printf("Card Number: %s\n", entry.Url) + fmt.Printf("Card Type: %s\n", entry.Class) + + if entry.Issuer != "" { + fmt.Printf("Issuing Bank: %s\n", entry.Issuer) + } + + fmt.Println() + + fmt.Printf("Expiry Date: %s\n", entry.ExpiryDate) + + if settings.ShowPasswords || settingsRider.ShowPasswords { + fmt.Printf("Card CVV: %s\n", entry.Password) + fmt.Printf("Card PIN: %s\n", entry.Pin) + } else { + var asterisks1 []string + var asterisks2 []string + var i int + + for i = 0; i < len(entry.Password); i++ { + asterisks1 = append(asterisks1, "*") + } + fmt.Printf("Card CVV: %s\n", strings.Join(asterisks1, "")) + + for i = 0; i < len(entry.Pin); i++ { + asterisks2 = append(asterisks2, "*") + } + fmt.Printf("Card PIN: %s\n", strings.Join(asterisks2, "")) + } + + if len(entry.Tags) > 0 { + fmt.Printf("\nTags: %s\n", entry.Tags) + } + if len(entry.Notes) > 0 { + 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 + fmt.Printf("%s", getColor("default")) + + return nil - fmt.Printf("ID: %d\n", entry.ID) - fmt.Printf("Title: %s\n", entry.Title) - fmt.Printf("User: %s\n", entry.User) - fmt.Printf("URL: %s\n", entry.Url) - - if settings.ShowPasswords || settingsRider.ShowPasswords { - fmt.Printf("Password: %s\n", entry.Password) - } else { - var asterisks []string - - for i := 0; i < len(entry.Password); i++ { - asterisks = append(asterisks, "*") - } - fmt.Printf("Password: %s\n", strings.Join(asterisks, "")) - } - - 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) - - 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 - fmt.Printf("%s", getColor("default")) +// Print an entry to the console +func printEntry(entry *Entry, delim bool) error { - return nil + var err error + var settings *Settings + var customEntries []ExtendedEntry + + err, settings = getOrCreateLocalConfig(APP) + + if err != nil { + fmt.Printf("Error parsing config - \"%s\"\n", err.Error()) + return err + } + + if entry.Type == "card" { + return printCardEntry(entry, settings, delim) + } + + 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("ID: %d\n", entry.ID) + fmt.Printf("Title: %s\n", entry.Title) + fmt.Printf("User: %s\n", entry.User) + fmt.Printf("URL: %s\n", entry.Url) + + if settings.ShowPasswords || settingsRider.ShowPasswords { + fmt.Printf("Password: %s\n", entry.Password) + } else { + var asterisks []string + + for i := 0; i < len(entry.Password); i++ { + asterisks = append(asterisks, "*") + } + fmt.Printf("Password: %s\n", strings.Join(asterisks, "")) + } + + 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) + + 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 + fmt.Printf("%s", getColor("default")) + + return nil } // Print an entry to the console with minimal data func printEntryMinimal(entry *Entry, delim bool) error { - var err error - var settings *Settings + var err error + var settings *Settings - err, settings = getOrCreateLocalConfig(APP) + err, settings = getOrCreateLocalConfig(APP) - if err != nil { - fmt.Printf("Error parsing config - \"%s\"\n", err.Error()) - return err - } + 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))) - } + 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) - } + 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")) + 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) + printDelim(settings.Delim, settings.Color) - // Reset - fmt.Printf("%s", getColor("default")) + // Reset + fmt.Printf("%s", getColor("default")) - return nil + return nil } // Read user input and return entered value func readInput(reader *bufio.Reader, prompt string) string { - var input string - fmt.Printf(prompt + ": ") - input, _ = reader.ReadString('\n') + var input string + fmt.Printf(prompt + ": ") + input, _ = reader.ReadString('\n') - return strings.TrimSpace(input) + return strings.TrimSpace(input) } // Check for an active, decrypted database func checkActiveDatabase() error { - if !hasActiveDatabase() { - fmt.Printf("No decrypted active database found.\n") - return errors.New("no active database") - } + if !hasActiveDatabase() { + fmt.Printf("No decrypted active database found.\n") + return errors.New("no active database") + } - return nil + return nil } // Return true if active database is encrypted func isActiveDatabaseEncrypted() bool { - err, settings := getOrCreateLocalConfig(APP) - if err == nil && settings.ActiveDB != "" { - if _, err := os.Stat(settings.ActiveDB); err == nil { - if _, flag := isFileEncrypted(settings.ActiveDB); flag { - return true - } - } - } + err, settings := getOrCreateLocalConfig(APP) + if err == nil && settings.ActiveDB != "" { + if _, err := os.Stat(settings.ActiveDB); err == nil { + if _, flag := isFileEncrypted(settings.ActiveDB); flag { + return true + } + } + } - return false + return false } // Return true if always encrypt is on func isEncryptOn() bool { - _, settings := getOrCreateLocalConfig(APP) - return settings.KeepEncrypted + _, settings := getOrCreateLocalConfig(APP) + return settings.KeepEncrypted } // Combination of above 2 logic plus auto encryption on (a play on CryptOn) func isActiveDatabaseEncryptedAndMaxKryptOn() (bool, string) { - err, settings := getOrCreateLocalConfig(APP) - if err == nil && settings.ActiveDB != "" { - if _, err := os.Stat(settings.ActiveDB); err == nil { - if _, flag := isFileEncrypted(settings.ActiveDB); flag && settings.KeepEncrypted && settings.AutoEncrypt { - return true, settings.ActiveDB - } - } - } + err, settings := getOrCreateLocalConfig(APP) + if err == nil && settings.ActiveDB != "" { + if _, err := os.Stat(settings.ActiveDB); err == nil { + if _, flag := isFileEncrypted(settings.ActiveDB); flag && settings.KeepEncrypted && settings.AutoEncrypt { + return true, settings.ActiveDB + } + } + } - return false, "" + return false, "" } // (Temporarily) enable showing of passwords func setShowPasswords() error { - // fmt.Printf("Setting show passwords to true\n") - settingsRider.ShowPasswords = true - return nil + // fmt.Printf("Setting show passwords to true\n") + settingsRider.ShowPasswords = true + return nil } // Copy the password to clipboard - only for single listings or single search results func setCopyPasswordToClipboard() error { - settingsRider.CopyPassword = true - return nil + settingsRider.CopyPassword = true + return nil } func setAssumeYes() error { - settingsRider.AssumeYes = true - return nil + settingsRider.AssumeYes = true + return nil +} + +func setType(_type string) { + settingsRider.Type = _type } func copyPasswordToClipboard(passwd string) { - clipboard.WriteAll(passwd) + clipboard.WriteAll(passwd) } // Generate a random file name func randomFileName(folder string, suffix string) string { - _, name := generateRandomBytes(16) - return filepath.Join(folder, hex.EncodeToString(name)+suffix) + _, name := generateRandomBytes(16) + return filepath.Join(folder, hex.EncodeToString(name)+suffix) +} + +// Detect card type from card number +func detectCardType(cardNum string) (string, error) { + + var cardTypeIndex creditcard.CardType + var err error + + card := creditcard.Card{ + Type: "N/A", + Number: cardNum, + ExpiryMonth: 12, + ExpiryYear: 99, + CVV: "999", + } + + cardTypeIndex, err = card.DetermineCardType() + if err != nil { + return "", err + } + + return creditcard.CardTypeNames[cardTypeIndex], nil +} + +// Validate CVV +func validateCvv(cardCvv string, cardClass string) bool { + + var matched bool + + // Amex CVV is 4 digits, rest are 3 + if cardClass == "American Express" { + if matched, _ = regexp.Match(`^\d{4}$`, []byte(cardCvv)); matched { + return matched + } + } else { + if matched, _ = regexp.Match(`^\d{3}$`, []byte(cardCvv)); matched { + return matched + } + } + + return false +} + +func validateCardPin(cardPin string) bool { + + // A PIN is 4 digits or more + if matched, _ := regexp.Match(`^\d{4,}$`, []byte(cardPin)); matched { + return matched + } + + return false +} + +// Verify if the expiry date is in the form mm/dd +func checkValidExpiry(expiryDate string) bool { + + pieces := strings.Split(expiryDate, "/") + + if len(pieces) == 2 { + // Sofar, so good + var month int + var year int + var err error + + month, err = strconv.Atoi(pieces[0]) + if err != nil { + fmt.Printf("Error parsing month: %s: \"%s\"\n", month, err.Error()) + return false + } + year, err = strconv.Atoi(pieces[1]) + if err != nil { + fmt.Printf("Error parsing year: %s: \"%s\"\n", year, err.Error()) + return false + } + + // Month should be in range 1 -> 12 + if month < 1 || month > 12 { + fmt.Printf("Error: invalid value for month - %d!\n", month) + return false + } + // Year should be >= current year + currYear, _ := strconv.Atoi(strconv.Itoa(time.Now().Year())[2:]) + if year < currYear { + fmt.Printf("Error: year should be >= %d\n", currYear) + return false + } + + return true + } else { + fmt.Println("Error: invalid input") + return false + } + } From 96d816feb24163a1e8f9d9ecb9d90ff85dc04f05 Mon Sep 17 00:00:00 2001 From: Anand Balachandran Pillai Date: Mon, 15 Aug 2022 00:53:39 +0530 Subject: [PATCH 09/11] ref issue 28 - pretty print card numbers --- actions.go | 3 +-- utils.go | 23 ++++++++++++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/actions.go b/actions.go index f149966..6f75697 100644 --- a/actions.go +++ b/actions.go @@ -274,8 +274,7 @@ func addNewCardEntry() error { err, cardPin = readPassword() if !validateCardPin(cardPin) { - fmt.Printf("\nError - Invalid PIN") - return errors.New("Error - Invalid PIN") + fmt.Printf("\n") } cardIssuer = readInput(reader, "\nIssuing Bank") diff --git a/utils.go b/utils.go index 7adca13..9e4069a 100644 --- a/utils.go +++ b/utils.go @@ -300,6 +300,27 @@ func printDelim(delimChar string, color string) { fmt.Println(strings.Join(delims, "")) } +// Prettify credit/debit card numbers +func prettifyCardNumber(cardNumber string) string { + + // Amex cards are 15 digits - group as 4, 6, 5 + // Any 16 digits - group as 4/4/4/4 + var numbers []string + + if len(cardNumber) == 15 { + numbers = append(numbers, cardNumber[0:4]) + numbers = append(numbers, cardNumber[4:10]) + numbers = append(numbers, cardNumber[10:15]) + } else if len(cardNumber) == 16 { + numbers = append(numbers, cardNumber[0:4]) + numbers = append(numbers, cardNumber[4:8]) + numbers = append(numbers, cardNumber[8:12]) + numbers = append(numbers, cardNumber[12:16]) + } + + return strings.Join(numbers, " ") +} + // Print a card entry to the console func printCardEntry(entry *Entry, settings* Settings, delim bool) error { @@ -317,7 +338,7 @@ func printCardEntry(entry *Entry, settings* Settings, delim bool) error { fmt.Printf("ID: %d\n", entry.ID) fmt.Printf("Card Name: %s\n", entry.Title) fmt.Printf("Card Holder: %s\n", entry.User) - fmt.Printf("Card Number: %s\n", entry.Url) + fmt.Printf("Card Number: %s\n", prettifyCardNumber(entry.Url)) fmt.Printf("Card Type: %s\n", entry.Class) if entry.Issuer != "" { From 43e7a3281ba1a68a4056a738e1bf96f1732541d2 Mon Sep 17 00:00:00 2001 From: Anand Date: Sat, 19 Aug 2023 22:33:04 +0530 Subject: [PATCH 10/11] edit card entries correctly, print type of entry --- actions.go | 1903 ++++++++++++++++++++--------------------------- db.go | 1031 +++++++++++++------------ export.go | 383 ++++++++++ main.go | 408 +++++----- test/testpgp.go | 27 +- utils.go | 924 +++++++++++------------ 6 files changed, 2401 insertions(+), 2275 deletions(-) create mode 100644 export.go diff --git a/actions.go b/actions.go index 6f75697..7b024b4 100644 --- a/actions.go +++ b/actions.go @@ -2,1298 +2,989 @@ package main import ( - "bufio" - "encoding/csv" - "errors" - "fmt" - "gorm.io/gorm" - "os" - "os/exec" - "os/signal" - "path/filepath" - "strconv" - "strings" - "syscall" + "bufio" + "errors" + "fmt" + "gorm.io/gorm" + "os" + "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 { - 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 { - // 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) - } - } - } - - if !activeEncrypted { - // Use should manually encrypt before switching - fmt.Println("Auto-encrypt disabled, encrypt existing database before switching to new.") - return nil - } - - if newEncrypted && !settings.AutoEncrypt { - // 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 { + 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 { + // 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) + } + } + } + + if !activeEncrypted { + // Use should manually encrypt before switching + fmt.Println("Auto-encrypt disabled, encrypt existing database before switching to new.") + return nil + } + + if newEncrypted && !settings.AutoEncrypt { + // 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 for a card type func addNewCardEntry() error { - var cardHolder string - var cardName string - var cardNumber string - var cardCvv string - var cardPin string - var cardIssuer string - var cardClass string - var cardExpiry string - - var notes string - var tags string - var err error - var customEntries []CustomEntry - - if err = checkActiveDatabase(); err != nil { - return err - } - - reader := bufio.NewReader(os.Stdin) - cardNumber = readInput(reader, "Card Number") - cardClass, err = detectCardType(cardNumber) - - if err != nil { - fmt.Printf("Error - %s\n", err.Error()) - return err - } else { - fmt.Printf("\n", cardClass) - } - - cardHolder = readInput(reader, "Name on the Card") - cardExpiry = readInput(reader, "Expiry Date as mm/dd") - - // expiry has to be in the form of / - if !checkValidExpiry(cardExpiry) { - return errors.New("Invalid Expiry Date") - } - - fmt.Printf("CVV: ") - err, cardCvv = readPassword() - - if !validateCvv(cardCvv, cardClass) { - fmt.Printf("\nError - Invalid CVV for %s\n", cardClass) - return errors.New(fmt.Sprintf("Error - Invalid CVV for %s\n", cardClass)) - } - - fmt.Printf("\nCard PIN: ") - err, cardPin = readPassword() - - if !validateCardPin(cardPin) { - fmt.Printf("\n") - } - - cardIssuer = readInput(reader, "\nIssuing Bank") - cardName = readInput(reader, "A name for this Card") - // Name cant be blank - if len(cardName) == 0 { - fmt.Printf("Error - name cant be blank") - return errors.New("Empty card name") - } - - tags = readInput(reader, "\nTags (separated by space): ") - notes = readInput(reader, "Notes") - - customEntries = addCustomFields(reader) - - err = addNewDatabaseCardEntry(cardName, cardNumber, cardHolder, cardIssuer, - cardClass, cardCvv, cardPin, cardExpiry, notes, tags, customEntries) - - if err != nil { - fmt.Printf("Error adding entry - \"%s\"\n", err.Error()) - } - - return err + var cardHolder string + var cardName string + var cardNumber string + var cardCvv string + var cardPin string + var cardIssuer string + var cardClass string + var cardExpiry string + + var notes string + var tags string + var err error + var customEntries []CustomEntry + + if err = checkActiveDatabase(); err != nil { + return err + } + + reader := bufio.NewReader(os.Stdin) + cardNumber = readInput(reader, "Card Number") + cardClass, err = detectCardType(cardNumber) + + if err != nil { + fmt.Printf("Error - %s\n", err.Error()) + return err + } else { + fmt.Printf("\n", cardClass) + } + + cardHolder = readInput(reader, "Name on the Card") + cardExpiry = readInput(reader, "Expiry Date as mm/dd") + + // expiry has to be in the form of / + if !checkValidExpiry(cardExpiry) { + return errors.New("Invalid Expiry Date") + } + + fmt.Printf("CVV: ") + err, cardCvv = readPassword() + + if !validateCvv(cardCvv, cardClass) { + fmt.Printf("\nError - Invalid CVV for %s\n", cardClass) + return errors.New(fmt.Sprintf("Error - Invalid CVV for %s\n", cardClass)) + } + + fmt.Printf("\nCard PIN: ") + err, cardPin = readPassword() + + if !validateCardPin(cardPin) { + fmt.Printf("\n") + } + + cardIssuer = readInput(reader, "\nIssuing Bank") + cardName = readInput(reader, "A name for this Card") + // Name cant be blank + if len(cardName) == 0 { + fmt.Printf("Error - name cant be blank") + return errors.New("Empty card name") + } + + tags = readInput(reader, "\nTags (separated by space): ") + notes = readInput(reader, "Notes") + + customEntries = addCustomFields(reader) + + err = addNewDatabaseCardEntry(cardName, cardNumber, cardHolder, cardIssuer, + cardClass, cardCvv, cardPin, cardExpiry, notes, tags, customEntries) + + if err != nil { + fmt.Printf("Error adding entry - \"%s\"\n", err.Error()) + } + + return err } - // 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 tags string - var err error - var customEntries []CustomEntry - - if err = checkActiveDatabase(); err != nil { - return err - } - - if settingsRider.Type == "card" { - return addNewCardEntry() - } - - 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) - - tags = readInput(reader, "\nTags (separated by space): ") - notes = readInput(reader, "Notes") - - // 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, tags, 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 tags string + var err error + var customEntries []CustomEntry + + if err = checkActiveDatabase(); err != nil { + return err + } + + if settingsRider.Type == "card" { + return addNewCardEntry() + } + + 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) + + tags = readInput(reader, "\nTags (separated by space): ") + notes = readInput(reader, "Notes") + + // 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, tags, 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: " + customEntry.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: " + customEntry.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 card entry by id +func editCurrentCardEntry(entry *Entry) error { + var klass string + var err error + var flag bool + var customEntries []CustomEntry + + reader := bufio.NewReader(os.Stdin) + + fmt.Printf("Card Title: %s\n", entry.Title) + title := readInput(reader, "New Card Title") + fmt.Printf("Name on Card: %s\n", entry.User) + name := readInput(reader, "New Name on Card") + fmt.Printf("Card Number: %s\n", entry.Url) + number := readInput(reader, "New Card Number") + if number != "" { + klass, err = detectCardType(number) + + if err != nil { + fmt.Printf("Error - %s\n", err.Error()) + return err + } else { + fmt.Printf("\n", klass) + } + } + + fmt.Printf("Card CVV: %s\n", entry.Password) + fmt.Printf("New Card CVV: ") + err, cvv := readPassword() + + if cvv != "" && !validateCvv(cvv, klass) { + fmt.Printf("\nError - Invalid CVV for %s\n", klass) + return errors.New(fmt.Sprintf("Error - Invalid CVV for %s\n", klass)) + } + fmt.Printf("\nCard PIN: %s\n", entry.Pin) + fmt.Printf("New Card PIN: ") + err, pin := readPassword() + + if pin != "" && !validateCardPin(pin) { + fmt.Printf("\n") + } + fmt.Printf("\nCard Expiry Date: %s\n", entry.ExpiryDate) + expiryDate := readInput(reader, "New Card Expiry Date (as mm/dd): ") + // expiry has to be in the form of / + if expiryDate != "" && !checkValidExpiry(expiryDate) { + return errors.New("Invalid Expiry Date") + } + tags := readInput(reader, "\nTags (separated by space): ") + notes := readInput(reader, "Notes") + + customEntries, flag = addOrUpdateCustomFields(reader, entry) + + // Update + err = updateDatabaseCardEntry(entry, title, number, name, + klass, cvv, pin, expiryDate, notes, tags, customEntries, flag) + + if err != nil { + fmt.Printf("Error adding entry - \"%s\"\n", err.Error()) + } + + return nil } // Edit a current entry by id func editCurrentEntry(idString string) error { - var userName string - var title string - var url string - var notes string - var tags 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 tags 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 - } + if entry.Type == "card" { + return editCurrentCardEntry(entry) + } - 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 Tags: %s\n", entry.Tags) - tags = readInput(reader, "New Tags") + 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") + 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, tags, notes, customEntries, flag) - if err != nil { - fmt.Printf("Error updating entry - \"%s\"\n", err.Error()) - } + // Update + err = updateDatabaseEntry(entry, title, userName, url, passwd, tags, 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 - var terms []string - - if err = checkActiveDatabase(); err != nil { - return err - } - - 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 - } 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 + var terms []string + + if err = checkActiveDatabase(); err != nil { + return err + } + + 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 + } 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) - - 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 + 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 entryNew *Entry - var exEntries []ExtendedEntry + var err error + var entry *Entry + var entryNew *Entry + var exEntries []ExtendedEntry - var id int + 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, entryNew = cloneEntry(entry) - if err != nil { - fmt.Printf("Error cloning entry: \"%s\"\n", err.Error()) - return err - } + err, entryNew = cloneEntry(entry) + if err != nil { + fmt.Printf("Error cloning entry: \"%s\"\n", err.Error()) + return err + } - exEntries = getExtendedEntries(entry) + exEntries = getExtendedEntries(entry) - if len(exEntries) > 0 { - fmt.Printf("%d extended entries found\n", len(exEntries)) + 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 - } - } + err = cloneExtendedEntries(entryNew, exEntries) + if err != nil { + fmt.Printf("Error cloning extended entries: \"%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("Encryption Password: ") - err, passwd = readPassword() - - if err == nil { - fmt.Printf("\nEncryption Password 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("Encryption Password: ") + err, passwd = readPassword() + + if err == nil { + fmt.Printf("\nEncryption Password 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("Decryption Password: ") - err, passwd = readPassword() + fmt.Printf("Decryption 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("...decryption complete.") - } + if err == nil { + fmt.Println("...decryption complete.") + } - return err, passwd + 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 { - fmt.Printf("Error decrypting - %s: %s\n", dbPath, err.Error()) - 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 { - - 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 - -} - -// 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 - -} - -// 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 - -} - -// 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 "} - - err, dataArray = entriesToStringArray(false) - - 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 - } - - defer fh.Close() - - writer := bufio.NewWriter(fh) - - writer.WriteString("\n") - 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
\n") - - writer.WriteString("\n") - - writer.Flush() - - return nil - -} - -// Export current database to CSV -func exportToCsv(fileName string) error { + var err error + var flag bool + var passwd string + var db *gorm.DB - var err error - var dataArray [][]string - var fh *os.File + if _, err = os.Stat(dbPath); os.IsNotExist(err) { + fmt.Printf("Error - path %s does not exist\n", dbPath) + return err + } - err, dataArray = entriesToStringArray(false) + if err, flag = isFileEncrypted(dbPath); flag { + err, passwd = decryptDatabase(dbPath) + if err != nil { + fmt.Printf("Error decrypting - %s: %s\n", dbPath, err.Error()) + return err + } + } - if err != nil { - fmt.Printf("Error exporting entries to string array - \"%s\"\n", err.Error()) - return err - } + err, db = openDatabase(dbPath) - fh, err = os.Create(fileName) - if err != nil { - fmt.Printf("Cannt open \"%s\" for writing - \"%s\"\n", fileName, err.Error()) - return err - } + if err != nil { + fmt.Printf("Error opening database path - %s: %s\n", dbPath, err.Error()) + return err + } - writer := csv.NewWriter(fh) + fmt.Println("Migrating tables ...") + err = db.AutoMigrate(&Entry{}) - // Write header - writer.Write([]string{"ID", "Title", "User", "URL", "Password", "Notes", "Modified"}) + if err != nil { + fmt.Printf("Error migrating table \"entries\" - %s: %s\n", dbPath, err.Error()) + return err + } - 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 - } - } + err = db.AutoMigrate(&ExtendedEntry{}) - writer.Flush() + if err != nil { + fmt.Printf("Error migrating table \"exentries\" - %s: %s\n", dbPath, err.Error()) + return err + } - if err != nil { - return err - } + if flag { + // File was encrypted - encrypt it again + encryptDatabase(dbPath, &passwd) + } - 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) + fmt.Println("Migration successful.") - return nil + return nil } diff --git a/db.go b/db.go index ba29379..1b413cb 100644 --- a/db.go +++ b/db.go @@ -2,100 +2,99 @@ package main import ( - "database/sql" - "fmt" - "gorm.io/driver/sqlite" - "gorm.io/gorm" - "gorm.io/gorm/logger" - "os" - "path/filepath" - "strconv" - "strings" - "time" + "database/sql" + "fmt" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" + "os" + "path/filepath" + "strconv" + "strings" + "time" ) // Structure representing an entry in the db type Entry struct { - ID int `gorm:"column:id;autoIncrement;primaryKey"` - Title string `gorm:"column:title"` // For card type this -> Card Name - User string `gorm:"column:user"` // For card type this -> Card Holder Name - Url string `gorm:"column:url"` // For card type this -> Card Number - Password string `gorm:"column:password"` // For card type this -> CVV number - Pin string `gorm:"column:pin"` // For card type this -> card pin - ExpiryDate string `gorm:"colum:expiry_date"` // For card type this -> Card expiry date - Issuer string `gorm:"column:issuer"` // For card type this -> Issuing bank - Class string `gorm:"column:class"` // For card type this -> visa/mastercard/amex etc - - Notes string `gorm:"column:notes"` - Tags string `gorm:"column:tags"` - Type string `gorm:"column:type"` // Entry type, default/card/ID - Timestamp time.Time `gorm:"type:timestamp;default:(datetime('now','localtime'))"` // sqlite3 - -// ID int `gorm:"column:id;autoIncrement;primaryKey"` -// Type string `gorm:"column:type"` // Type of entry - password (default), card, identity etc -// Title string `gorm:"column:title"` -// Name string `gorm:"column:name"` // Card holder name/ID card name - for types cards/identity -// Company string `gorm:"column:company"` // Company name of person - for type identity and -// // Credit card company for type CC -// Number string `gorm:"column:number"` // Number type - CC number for credit cards -// // ID card number for identity types -// SecurityCode string `gorm:"security_code"` // CVV number/security code for CC type -// ExpiryMonth string `gorm:"expiry_month"` // CC or Identity document expiry month -// ExpiryDay string `gorm:"expiry_day"` // Identity document expiry day -// ExpiryYear string `gorm:"expiry_year"` // CC or Identity document expiry year -// FirstName string `gorm:"column:first_name"` // first name - for ID card types -// MiddleName string `gorm:"column:middle_name"` // middle name - for ID card types -// LastName string `gorm:"column:last_name"` // last name - for ID card types -// Email string `gorm:"email"` // Email - for ID card types -// PhoneNumber string `gorm:"phone_number"` // Phone number - for ID card types - -// Active bool `gorm:"active;default:true"` // Is the id card/CC active ? -// User string `gorm:"column:user"` -// 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 + ID int `gorm:"column:id;autoIncrement;primaryKey"` + Title string `gorm:"column:title"` // For card type this -> Card Name + User string `gorm:"column:user"` // For card type this -> Card Holder Name + Url string `gorm:"column:url"` // For card type this -> Card Number + Password string `gorm:"column:password"` // For card type this -> CVV number + Pin string `gorm:"column:pin"` // For card type this -> card pin + ExpiryDate string `gorm:"colum:expiry_date"` // For card type this -> Card expiry date + Issuer string `gorm:"column:issuer"` // For card type this -> Issuing bank + Class string `gorm:"column:class"` // For card type this -> visa/mastercard/amex etc + + Notes string `gorm:"column:notes"` + Tags string `gorm:"column:tags"` + Type string `gorm:"column:type"` // Entry type, default/card/ID + Timestamp time.Time `gorm:"type:timestamp;default:(datetime('now','localtime'))"` // sqlite3 + + // ID int `gorm:"column:id;autoIncrement;primaryKey"` + // Type string `gorm:"column:type"` // Type of entry - password (default), card, identity etc + // Title string `gorm:"column:title"` + // Name string `gorm:"column:name"` // Card holder name/ID card name - for types cards/identity + // Company string `gorm:"column:company"` // Company name of person - for type identity and + // // Credit card company for type CC + // Number string `gorm:"column:number"` // Number type - CC number for credit cards + // // ID card number for identity types + // SecurityCode string `gorm:"security_code"` // CVV number/security code for CC type + // ExpiryMonth string `gorm:"expiry_month"` // CC or Identity document expiry month + // ExpiryDay string `gorm:"expiry_day"` // Identity document expiry day + // ExpiryYear string `gorm:"expiry_year"` // CC or Identity document expiry year + // FirstName string `gorm:"column:first_name"` // first name - for ID card types + // MiddleName string `gorm:"column:middle_name"` // middle name - for ID card types + // LastName string `gorm:"column:last_name"` // last name - for ID card types + // Email string `gorm:"email"` // Email - for ID card types + // PhoneNumber string `gorm:"phone_number"` // Phone number - for ID card types + + // Active bool `gorm:"active;default:true"` // Is the id card/CC active ? + // User string `gorm:"column:user"` + // 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 } - func (e *Entry) TableName() string { - return "entries" + 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 + 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 + Entry Entry `gorm:"foreignKey:EntryID"` + EntryID int } func (ex *ExtendedEntry) TableName() string { - return "exentries" + return "exentries" } type Address struct { - ID int `gorm:"column:id;autoIncrement;primaryKey"` - Number string `gorm:"column:number"` // Flat or building number + ID int `gorm:"column:id;autoIncrement;primaryKey"` + Number string `gorm:"column:number"` // Flat or building number Building string `gorm:"column:building"` // Apartment or building or society name - Street string `gorm:"column:street"` // Street address + Street string `gorm:"column:street"` // Street address Locality string `gorm:"column:locality"` // Name of the locality e.g: Whitefield - Area string `gorm:"column:area"` // Name of the larger area e.g: East Bangalore - City string `gorm:"column:city"` // Name of the city e.g: Bangalore - State string `gorm:"column:state"` // Name of the state e.g: Karnataka - Country string `gorm:"column:country"` // Name of the country e.g: India + Area string `gorm:"column:area"` // Name of the larger area e.g: East Bangalore + City string `gorm:"column:city"` // Name of the city e.g: Bangalore + State string `gorm:"column:state"` // Name of the state e.g: Karnataka + Country string `gorm:"column:country"` // Name of the country e.g: India Landmark string `gorm:"column:landmark"` // Name of landmark if any - ZipCode string `gorm:"column:zipcode"` // PIN/ZIP code - Type string `gorm:"column:type"` // Type of address: Home/Work/Business + ZipCode string `gorm:"column:zipcode"` // PIN/ZIP code + Type string `gorm:"column:type"` // Type of address: Home/Work/Business Entry Entry `gorm:"foreignKey:EntryID"` - EntryID int + EntryID int } func (ad *Address) TableName() string { @@ -106,7 +105,7 @@ func (ad *Address) TableName() string { func (e1 *Entry) Copy(e2 *Entry) { if e2 != nil { - switch (e2.Type) { + switch e2.Type { case "password": e1.Title = e2.Title e1.User = e2.User @@ -124,21 +123,21 @@ func (e1 *Entry) Copy(e2 *Entry) { e1.ExpiryDate = e2.ExpiryDate e1.Tags = e2.Tags e1.Notes = e2.Notes - e1.Type = e2.Type - // case "identity": - // e1.Title = e2.Title - // e1.Name = e2.Name - // e1.Company = e2.Company - // e1.FirstName = e2.FirstName - // e1.LastName = e2.LastName - // e1.MiddleName = e2.MiddleName - // e1.User = e2.User - // e1.Email = e2.Email - // e1.PhoneNumber = e2.PhoneNumber - // e1.Number = e2.Number - // e1.Notes = e2.Notes - // e1.Tags = e2.Tags - // e1.Type = e2.Type + e1.Type = e2.Type + // case "identity": + // e1.Title = e2.Title + // e1.Name = e2.Name + // e1.Company = e2.Company + // e1.FirstName = e2.FirstName + // e1.LastName = e2.LastName + // e1.MiddleName = e2.MiddleName + // e1.User = e2.User + // e1.Email = e2.Email + // e1.PhoneNumber = e2.PhoneNumber + // e1.Number = e2.Number + // e1.Notes = e2.Notes + // e1.Tags = e2.Tags + // e1.Type = e2.Type } } } @@ -146,566 +145,618 @@ 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 - } + 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) { - db, err := gorm.Open(sqlite.Open(filePath), &gorm.Config{ - Logger: logger.Default.LogMode(logger.Silent), - }) - return err, db + db, err := gorm.Open(sqlite.Open(filePath), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Silent), + }) + return err, db } // Create a new table for Entries in the database func createNewEntry(db *gorm.DB) error { - return db.AutoMigrate(&Entry{}) + return db.AutoMigrate(&Entry{}) } // Create a new table for Extended Entries in the database func createNewExEntry(db *gorm.DB) error { - return db.AutoMigrate(&ExtendedEntry{}) + return db.AutoMigrate(&ExtendedEntry{}) } // Init new database including tables func initNewDatabase(dbPath string) error { - var err error - var db *gorm.DB - var absPath string - - if hasActiveDatabase() { - // Has an active database - encrypt it before creating new one - _, activeDbPath := getActiveDatabase() - absPath, _ = filepath.Abs(dbPath) - - if absPath == activeDbPath { - fmt.Printf("Database already exists and is active - %s\n", dbPath) - return nil - } else { - // TBD - fmt.Printf("Encrytping current database - %s\n", activeDbPath) - encryptDatabase(activeDbPath, nil) - } - } - - if _, err = os.Stat(dbPath); err == nil { - // filePath exists, remove it - os.Remove(dbPath) - } - - err, db = openDatabase(dbPath) - if err != nil { - fmt.Printf("Error creating new database - \"%s\"\n", err.Error()) - return err - } - - err = createNewEntry(db) - if err != nil { - fmt.Printf("Error creating schema - \"%s\"\n", err.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 - absPath, err = filepath.Abs(dbPath) - // Chmod it - os.Chmod(absPath, 0600) - - if err == nil { - fmt.Printf("Updating active db path - %s\n", absPath) - updateActiveDbPath(absPath) - } else { - fmt.Printf("Error - %s\n", err.Error()) - return err - } - - return nil + var err error + var db *gorm.DB + var absPath string + + if hasActiveDatabase() { + // Has an active database - encrypt it before creating new one + _, activeDbPath := getActiveDatabase() + absPath, _ = filepath.Abs(dbPath) + + if absPath == activeDbPath { + fmt.Printf("Database already exists and is active - %s\n", dbPath) + return nil + } else { + // TBD + fmt.Printf("Encrytping current database - %s\n", activeDbPath) + encryptDatabase(activeDbPath, nil) + } + } + + if _, err = os.Stat(dbPath); err == nil { + // filePath exists, remove it + os.Remove(dbPath) + } + + err, db = openDatabase(dbPath) + if err != nil { + fmt.Printf("Error creating new database - \"%s\"\n", err.Error()) + return err + } + + err = createNewEntry(db) + if err != nil { + fmt.Printf("Error creating schema - \"%s\"\n", err.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 + absPath, err = filepath.Abs(dbPath) + // Chmod it + os.Chmod(absPath, 0600) + + if err == nil { + fmt.Printf("Updating active db path - %s\n", absPath) + updateActiveDbPath(absPath) + } else { + fmt.Printf("Error - %s\n", err.Error()) + return err + } + + return nil } // Open currently active database func openActiveDatabase() (error, *gorm.DB) { - var dbPath string - var err error + var dbPath string + var err error - err, dbPath = getActiveDatabase() - if err != nil { - fmt.Printf("Error getting active database path - %s\n", err.Error()) - return err, nil - } + err, dbPath = getActiveDatabase() + if err != nil { + fmt.Printf("Error getting active database path - %s\n", err.Error()) + return err, nil + } - err, db := openDatabase(dbPath) - if err != nil { - fmt.Printf("Error opening active database path - %s: %s\n", dbPath, err.Error()) - return err, nil - } + err, db := openDatabase(dbPath) + if err != nil { + fmt.Printf("Error opening active database path - %s: %s\n", dbPath, err.Error()) + return err, nil + } - return nil, 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 + var count int + var err error - err = createNewExEntry(db) - if err != nil { - fmt.Printf("Error creating schema - \"%s\"\n", err.Error()) - return err - } + err = createNewExEntry(db) + if err != nil { + fmt.Printf("Error creating schema - \"%s\"\n", err.Error()) + return err + } - for _, customEntry := range customEntries { - var exEntry ExtendedEntry + for _, customEntry := range customEntries { + var exEntry ExtendedEntry - exEntry = ExtendedEntry{FieldName: customEntry.fieldName, FieldValue: customEntry.fieldValue, - EntryID: entry.ID} + exEntry = ExtendedEntry{FieldName: customEntry.fieldName, FieldValue: customEntry.fieldValue, + EntryID: entry.ID} - resultEx := db.Create(&exEntry) - if resultEx.Error == nil && resultEx.RowsAffected == 1 { - count += 1 - } - } + 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 + 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 + 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 - } + 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) + db.Where("entry_id = ?", entry.ID).Delete(&customEntries) - for _, customEntry := range updatedEntries { - var exEntry ExtendedEntry + for _, customEntry := range updatedEntries { + var exEntry ExtendedEntry - exEntry = ExtendedEntry{FieldName: customEntry.fieldName, FieldValue: customEntry.fieldValue, - EntryID: entry.ID} + exEntry = ExtendedEntry{FieldName: customEntry.fieldName, FieldValue: customEntry.fieldValue, + EntryID: entry.ID} - resultEx := db.Create(&exEntry) - if resultEx.Error == nil && resultEx.RowsAffected == 1 { - count += 1 - } - } + 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 + 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, 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, Tags: strings.TrimSpace(tags), - Notes: notes} - - err, db = openActiveDatabase() - if err == nil && db != nil { - // 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 - } - } - - return err + 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, Tags: strings.TrimSpace(tags), + Notes: notes} + + err, db = openActiveDatabase() + if err == nil && db != nil { + // 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 + } + } + + return err +} + +func updateDatabaseCardEntry(entry *Entry, cardName, cardNumber, cardHolder, cardClass, + cardCvv, cardPin, cardExpiry, notes, tags string, customEntries []CustomEntry, + flag bool) error { + + var updateMap map[string]interface{} + updateMap = make(map[string]interface{}) + + keyValMap := map[string]string{ + "title": cardName, + "user": cardHolder, + "url": cardNumber, + "password": cardCvv, + "pin": cardPin, + // Issuer has to be the same + "class": cardClass, + "expiry_date": cardExpiry, + "tags": strings.TrimSpace(tags), + "notes": notes, + } + + for key, val := range keyValMap { + if len(val) > 0 { + updateMap[key] = val + } + } + fmt.Printf("%+v\n", updateMap) + + if len(updateMap) == 0 && !flag { + fmt.Printf("Nothing to update\n") + return nil + } + + // Update timestamp also + updateMap["timestamp"] = time.Now() + + err, db := openActiveDatabase() + + if err == nil && db != nil { + result := db.Model(entry).Updates(updateMap) + if result.Error != nil { + return result.Error + } + + if flag { + replaceCustomEntries(db, entry, customEntries) + } + fmt.Println("Updated entry.") + return nil + } + + return err } // Add a new card entry to current database func addNewDatabaseCardEntry(cardName, cardNumber, cardHolder, cardIssuer, cardClass, - cardCvv, cardPin, cardExpiry, notes, tags string, customEntries []CustomEntry) error { - - var entry Entry - var err error - var db *gorm.DB - - entry = Entry{ - Title: cardName, - User: cardHolder, - Url: cardNumber, - Password: cardCvv, - Pin: cardPin, - Issuer: cardIssuer, - Class: cardClass, - ExpiryDate: cardExpiry, - Type: "card", - Tags: strings.TrimSpace(tags), - Notes: notes} - - err, db = openActiveDatabase() - if err == nil && db != nil { - // 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 - } - } - - return err -} + cardCvv, cardPin, cardExpiry, notes, tags string, customEntries []CustomEntry) error { + + var entry Entry + var err error + var db *gorm.DB + + entry = Entry{ + Title: cardName, + User: cardHolder, + Url: cardNumber, + Password: cardCvv, + Pin: cardPin, + Issuer: cardIssuer, + Class: cardClass, + ExpiryDate: cardExpiry, + Type: "card", + Tags: strings.TrimSpace(tags), + Notes: notes} + + err, db = openActiveDatabase() + if err == nil && db != nil { + // 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 + } + } + return err +} // Update current database entry with new values 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, - "tags": tags} - - for key, val := range keyValMap { - if len(val) > 0 { - updateMap[key] = val - } - } - - if len(updateMap) == 0 && !flag { - fmt.Printf("Nothing to update\n") - return nil - } - - // Update timestamp also - updateMap["timestamp"] = time.Now() - - err, db := openActiveDatabase() - - if err == nil && db != nil { - result := db.Model(entry).Updates(updateMap) - if result.Error != nil { - return result.Error - } - - if flag { - replaceCustomEntries(db, entry, customEntries) - } - fmt.Println("Updated entry.") - return nil - } - - return err + 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, + "tags": tags} + + for key, val := range keyValMap { + if len(val) > 0 { + updateMap[key] = val + } + } + + if len(updateMap) == 0 && !flag { + fmt.Printf("Nothing to update\n") + return nil + } + + // Update timestamp also + updateMap["timestamp"] = time.Now() + + err, db := openActiveDatabase() + + if err == nil && db != nil { + result := db.Model(entry).Updates(updateMap) + if result.Error != nil { + return result.Error + } + + if flag { + replaceCustomEntries(db, entry, customEntries) + } + fmt.Println("Updated entry.") + return nil + } + + return err } // Find entry given the id func getEntryById(id int) (error, *Entry) { - var entry Entry - var err error - var db *gorm.DB - - err, db = openActiveDatabase() - if err == nil && db != nil { - result := db.First(&entry, id) - if result.Error == nil { - return nil, &entry - } else { - return result.Error, nil - } - } - - return err, nil + var entry Entry + var err error + var db *gorm.DB + + err, db = openActiveDatabase() + if err == nil && db != nil { + result := db.First(&entry, id) + if result.Error == nil { + return nil, &entry + } else { + return result.Error, nil + } + } + + return err, nil } // Search database for the given string and return all matches func searchDatabaseEntry(term string) (error, []Entry) { - var entries []Entry - var err error - var db *gorm.DB - var searchTerm string + var entries []Entry + var err error + var db *gorm.DB + var searchTerm string + + err, db = openActiveDatabase() + if err == nil && db != nil { + searchTerm = fmt.Sprintf("%%%s%%", term) + // Search on fields title, user, url and notes and tags. + query := db.Where(fmt.Sprintf("title like \"%s\"", searchTerm)) - err, db = openActiveDatabase() - if err == nil && db != nil { - searchTerm = fmt.Sprintf("%%%s%%", term) - // Search on fields title, user, url and notes and tags. - query := db.Where(fmt.Sprintf("title like \"%s\"", searchTerm)) + for _, field := range []string{"user", "url", "notes", "tags"} { + query = query.Or(fmt.Sprintf("%s like \"%s\"", field, searchTerm)) + } - for _, field := range[]string{"user", "url", "notes", "tags"} { - query = query.Or(fmt.Sprintf("%s like \"%s\"", field, searchTerm)) - } - - res := query.Find(&entries) + res := query.Find(&entries) - if res.Error != nil { - return res.Error, nil - } + if res.Error != nil { + return res.Error, nil + } - return nil, entries - } + return nil, entries + } - return err, entries + return err, entries } // Union of two entry arrays func union(entry1 []Entry, entry2 []Entry) []Entry { - m := make(map[int]bool) + m := make(map[int]bool) - for _, item := range entry1 { - m[item.ID] = true - } + for _, item := range entry1 { + m[item.ID] = true + } - for _, item := range entry2 { - if _, ok := m[item.ID]; !ok { - entry1 = append(entry1, item) - } - } + for _, item := range entry2 { + if _, ok := m[item.ID]; !ok { + entry1 = append(entry1, item) + } + } - return entry1 + return entry1 } // Intersection of two entry arrays func intersection(entry1 []Entry, entry2 []Entry) []Entry { - var common []Entry + var common []Entry - m := make(map[int]bool) + m := make(map[int]bool) - for _, item := range entry1 { - m[item.ID] = true - } + for _, item := range entry1 { + m[item.ID] = true + } - for _, item := range entry2 { - if _, ok := m[item.ID]; ok { - common = append(common, item) - } - } + for _, item := range entry2 { + if _, ok := m[item.ID]; ok { + common = append(common, item) + } + } - return common + 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 + 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 { - var err error - var db *gorm.DB + var err error + var db *gorm.DB - err, db = openActiveDatabase() - if err == nil && db != nil { - var exEntries []ExtendedEntry + err, db = openActiveDatabase() + if err == nil && db != nil { + var exEntries []ExtendedEntry - res := db.Delete(entry) - if res.Error != nil { - return res.Error - } + 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 - } - } + // 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 - } + return nil + } - return err + return err } // Clone an entry and return cloned entry func cloneEntry(entry *Entry) (error, *Entry) { - var entryNew Entry - var err error - var db *gorm.DB + var entryNew Entry + var err error + var db *gorm.DB - err, db = openActiveDatabase() - if err == nil && db != nil { - entryNew.Copy(entry) + err, db = openActiveDatabase() + if err == nil && db != nil { + entryNew.Copy(entry) - result := db.Create(&entryNew) - if result.Error == nil && result.RowsAffected == 1 { - fmt.Printf("Cloned to new entry, id: %d.\n", entryNew.ID) - return nil, &entryNew - } else if result.Error != nil { - return result.Error, nil - } - } + result := db.Create(&entryNew) + if result.Error == nil && result.RowsAffected == 1 { + fmt.Printf("Cloned to new entry, id: %d.\n", entryNew.ID) + return nil, &entryNew + } else if result.Error != nil { + return result.Error, nil + } + } - return err, nil + 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 + var err error + var db *gorm.DB - err, db = openActiveDatabase() - if err == nil && db != nil { - for _, exEntry := range exEntries { - var exEntryNew ExtendedEntry + 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 + exEntryNew.Copy(&exEntry) + // Update the ID! + exEntryNew.EntryID = entry.ID - result := db.Create(&exEntryNew) - if result.Error != nil { - return result.Error - } - } - } + result := db.Create(&exEntryNew) + if result.Error != nil { + return result.Error + } + } + } - return err + return err } // Return an iterator over all entries using the given order query keys func iterateEntries(orderKey string, order string) (error, []Entry) { - var err error - var db *gorm.DB - var entries []Entry + var err error + var db *gorm.DB + var entries []Entry - err, db = openActiveDatabase() + err, db = openActiveDatabase() - if err == nil && db != nil { - var rows *sql.Rows + if err == nil && db != nil { + var rows *sql.Rows - rows, err = db.Model(&Entry{}).Order(fmt.Sprintf("%s %s", orderKey, order)).Rows() - for rows.Next() { - var entry Entry + rows, err = db.Model(&Entry{}).Order(fmt.Sprintf("%s %s", orderKey, order)).Rows() + for rows.Next() { + var entry Entry - db.ScanRows(rows, &entry) - entries = append(entries, entry) - } + db.ScanRows(rows, &entry) + entries = append(entries, entry) + } - return nil, entries - } + return nil, entries + } - return err, nil + return err, nil } // Export all entries to string array func entriesToStringArray(skipLongFields bool) (error, [][]string) { - var err error - var db *gorm.DB - var dataArray [][]string + var err error + var db *gorm.DB + var dataArray [][]string - err, db = openActiveDatabase() + err, db = openActiveDatabase() - if err == nil && db != nil { - var rows *sql.Rows - var count int64 + if err == nil && db != nil { + var rows *sql.Rows + var count int64 - db.Model(&Entry{}).Count(&count) + db.Model(&Entry{}).Count(&count) - dataArray = make([][]string, 0, count) + dataArray = make([][]string, 0, count) - rows, err = db.Model(&Entry{}).Order("id asc").Rows() - for rows.Next() { - var entry Entry - var entryData []string + rows, err = db.Model(&Entry{}).Order("id asc").Rows() + for rows.Next() { + var entry Entry + var entryData []string - db.ScanRows(rows, &entry) + db.ScanRows(rows, &entry) - if skipLongFields { - // Skip Notes - entryData = []string{strconv.Itoa(entry.ID), entry.Title, entry.User, entry.Password, entry.Timestamp.Format("2006-06-02 15:04:05")} - } else { - entryData = []string{strconv.Itoa(entry.ID), entry.Title, entry.User, entry.Url, entry.Password, entry.Notes, entry.Timestamp.Format("2006-06-02 15:04:05")} - } + if skipLongFields { + // Skip Notes + entryData = []string{strconv.Itoa(entry.ID), entry.Title, entry.User, entry.Password, entry.Timestamp.Format("2006-06-02 15:04:05")} + } else { + entryData = []string{strconv.Itoa(entry.ID), entry.Title, entry.User, entry.Url, entry.Password, entry.Notes, entry.Timestamp.Format("2006-06-02 15:04:05")} + } - dataArray = append(dataArray, entryData) - } - } + dataArray = append(dataArray, entryData) + } + } - return err, dataArray + 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 + var err error + var db *gorm.DB + var customEntries []ExtendedEntry - err, db = openActiveDatabase() + err, db = openActiveDatabase() - if err == nil && db != nil { - db.Where("entry_id = ?", entry.ID).Find(&customEntries) - } + if err == nil && db != nil { + db.Where("entry_id = ?", entry.ID).Find(&customEntries) + } - return customEntries + return customEntries } diff --git a/export.go b/export.go new file mode 100644 index 0000000..30442b6 --- /dev/null +++ b/export.go @@ -0,0 +1,383 @@ +package main + +import ( + "bufio" + "encoding/csv" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" +) + +// 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 +} + +// 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 + +} + +// 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 + +} + +// 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 + +} + +// 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 "} + + err, dataArray = entriesToStringArray(false) + + 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 + } + + defer fh.Close() + + writer := bufio.NewWriter(fh) + + writer.WriteString("\n") + 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
\n") + + writer.WriteString("\n") + + writer.Flush() + + return nil + +} + +// Export current database to CSV +func exportToCsv(fileName string) error { + + var err error + var dataArray [][]string + var fh *os.File + + err, dataArray = entriesToStringArray(false) + + 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 + } + + writer := csv.NewWriter(fh) + + // 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 + } + } + + writer.Flush() + + 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) + + return nil +} diff --git a/main.go b/main.go index 2f878b5..0078724 100644 --- a/main.go +++ b/main.go @@ -3,10 +3,10 @@ package main import ( - "fmt" - "github.com/pythonhacker/argparse" - "os" - "strings" + "fmt" + "github.com/pythonhacker/argparse" + "os" + "strings" ) const VERSION = 0.4 @@ -21,245 +21,245 @@ type actionFunc func(string) error type actionFunc2 func(string) (error, string) type voidFunc func() error type voidFunc2 func() (error, string) -type settingFunc func(string) +type settingFunc func(string) // Structure to keep the options data type CmdOption struct { - Short string - Long string - Help string - Path string - Default string + Short string + Long string + Help string + Path string + Default string } // Print the program's usage string and exit func printUsage() error { - // getopt.Usage() - os.Exit(0) + // getopt.Usage() + os.Exit(0) - return nil + return nil } // Print the program's version info and exit func printVersionInfo() error { - fmt.Printf("%s version %.2f\n", APP, VERSION) - os.Exit(0) + fmt.Printf("%s version %.2f\n", APP, VERSION) + os.Exit(0) - return nil + return nil } // Command-line wrapper to generateRandomPassword func genPass() (error, string) { - var err error - var passwd string + var err error + var passwd string - err, passwd = generateStrongPassword() + err, passwd = generateStrongPassword() - if err != nil { - fmt.Printf("Error generating password - \"%s\"\n", err.Error()) - return err, "" - } + if err != nil { + fmt.Printf("Error generating password - \"%s\"\n", err.Error()) + return err, "" + } - fmt.Println(passwd) + fmt.Println(passwd) - if settingsRider.CopyPassword { - copyPasswordToClipboard(passwd) - fmt.Println("Password copied to clipboard") - } + if settingsRider.CopyPassword { + copyPasswordToClipboard(passwd) + fmt.Println("Password copied to clipboard") + } - return nil, passwd + return nil, passwd } // // Perform an action by using the command line options map func performAction(optMap map[string]interface{}) { - var flag bool - - boolActionsMap := map[string]voidFunc{ - "add": WrapperMaxKryptVoidFunc(addNewEntry), - "version": printVersionInfo, - "help": printUsage, - "path": showActiveDatabasePath, - "list-all": WrapperMaxKryptVoidFunc(listAllEntries), - "encrypt": encryptActiveDatabase, - } - - stringActionsMap := map[string]actionFunc{ - "edit": WrapperMaxKryptStringFunc(editCurrentEntry), - "init": initNewDatabase, - "list-entry": WrapperMaxKryptStringFunc(listCurrentEntry), - "remove": WrapperMaxKryptStringFunc(removeCurrentEntry), - "clone": WrapperMaxKryptStringFunc(copyCurrentEntry), - "use-db": setActiveDatabasePath, - "export": exportToFile, - "migrate": migrateDatabase, - } - - stringListActionsMap := map[string]actionFunc{ - "find": WrapperMaxKryptStringFunc(findCurrentEntry), - } - - stringActions2Map := map[string]actionFunc2{ - "decrypt": decryptDatabase, - } - - flagsActions2Map := map[string]voidFunc2{ - "genpass": genPass, - } - - flagsActionsMap := map[string]voidFunc{ - "show": setShowPasswords, - "copy": setCopyPasswordToClipboard, - "assume-yes": setAssumeYes, - } - - flagsSettingsMap := map[string]settingFunc{ - "type": setType, - } - - // Flag actions - always done - for key, mappedFunc := range flagsActionsMap { - if *optMap[key].(*bool) { - mappedFunc() - } - } - - // Flag 2 actions - for key, mappedFunc := range flagsActions2Map { - if *optMap[key].(*bool) { - mappedFunc() - flag = true - break - } - } - - // Settings - for key, mappedFunc := range flagsSettingsMap { - if *optMap[key].(*string) != ""{ - var val = *(optMap[key].(*string)) - mappedFunc(val) - } - } - - // One of bool or string actions - for key, mappedFunc := range boolActionsMap { - if *optMap[key].(*bool) { - mappedFunc() - flag = true - break - } - } - - if flag { - return - } - - for key, mappedFunc := range stringActionsMap { - if *optMap[key].(*string) != "" { - - var val = *(optMap[key].(*string)) - mappedFunc(val) - flag = true - break - } - } - - 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 - } - - for key, mappedFunc := range stringActions2Map { - if *optMap[key].(*string) != "" { - var val = *(optMap[key].(*string)) - mappedFunc(val) - break - } - } + var flag bool + + boolActionsMap := map[string]voidFunc{ + "add": WrapperMaxKryptVoidFunc(addNewEntry), + "version": printVersionInfo, + "help": printUsage, + "path": showActiveDatabasePath, + "list-all": WrapperMaxKryptVoidFunc(listAllEntries), + "encrypt": encryptActiveDatabase, + } + + stringActionsMap := map[string]actionFunc{ + "edit": WrapperMaxKryptStringFunc(editCurrentEntry), + "init": initNewDatabase, + "list-entry": WrapperMaxKryptStringFunc(listCurrentEntry), + "remove": WrapperMaxKryptStringFunc(removeCurrentEntry), + "clone": WrapperMaxKryptStringFunc(copyCurrentEntry), + "use-db": setActiveDatabasePath, + "export": exportToFile, + "migrate": migrateDatabase, + } + + stringListActionsMap := map[string]actionFunc{ + "find": WrapperMaxKryptStringFunc(findCurrentEntry), + } + + stringActions2Map := map[string]actionFunc2{ + "decrypt": decryptDatabase, + } + + flagsActions2Map := map[string]voidFunc2{ + "genpass": genPass, + } + + flagsActionsMap := map[string]voidFunc{ + "show": setShowPasswords, + "copy": setCopyPasswordToClipboard, + "assume-yes": setAssumeYes, + } + + flagsSettingsMap := map[string]settingFunc{ + "type": setType, + } + + // Flag actions - always done + for key, mappedFunc := range flagsActionsMap { + if *optMap[key].(*bool) { + mappedFunc() + } + } + + // Flag 2 actions + for key, mappedFunc := range flagsActions2Map { + if *optMap[key].(*bool) { + mappedFunc() + flag = true + break + } + } + + // Settings + for key, mappedFunc := range flagsSettingsMap { + if *optMap[key].(*string) != "" { + var val = *(optMap[key].(*string)) + mappedFunc(val) + } + } + + // One of bool or string actions + for key, mappedFunc := range boolActionsMap { + if *optMap[key].(*bool) { + mappedFunc() + flag = true + break + } + } + + if flag { + return + } + + for key, mappedFunc := range stringActionsMap { + if *optMap[key].(*string) != "" { + + var val = *(optMap[key].(*string)) + mappedFunc(val) + flag = true + break + } + } + + 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 + } + + for key, mappedFunc := range stringActions2Map { + if *optMap[key].(*string) != "" { + var val = *(optMap[key].(*string)) + mappedFunc(val) + break + } + } } func initializeCmdLine(parser *argparse.Parser) map[string]interface{} { - var optMap map[string]interface{} - - optMap = make(map[string]interface{}) - - stringOptions := []CmdOption{ - {"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", "", ""}, - {"t", "type", "Specify type when adding a new entry", "", ""}, - } - - for _, opt := range stringOptions { - 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", "", ""}, - {"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 this help message and exit", "", ""}, - } - - for _, opt := range boolOptions { - optMap[opt.Long] = parser.Flag(string(opt.Short), opt.Long, &argparse.Options{Help: opt.Help}) - } - - return optMap + var optMap map[string]interface{} + + optMap = make(map[string]interface{}) + + stringOptions := []CmdOption{ + {"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", "", ""}, + {"t", "type", "Specify type when adding a new entry", "", ""}, + } + + for _, opt := range stringOptions { + 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", "", ""}, + {"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 this help message and exit", "", ""}, + } + + for _, opt := range boolOptions { + optMap[opt.Long] = parser.Flag(string(opt.Short), opt.Long, &argparse.Options{Help: opt.Help}) + } + + return optMap } // Main routine func main() { - if len(os.Args) == 1 { - os.Args = append(os.Args, "-h") - } + if len(os.Args) == 1 { + os.Args = append(os.Args, "-h") + } - parser := argparse.NewParser("varuh", - "Password manager for the command line for Unix like operating systems", - AUTHOR_INFO, - ) + parser := argparse.NewParser("varuh", + "Password manager for the command line for Unix like operating systems", + AUTHOR_INFO, + ) - optMap := initializeCmdLine(parser) + optMap := initializeCmdLine(parser) - err := parser.Parse(os.Args) + err := parser.Parse(os.Args) - if err != nil { - fmt.Println(parser.Usage(err)) - } + if err != nil { + fmt.Println(parser.Usage(err)) + } - getOrCreateLocalConfig(APP) + getOrCreateLocalConfig(APP) - performAction(optMap) + performAction(optMap) } diff --git a/test/testpgp.go b/test/testpgp.go index 4adb384..844fe03 100644 --- a/test/testpgp.go +++ b/test/testpgp.go @@ -3,26 +3,25 @@ package main import ( - "os" - "os/user" - "fmt" "bytes" + "fmt" + "golang.org/x/crypto/openpgp" "io/ioutil" + "os" + "os/user" "path/filepath" - "golang.org/x/crypto/openpgp" ) - func main() { currUser, _ := user.Current() secretText := "These are the nuclear launch codes - A/B/C/D" path, err := filepath.Abs(filepath.Join(currUser.HomeDir, ".gnupg/pubring.kbx")) fmt.Println(path) - + fh, _ := os.Open(path) defer fh.Close() - + entityList, err := openpgp.ReadArmoredKeyRing(fh) if err != nil { fmt.Println("1") @@ -34,7 +33,7 @@ func main() { _, err = w.Write([]byte(secretText)) if err != nil { - fmt.Println("2") + fmt.Println("2") panic(err) } @@ -44,16 +43,16 @@ func main() { } data, err := ioutil.ReadAll(buf) - if err != nil { - fmt.Println("3") + if err != nil { + fmt.Println("3") panic(err) - } - + } + // encStr := base64.StdEncoding.EncodeToString(bytes) - + err = os.WriteFile("test.gpg", data, 0644) if err != nil { - fmt.Println("4") + fmt.Println("4") panic(err) } } diff --git a/utils.go b/utils.go index 9e4069a..bd51a07 100644 --- a/utils.go +++ b/utils.go @@ -2,52 +2,52 @@ package main import ( - "bufio" - "time" - "regexp" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "github.com/atotto/clipboard" - "github.com/kirsle/configdir" - "golang.org/x/crypto/ssh/terminal" - "io/fs" - "os" - "path/filepath" - "strings" - "strconv" - "github.com/polyglothacker/creditcard" + "bufio" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "github.com/atotto/clipboard" + "github.com/kirsle/configdir" + "github.com/polyglothacker/creditcard" + "golang.org/x/crypto/ssh/terminal" + "io/fs" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" ) const DELIMSIZE int = 69 // Over-ride settings via cmd line type SettingsOverride struct { - ShowPasswords bool - CopyPassword bool - AssumeYes bool - Type string // Type of entity to add + ShowPasswords bool + CopyPassword bool + AssumeYes bool + Type string // Type of entity to add } // Settings structure for local config type Settings struct { - ActiveDB string `json:"active_db"` - Cipher string `json:"cipher"` - AutoEncrypt bool `json:"auto_encrypt"` - KeepEncrypted bool `json:"encrypt_on"` - ShowPasswords bool `json:"visible_passwords"` - ConfigPath string `json:"path"` - // Key to order listings when using -a option - // Valid values are - // 1. timestamp,{desc,asc} - // 2. title,{desc,asc} - // 3. username, {desc,asc} - // 4. id, {desc,asc{ - ListOrder string `json:"list_order"` - Delim string `json:"delimiter"` - Color string `json:"color"` // fg color to print - BgColor string `json:"bgcolor"` // bg color to print + ActiveDB string `json:"active_db"` + Cipher string `json:"cipher"` + AutoEncrypt bool `json:"auto_encrypt"` + KeepEncrypted bool `json:"encrypt_on"` + ShowPasswords bool `json:"visible_passwords"` + ConfigPath string `json:"path"` + // Key to order listings when using -a option + // Valid values are + // 1. timestamp,{desc,asc} + // 2. title,{desc,asc} + // 3. username, {desc,asc} + // 4. id, {desc,asc{ + ListOrder string `json:"list_order"` + Delim string `json:"delimiter"` + Color string `json:"color"` // fg color to print + BgColor string `json:"bgcolor"` // bg color to print } // Global settings override @@ -56,629 +56,631 @@ var settingsRider SettingsOverride // Write settings to disk func writeSettings(settings *Settings, configFile string) error { - fh, err := os.Create(configFile) - if err != nil { - fmt.Printf("Error generating configuration file %s - \"%s\"\n", configFile, err.Error()) - return err - } + fh, err := os.Create(configFile) + if err != nil { + fmt.Printf("Error generating configuration file %s - \"%s\"\n", configFile, err.Error()) + return err + } - defer fh.Close() + defer fh.Close() - encoder := json.NewEncoder(fh) - encoder.SetIndent("", "\t") - err = encoder.Encode(&settings) + encoder := json.NewEncoder(fh) + encoder.SetIndent("", "\t") + err = encoder.Encode(&settings) - return err + return err } // Write updated settings to disk func updateSettings(settings *Settings, configFile string) error { - fh, err := os.OpenFile(configFile, os.O_RDWR, 0644) - if err != nil { - fmt.Printf("Error opening config file %s - \"%s\"\n", configFile, err.Error()) - return err - } + fh, err := os.OpenFile(configFile, os.O_RDWR, 0644) + if err != nil { + fmt.Printf("Error opening config file %s - \"%s\"\n", configFile, err.Error()) + return err + } - defer fh.Close() + defer fh.Close() - encoder := json.NewEncoder(fh) - encoder.SetIndent("", "\t") - err = encoder.Encode(&settings) + encoder := json.NewEncoder(fh) + encoder.SetIndent("", "\t") + err = encoder.Encode(&settings) - if err != nil { - fmt.Printf("Error updating config %s - \"%s\"\n", configFile, err.Error()) - return err - } + if err != nil { + fmt.Printf("Error updating config %s - \"%s\"\n", configFile, err.Error()) + return err + } - return err + return err } // Make the per-user configuration folder and return local settings func getOrCreateLocalConfig(app string) (error, *Settings) { - var settings Settings - var configPath string - var configFile string - var err error - var fh *os.File + var settings Settings + var configPath string + var configFile string + var err error + var fh *os.File - configPath = configdir.LocalConfig(app) - err = configdir.MakePath(configPath) // Ensure it exists. - if err != nil { - return err, nil - } + configPath = configdir.LocalConfig(app) + err = configdir.MakePath(configPath) // Ensure it exists. + if err != nil { + return err, nil + } - configFile = filepath.Join(configPath, "config.json") - // fmt.Printf("Config file, path => %s %s\n", configFile, configPath) + configFile = filepath.Join(configPath, "config.json") + // fmt.Printf("Config file, path => %s %s\n", configFile, configPath) - if _, err = os.Stat(configFile); err == nil { - fh, err = os.Open(configFile) - if err != nil { - return err, nil - } + if _, err = os.Stat(configFile); err == nil { + fh, err = os.Open(configFile) + if err != nil { + return err, nil + } - defer fh.Close() + defer fh.Close() - decoder := json.NewDecoder(fh) - err = decoder.Decode(&settings) - if err != nil { - return err, nil - } + decoder := json.NewDecoder(fh) + err = decoder.Decode(&settings) + if err != nil { + return err, nil + } - } else { - // fmt.Printf("Creating default configuration ...") - settings = Settings{"", "aes", true, true, false, configFile, "id,asc", ">", "default", "bgblack"} + } else { + // fmt.Printf("Creating default configuration ...") + settings = Settings{"", "aes", true, true, false, configFile, "id,asc", ">", "default", "bgblack"} - if err = writeSettings(&settings, configFile); err == nil { - // fmt.Println(" ...done") - } else { - return err, nil - } - } + if err = writeSettings(&settings, configFile); err == nil { + // fmt.Println(" ...done") + } else { + return err, nil + } + } - return nil, &settings + return nil, &settings } // Return if there is an active, decrypted database func hasActiveDatabase() bool { - err, settings := getOrCreateLocalConfig(APP) - if err == nil && settings.ActiveDB != "" { - if _, err := os.Stat(settings.ActiveDB); err == nil { - if _, flag := isFileEncrypted(settings.ActiveDB); !flag { - return true - } - return false - } - } + err, settings := getOrCreateLocalConfig(APP) + if err == nil && settings.ActiveDB != "" { + if _, err := os.Stat(settings.ActiveDB); err == nil { + if _, flag := isFileEncrypted(settings.ActiveDB); !flag { + return true + } + return false + } + } - if err != nil { - fmt.Printf("Error parsing local config - \"%s\"\n", err.Error()) - } + if err != nil { + fmt.Printf("Error parsing local config - \"%s\"\n", err.Error()) + } - return false + return false } // Get the current active database func getActiveDatabase() (error, string) { - err, settings := getOrCreateLocalConfig(APP) - if err == nil && settings.ActiveDB != "" { - if _, err := os.Stat(settings.ActiveDB); err == nil { - return nil, settings.ActiveDB - } - } + err, settings := getOrCreateLocalConfig(APP) + if err == nil && settings.ActiveDB != "" { + if _, err := os.Stat(settings.ActiveDB); err == nil { + return nil, settings.ActiveDB + } + } - if err != nil { - fmt.Printf("Error parsing local config - \"%s\"\n", err.Error()) - } + if err != nil { + fmt.Printf("Error parsing local config - \"%s\"\n", err.Error()) + } - return err, "" + return err, "" } // Update the active db path func updateActiveDbPath(dbPath string) error { - _, settings := getOrCreateLocalConfig(APP) + _, settings := getOrCreateLocalConfig(APP) - if settings != nil { - settings.ActiveDB = dbPath - } + if settings != nil { + settings.ActiveDB = dbPath + } - return updateSettings(settings, settings.ConfigPath) + return updateSettings(settings, settings.ConfigPath) } // Read the password from console without echoing func readPassword() (error, string) { - var passwd []byte - var err error + var passwd []byte + var err error - passwd, err = terminal.ReadPassword(int(os.Stdin.Fd())) - return err, string(passwd) + passwd, err = terminal.ReadPassword(int(os.Stdin.Fd())) + return err, string(passwd) } // Rewrite the contents of the base file (path minus extension) with the new contents func rewriteBaseFile(path string, contents []byte, mode fs.FileMode) (error, string) { - var err error - var origFile string + var err error + var origFile string - origFile = strings.TrimSuffix(path, filepath.Ext(path)) - // Overwrite it - err = os.WriteFile(origFile, contents, 0644) + origFile = strings.TrimSuffix(path, filepath.Ext(path)) + // Overwrite it + err = os.WriteFile(origFile, contents, 0644) - if err == nil { - // Chmod it - os.Chmod(origFile, mode) - } + if err == nil { + // Chmod it + os.Chmod(origFile, mode) + } - return err, origFile + 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 + var err error - // Overwrite it - err = os.WriteFile(path, contents, 0644) + // Overwrite it + err = os.WriteFile(path, contents, 0644) - if err == nil { - // Chmod it - os.Chmod(path, mode) - } + if err == nil { + // Chmod it + os.Chmod(path, mode) + } - return err, path + return err, path } // Get color codes for console colors func getColor(code string) string { - colors := map[string]string{ - "black": "\x1b[30m", - "blue": "\x1B[34m", - "red": "\x1B[31m", - "green": "\x1B[32m", - "yellow": "\x1B[33m", - "magenta": "\x1B[35m", - "cyan": "\x1B[36m", - "white": "\x1B[37m", - - // From https://gist.github.com/abritinthebay/d80eb99b2726c83feb0d97eab95206c4 - // esoteric options - "bright": "\x1b[1m", - "dim": "\x1b[2m", - "underscore": "\x1b[4m", - "blink": "\x1b[5m", - "reverse": "\x1b[7m", - "hidden": "\x1b[8m", - - // background color options - "bgblack": "\x1b[40m", - "bgred": "\x1b[41m", - "bggreen": "\x1b[42m", - "bgyellow": "\x1b[43m", - "bgblue": "\x1b[44m", - "bgmagenta": "\x1b[45m", - "bgcyan": "\x1b[46m", - "bgwhite": "\x1b[47m", - - // reset color code - "reset": "\x1B[0m", - "default": "\x1B[0m", - } - - if color, ok := colors[code]; ok { - return color - } else { - return colors["default"] - } + colors := map[string]string{ + "black": "\x1b[30m", + "blue": "\x1B[34m", + "red": "\x1B[31m", + "green": "\x1B[32m", + "yellow": "\x1B[33m", + "magenta": "\x1B[35m", + "cyan": "\x1B[36m", + "white": "\x1B[37m", + + // From https://gist.github.com/abritinthebay/d80eb99b2726c83feb0d97eab95206c4 + // esoteric options + "bright": "\x1b[1m", + "dim": "\x1b[2m", + "underscore": "\x1b[4m", + "blink": "\x1b[5m", + "reverse": "\x1b[7m", + "hidden": "\x1b[8m", + + // background color options + "bgblack": "\x1b[40m", + "bgred": "\x1b[41m", + "bggreen": "\x1b[42m", + "bgyellow": "\x1b[43m", + "bgblue": "\x1b[44m", + "bgmagenta": "\x1b[45m", + "bgcyan": "\x1b[46m", + "bgwhite": "\x1b[47m", + + // reset color code + "reset": "\x1B[0m", + "default": "\x1B[0m", + } + + if color, ok := colors[code]; ok { + return color + } else { + return colors["default"] + } } // Print the delimiter line for listings func printDelim(delimChar string, color string) { - var delims []string + var delims []string - if color == "underscore" { - // Override delimieter to space - delimChar = " " - } + if color == "underscore" { + // Override delimieter to space + delimChar = " " + } - if len(delimChar) > 1 { - // slice it - take only the first - delimChar = string(delimChar[0]) - } - for i := 0; i < DELIMSIZE; i++ { - delims = append(delims, delimChar) - } + if len(delimChar) > 1 { + // slice it - take only the first + delimChar = string(delimChar[0]) + } + for i := 0; i < DELIMSIZE; i++ { + delims = append(delims, delimChar) + } - fmt.Println(strings.Join(delims, "")) + fmt.Println(strings.Join(delims, "")) } // Prettify credit/debit card numbers func prettifyCardNumber(cardNumber string) string { - // Amex cards are 15 digits - group as 4, 6, 5 - // Any 16 digits - group as 4/4/4/4 - var numbers []string + // Amex cards are 15 digits - group as 4, 6, 5 + // Any 16 digits - group as 4/4/4/4 + var numbers []string - if len(cardNumber) == 15 { - numbers = append(numbers, cardNumber[0:4]) - numbers = append(numbers, cardNumber[4:10]) - numbers = append(numbers, cardNumber[10:15]) - } else if len(cardNumber) == 16 { - numbers = append(numbers, cardNumber[0:4]) - numbers = append(numbers, cardNumber[4:8]) - numbers = append(numbers, cardNumber[8:12]) - numbers = append(numbers, cardNumber[12:16]) - } + if len(cardNumber) == 15 { + numbers = append(numbers, cardNumber[0:4]) + numbers = append(numbers, cardNumber[4:10]) + numbers = append(numbers, cardNumber[10:15]) + } else if len(cardNumber) == 16 { + numbers = append(numbers, cardNumber[0:4]) + numbers = append(numbers, cardNumber[4:8]) + numbers = append(numbers, cardNumber[8:12]) + numbers = append(numbers, cardNumber[12:16]) + } - return strings.Join(numbers, " ") + return strings.Join(numbers, " ") } // Print a card entry to the console -func printCardEntry(entry *Entry, settings* Settings, delim bool) error { - - var customEntries []ExtendedEntry - - 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("ID: %d\n", entry.ID) - fmt.Printf("Card Name: %s\n", entry.Title) - fmt.Printf("Card Holder: %s\n", entry.User) - fmt.Printf("Card Number: %s\n", prettifyCardNumber(entry.Url)) - fmt.Printf("Card Type: %s\n", entry.Class) - - if entry.Issuer != "" { - fmt.Printf("Issuing Bank: %s\n", entry.Issuer) - } - - fmt.Println() - - fmt.Printf("Expiry Date: %s\n", entry.ExpiryDate) - - if settings.ShowPasswords || settingsRider.ShowPasswords { - fmt.Printf("Card CVV: %s\n", entry.Password) - fmt.Printf("Card PIN: %s\n", entry.Pin) - } else { - var asterisks1 []string - var asterisks2 []string - var i int - - for i = 0; i < len(entry.Password); i++ { - asterisks1 = append(asterisks1, "*") - } - fmt.Printf("Card CVV: %s\n", strings.Join(asterisks1, "")) - - for i = 0; i < len(entry.Pin); i++ { - asterisks2 = append(asterisks2, "*") - } - fmt.Printf("Card PIN: %s\n", strings.Join(asterisks2, "")) - } - - if len(entry.Tags) > 0 { - fmt.Printf("\nTags: %s\n", entry.Tags) - } - if len(entry.Notes) > 0 { - 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 - fmt.Printf("%s", getColor("default")) - - return nil +func printCardEntry(entry *Entry, settings *Settings, delim bool) error { + + var customEntries []ExtendedEntry + + 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("[Type: card]\n") + fmt.Printf("ID: %d\n", entry.ID) + fmt.Printf("Card Name: %s\n", entry.Title) + fmt.Printf("Card Holder: %s\n", entry.User) + fmt.Printf("Card Number: %s\n", prettifyCardNumber(entry.Url)) + fmt.Printf("Card Type: %s\n", entry.Class) + + if entry.Issuer != "" { + fmt.Printf("Issuing Bank: %s\n", entry.Issuer) + } + + fmt.Println() + + fmt.Printf("Expiry Date: %s\n", entry.ExpiryDate) + + if settings.ShowPasswords || settingsRider.ShowPasswords { + fmt.Printf("Card CVV: %s\n", entry.Password) + fmt.Printf("Card PIN: %s\n", entry.Pin) + } else { + var asterisks1 []string + var asterisks2 []string + var i int + + for i = 0; i < len(entry.Password); i++ { + asterisks1 = append(asterisks1, "*") + } + fmt.Printf("Card CVV: %s\n", strings.Join(asterisks1, "")) + + for i = 0; i < len(entry.Pin); i++ { + asterisks2 = append(asterisks2, "*") + } + fmt.Printf("Card PIN: %s\n", strings.Join(asterisks2, "")) + } + + if len(entry.Tags) > 0 { + fmt.Printf("\nTags: %s\n", entry.Tags) + } + if len(entry.Notes) > 0 { + 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-01-02 15:04:05")) + + printDelim(settings.Delim, settings.Color) + + // Reset + fmt.Printf("%s", getColor("default")) + + return nil } // Print an entry to the console func printEntry(entry *Entry, delim bool) error { - var err error - var settings *Settings - var customEntries []ExtendedEntry + var err error + var settings *Settings + var customEntries []ExtendedEntry - err, settings = getOrCreateLocalConfig(APP) + err, settings = getOrCreateLocalConfig(APP) - if err != nil { - fmt.Printf("Error parsing config - \"%s\"\n", err.Error()) - return err - } + if err != nil { + fmt.Printf("Error parsing config - \"%s\"\n", err.Error()) + return err + } - if entry.Type == "card" { - return printCardEntry(entry, settings, delim) - } - - fmt.Printf("%s", getColor(strings.ToLower(settings.Color))) - if strings.HasPrefix(settings.BgColor, "bg") { - fmt.Printf("%s", getColor(strings.ToLower(settings.BgColor))) - } + if entry.Type == "card" { + return printCardEntry(entry, settings, delim) + } - if delim { - printDelim(settings.Delim, settings.Color) - } + fmt.Printf("%s", getColor(strings.ToLower(settings.Color))) + if strings.HasPrefix(settings.BgColor, "bg") { + fmt.Printf("%s", getColor(strings.ToLower(settings.BgColor))) + } - fmt.Printf("ID: %d\n", entry.ID) - fmt.Printf("Title: %s\n", entry.Title) - fmt.Printf("User: %s\n", entry.User) - fmt.Printf("URL: %s\n", entry.Url) + if delim { + printDelim(settings.Delim, settings.Color) + } - if settings.ShowPasswords || settingsRider.ShowPasswords { - fmt.Printf("Password: %s\n", entry.Password) - } else { - var asterisks []string + fmt.Printf("[Type: password]\n") + fmt.Printf("ID: %d\n", entry.ID) + fmt.Printf("Title: %s\n", entry.Title) + fmt.Printf("User: %s\n", entry.User) + fmt.Printf("URL: %s\n", entry.Url) - for i := 0; i < len(entry.Password); i++ { - asterisks = append(asterisks, "*") - } - fmt.Printf("Password: %s\n", strings.Join(asterisks, "")) - } + if settings.ShowPasswords || settingsRider.ShowPasswords { + fmt.Printf("Password: %s\n", entry.Password) + } else { + var asterisks []string - 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) + for i := 0; i < len(entry.Password); i++ { + asterisks = append(asterisks, "*") + } + fmt.Printf("Password: %s\n", strings.Join(asterisks, "")) + } - if len(customEntries) > 0 { - for _, customEntry := range customEntries { - fmt.Printf("%s: %s\n", customEntry.FieldName, customEntry.FieldValue) - } - } + 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) - fmt.Printf("Modified: %s\n", entry.Timestamp.Format("2006-06-02 15:04:05")) + if len(customEntries) > 0 { + for _, customEntry := range customEntries { + fmt.Printf("%s: %s\n", customEntry.FieldName, customEntry.FieldValue) + } + } - printDelim(settings.Delim, settings.Color) + fmt.Printf("Modified: %s\n", entry.Timestamp.Format("2006-01-02 15:04:05")) - // Reset - fmt.Printf("%s", getColor("default")) + printDelim(settings.Delim, settings.Color) - return nil + // Reset + fmt.Printf("%s", getColor("default")) + + return nil } // Print an entry to the console with minimal data func printEntryMinimal(entry *Entry, delim bool) error { - var err error - var settings *Settings + var err error + var settings *Settings - err, settings = getOrCreateLocalConfig(APP) + err, settings = getOrCreateLocalConfig(APP) - if err != nil { - fmt.Printf("Error parsing config - \"%s\"\n", err.Error()) - return err - } + 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))) - } + 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) - } + 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")) + 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-01-02 15:04:05")) - printDelim(settings.Delim, settings.Color) + printDelim(settings.Delim, settings.Color) - // Reset - fmt.Printf("%s", getColor("default")) + // Reset + fmt.Printf("%s", getColor("default")) - return nil + return nil } // Read user input and return entered value func readInput(reader *bufio.Reader, prompt string) string { - var input string - fmt.Printf(prompt + ": ") - input, _ = reader.ReadString('\n') + var input string + fmt.Printf(prompt + ": ") + input, _ = reader.ReadString('\n') - return strings.TrimSpace(input) + return strings.TrimSpace(input) } // Check for an active, decrypted database func checkActiveDatabase() error { - if !hasActiveDatabase() { - fmt.Printf("No decrypted active database found.\n") - return errors.New("no active database") - } + if !hasActiveDatabase() { + fmt.Printf("No decrypted active database found.\n") + return errors.New("no active database") + } - return nil + return nil } // Return true if active database is encrypted func isActiveDatabaseEncrypted() bool { - err, settings := getOrCreateLocalConfig(APP) - if err == nil && settings.ActiveDB != "" { - if _, err := os.Stat(settings.ActiveDB); err == nil { - if _, flag := isFileEncrypted(settings.ActiveDB); flag { - return true - } - } - } + err, settings := getOrCreateLocalConfig(APP) + if err == nil && settings.ActiveDB != "" { + if _, err := os.Stat(settings.ActiveDB); err == nil { + if _, flag := isFileEncrypted(settings.ActiveDB); flag { + return true + } + } + } - return false + return false } // Return true if always encrypt is on func isEncryptOn() bool { - _, settings := getOrCreateLocalConfig(APP) - return settings.KeepEncrypted + _, settings := getOrCreateLocalConfig(APP) + return settings.KeepEncrypted } // Combination of above 2 logic plus auto encryption on (a play on CryptOn) func isActiveDatabaseEncryptedAndMaxKryptOn() (bool, string) { - err, settings := getOrCreateLocalConfig(APP) - if err == nil && settings.ActiveDB != "" { - if _, err := os.Stat(settings.ActiveDB); err == nil { - if _, flag := isFileEncrypted(settings.ActiveDB); flag && settings.KeepEncrypted && settings.AutoEncrypt { - return true, settings.ActiveDB - } - } - } + err, settings := getOrCreateLocalConfig(APP) + if err == nil && settings.ActiveDB != "" { + if _, err := os.Stat(settings.ActiveDB); err == nil { + if _, flag := isFileEncrypted(settings.ActiveDB); flag && settings.KeepEncrypted && settings.AutoEncrypt { + return true, settings.ActiveDB + } + } + } - return false, "" + return false, "" } // (Temporarily) enable showing of passwords func setShowPasswords() error { - // fmt.Printf("Setting show passwords to true\n") - settingsRider.ShowPasswords = true - return nil + // fmt.Printf("Setting show passwords to true\n") + settingsRider.ShowPasswords = true + return nil } // Copy the password to clipboard - only for single listings or single search results func setCopyPasswordToClipboard() error { - settingsRider.CopyPassword = true - return nil + settingsRider.CopyPassword = true + return nil } func setAssumeYes() error { - settingsRider.AssumeYes = true - return nil + settingsRider.AssumeYes = true + return nil } func setType(_type string) { - settingsRider.Type = _type + settingsRider.Type = _type } func copyPasswordToClipboard(passwd string) { - clipboard.WriteAll(passwd) + clipboard.WriteAll(passwd) } // Generate a random file name func randomFileName(folder string, suffix string) string { - _, name := generateRandomBytes(16) - return filepath.Join(folder, hex.EncodeToString(name)+suffix) + _, name := generateRandomBytes(16) + return filepath.Join(folder, hex.EncodeToString(name)+suffix) } // Detect card type from card number func detectCardType(cardNum string) (string, error) { - var cardTypeIndex creditcard.CardType - var err error - - card := creditcard.Card{ - Type: "N/A", - Number: cardNum, - ExpiryMonth: 12, - ExpiryYear: 99, - CVV: "999", - } + var cardTypeIndex creditcard.CardType + var err error + + card := creditcard.Card{ + Type: "N/A", + Number: cardNum, + ExpiryMonth: 12, + ExpiryYear: 99, + CVV: "999", + } - cardTypeIndex, err = card.DetermineCardType() - if err != nil { - return "", err - } + cardTypeIndex, err = card.DetermineCardType() + if err != nil { + return "", err + } - return creditcard.CardTypeNames[cardTypeIndex], nil + return creditcard.CardTypeNames[cardTypeIndex], nil } // Validate CVV func validateCvv(cardCvv string, cardClass string) bool { - var matched bool - - // Amex CVV is 4 digits, rest are 3 - if cardClass == "American Express" { - if matched, _ = regexp.Match(`^\d{4}$`, []byte(cardCvv)); matched { - return matched - } - } else { - if matched, _ = regexp.Match(`^\d{3}$`, []byte(cardCvv)); matched { - return matched - } - } + var matched bool + + // Amex CVV is 4 digits, rest are 3 + if cardClass == "American Express" { + if matched, _ = regexp.Match(`^\d{4}$`, []byte(cardCvv)); matched { + return matched + } + } else { + if matched, _ = regexp.Match(`^\d{3}$`, []byte(cardCvv)); matched { + return matched + } + } - return false + return false } func validateCardPin(cardPin string) bool { - // A PIN is 4 digits or more - if matched, _ := regexp.Match(`^\d{4,}$`, []byte(cardPin)); matched { - return matched - } + // A PIN is 4 digits or more + if matched, _ := regexp.Match(`^\d{4,}$`, []byte(cardPin)); matched { + return matched + } - return false + return false } // Verify if the expiry date is in the form mm/dd func checkValidExpiry(expiryDate string) bool { - - pieces := strings.Split(expiryDate, "/") - - if len(pieces) == 2 { - // Sofar, so good - var month int - var year int - var err error - - month, err = strconv.Atoi(pieces[0]) - if err != nil { - fmt.Printf("Error parsing month: %s: \"%s\"\n", month, err.Error()) - return false - } - year, err = strconv.Atoi(pieces[1]) - if err != nil { - fmt.Printf("Error parsing year: %s: \"%s\"\n", year, err.Error()) - return false - } - - // Month should be in range 1 -> 12 - if month < 1 || month > 12 { - fmt.Printf("Error: invalid value for month - %d!\n", month) - return false - } - // Year should be >= current year - currYear, _ := strconv.Atoi(strconv.Itoa(time.Now().Year())[2:]) - if year < currYear { - fmt.Printf("Error: year should be >= %d\n", currYear) - return false - } - - return true - } else { - fmt.Println("Error: invalid input") - return false - } - + + pieces := strings.Split(expiryDate, "/") + + if len(pieces) == 2 { + // Sofar, so good + var month int + var year int + var err error + + month, err = strconv.Atoi(pieces[0]) + if err != nil { + fmt.Printf("Error parsing month: %s: \"%s\"\n", month, err.Error()) + return false + } + year, err = strconv.Atoi(pieces[1]) + if err != nil { + fmt.Printf("Error parsing year: %s: \"%s\"\n", year, err.Error()) + return false + } + + // Month should be in range 1 -> 12 + if month < 1 || month > 12 { + fmt.Printf("Error: invalid value for month - %d!\n", month) + return false + } + // Year should be >= current year + currYear, _ := strconv.Atoi(strconv.Itoa(time.Now().Year())[2:]) + if year < currYear { + fmt.Printf("Error: year should be >= %d\n", currYear) + return false + } + + return true + } else { + fmt.Println("Error: invalid input") + return false + } + } From 55a225b090bccee31ab22840313466bcb9d73f39 Mon Sep 17 00:00:00 2001 From: Anand Date: Sun, 20 Aug 2023 09:43:12 +0530 Subject: [PATCH 11/11] formatting fixes, utility functions --- db.go | 30 +++++++++++++++++------------ utils.go | 58 +++++++++++++++++++++++++++++++++++--------------------- 2 files changed, 54 insertions(+), 34 deletions(-) diff --git a/db.go b/db.go index 1b413cb..37b554b 100644 --- a/db.go +++ b/db.go @@ -360,16 +360,17 @@ func updateDatabaseCardEntry(entry *Entry, cardName, cardNumber, cardHolder, car // Issuer has to be the same "class": cardClass, "expiry_date": cardExpiry, - "tags": strings.TrimSpace(tags), + "tags": tags, "notes": notes, } for key, val := range keyValMap { + val := strings.TrimSpace(val) if len(val) > 0 { updateMap[key] = val } } - fmt.Printf("%+v\n", updateMap) + // fmt.Printf("%+v\n", updateMap) if len(updateMap) == 0 && !flag { fmt.Printf("Nothing to update\n") @@ -405,18 +406,23 @@ func addNewDatabaseCardEntry(cardName, cardNumber, cardHolder, cardIssuer, cardC var err error var db *gorm.DB + fields := MapString([]string{cardName, cardHolder, cardNumber, cardCvv, + cardPin, cardIssuer, cardClass, cardExpiry, tags, notes}, + strings.TrimSpace) + entry = Entry{ - Title: cardName, - User: cardHolder, - Url: cardNumber, - Password: cardCvv, - Pin: cardPin, - Issuer: cardIssuer, - Class: cardClass, - ExpiryDate: cardExpiry, + Title: fields[0], + User: fields[1], + Url: fields[2], + Password: fields[3], + Pin: fields[4], + Issuer: fields[5], + Class: fields[6], + ExpiryDate: fields[7], Type: "card", - Tags: strings.TrimSpace(tags), - Notes: notes} + Tags: fields[8], + Notes: fields[9], + } err, db = openActiveDatabase() if err == nil && db != nil { diff --git a/utils.go b/utils.go index bd51a07..4289065 100644 --- a/utils.go +++ b/utils.go @@ -53,6 +53,26 @@ type Settings struct { // Global settings override var settingsRider SettingsOverride +// Map a function to an array of strings +func MapString(vs []string, f func(string) string) []string { + vsm := make([]string, len(vs)) + for i, v := range vs { + vsm[i] = f(v) + } + return vsm +} + +// Print a secret +func hideSecret(secret string) string { + var stars []string + + for i := 0; i < len(secret); i++ { + stars = append(stars, "*") + } + + return strings.Join(stars, "") +} + // Write settings to disk func writeSettings(settings *Settings, configFile string) error { @@ -307,6 +327,8 @@ func prettifyCardNumber(cardNumber string) string { // Any 16 digits - group as 4/4/4/4 var numbers []string + // Remove spaces in between + cardNumber = strings.Join(strings.Split(cardNumber, " "), "") if len(cardNumber) == 15 { numbers = append(numbers, cardNumber[0:4]) numbers = append(numbers, cardNumber[4:10]) @@ -347,26 +369,26 @@ func printCardEntry(entry *Entry, settings *Settings, delim bool) error { } fmt.Println() - fmt.Printf("Expiry Date: %s\n", entry.ExpiryDate) + passwd := strings.TrimSpace(entry.Password) + pin := strings.TrimSpace(entry.Pin) if settings.ShowPasswords || settingsRider.ShowPasswords { - fmt.Printf("Card CVV: %s\n", entry.Password) - fmt.Printf("Card PIN: %s\n", entry.Pin) - } else { - var asterisks1 []string - var asterisks2 []string - var i int - for i = 0; i < len(entry.Password); i++ { - asterisks1 = append(asterisks1, "*") + if len(passwd) > 0 { + fmt.Printf("Card CVV: %s\n", passwd) + } + if len(pin) > 0 { + fmt.Printf("Card PIN: %s\n", pin) } - fmt.Printf("Card CVV: %s\n", strings.Join(asterisks1, "")) + } else { - for i = 0; i < len(entry.Pin); i++ { - asterisks2 = append(asterisks2, "*") + if len(passwd) > 0 { + fmt.Printf("Card CVV: %s\n", hideSecret(passwd)) + } + if len(pin) > 0 { + fmt.Printf("Card PIN: %s\n", hideSecret(passwd)) } - fmt.Printf("Card PIN: %s\n", strings.Join(asterisks2, "")) } if len(entry.Tags) > 0 { @@ -377,7 +399,6 @@ func printCardEntry(entry *Entry, settings *Settings, delim bool) error { } // Query extended entries customEntries = getExtendedEntries(entry) - if len(customEntries) > 0 { for _, customEntry := range customEntries { fmt.Printf("%s: %s\n", customEntry.FieldName, customEntry.FieldValue) @@ -385,9 +406,7 @@ func printCardEntry(entry *Entry, settings *Settings, delim bool) error { } fmt.Printf("Modified: %s\n", entry.Timestamp.Format("2006-01-02 15:04:05")) - printDelim(settings.Delim, settings.Color) - // Reset fmt.Printf("%s", getColor("default")) @@ -431,12 +450,7 @@ func printEntry(entry *Entry, delim bool) error { if settings.ShowPasswords || settingsRider.ShowPasswords { fmt.Printf("Password: %s\n", entry.Password) } else { - var asterisks []string - - for i := 0; i < len(entry.Password); i++ { - asterisks = append(asterisks, "*") - } - fmt.Printf("Password: %s\n", strings.Join(asterisks, "")) + fmt.Printf("Password: %s\n", hideSecret(entry.Password)) } if len(entry.Tags) > 0 {