diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..d25d263 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,20 @@ +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: | + cd ${{ github.workspace }} && make diff --git a/actions.go b/actions.go index 09c94ca..7b024b4 100644 --- a/actions.go +++ b/actions.go @@ -3,12 +3,10 @@ package main import ( "bufio" - "encoding/csv" "errors" "fmt" "gorm.io/gorm" "os" - "os/exec" "os/signal" "path/filepath" "strconv" @@ -221,6 +219,84 @@ func setActiveDatabasePath(dbPath string) error { } } +// 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 +} + // Text menu driven function to add a new entry func addNewEntry() error { @@ -237,10 +313,13 @@ func addNewEntry() error { 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 } @@ -366,6 +445,69 @@ func addCustomFields(reader *bufio.Reader) []CustomEntry { 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 { @@ -391,6 +533,10 @@ func editCurrentEntry(idString string) error { return err } + if entry.Type == "card" { + return editCurrentCardEntry(entry) + } + reader := bufio.NewReader(os.Stdin) fmt.Printf("Current Title: %s\n", entry.Title) @@ -805,10 +951,10 @@ func migrateDatabase(dbPath string) error { if err, flag = isFileEncrypted(dbPath); flag { err, passwd = decryptDatabase(dbPath) - } - - if err != nil { - return err + if err != nil { + fmt.Printf("Error decrypting - %s: %s\n", dbPath, err.Error()) + return err + } } err, db = openDatabase(dbPath) @@ -842,374 +988,3 @@ func migrateDatabase(dbPath string) error { 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 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/db.go b/db.go index fa2f75a..37b554b 100644 --- a/db.go +++ b/db.go @@ -16,14 +16,47 @@ 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"` + 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 { @@ -45,15 +78,67 @@ 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.User = e2.User // card holder name + e1.Issuer = e2.Issuer + e1.Url = e2.Url + e1.Password = e2.Password + 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 + } } } @@ -259,6 +344,105 @@ func addNewDatabaseEntry(title, userName, url, passwd, tags string, 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": tags, + "notes": notes, + } + + for key, val := range keyValMap { + val := strings.TrimSpace(val) + 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 + + fields := MapString([]string{cardName, cardHolder, cardNumber, cardCvv, + cardPin, cardIssuer, cardClass, cardExpiry, tags, notes}, + strings.TrimSpace) + + entry = Entry{ + 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: fields[8], + Notes: fields[9], + } + + 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 { @@ -337,17 +521,14 @@ func searchDatabaseEntry(term string) (error, []Entry) { err, db = openActiveDatabase() if err == nil && db != nil { - var conditions []string - var condition string - 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 ?") + // 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)) } - condition = strings.Join(conditions, " OR ") - query := db.Where(condition, searchTerm, searchTerm, searchTerm, searchTerm) res := query.Find(&entries) if res.Error != nil { 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/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..0078724 100644 --- a/main.go +++ b/main.go @@ -21,6 +21,7 @@ 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 { @@ -112,6 +113,10 @@ func performAction(optMap map[string]interface{}) { "assume-yes": setAssumeYes, } + flagsSettingsMap := map[string]settingFunc{ + "type": setType, + } + // Flag actions - always done for key, mappedFunc := range flagsActionsMap { if *optMap[key].(*bool) { @@ -128,6 +133,14 @@ func performAction(optMap map[string]interface{}) { } } + // 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) { @@ -192,6 +205,7 @@ func initializeCmdLine(parser *argparse.Parser) map[string]interface{} { {"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 { 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 8c57d62..4289065 100644 --- a/utils.go +++ b/utils.go @@ -9,11 +9,15 @@ import ( "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 @@ -23,6 +27,7 @@ type SettingsOverride struct { ShowPasswords bool CopyPassword bool AssumeYes bool + Type string // Type of entity to add } // Settings structure for local config @@ -48,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 { @@ -295,6 +320,100 @@ 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 + + // 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]) + 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 { + + 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) + + passwd := strings.TrimSpace(entry.Password) + pin := strings.TrimSpace(entry.Pin) + if settings.ShowPasswords || settingsRider.ShowPasswords { + + if len(passwd) > 0 { + fmt.Printf("Card CVV: %s\n", passwd) + } + if len(pin) > 0 { + fmt.Printf("Card PIN: %s\n", pin) + } + } else { + + if len(passwd) > 0 { + fmt.Printf("Card CVV: %s\n", hideSecret(passwd)) + } + if len(pin) > 0 { + fmt.Printf("Card PIN: %s\n", hideSecret(passwd)) + } + } + + 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 { @@ -309,6 +428,10 @@ func printEntry(entry *Entry, delim bool) 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))) @@ -318,6 +441,7 @@ func printEntry(entry *Entry, delim bool) error { printDelim(settings.Delim, settings.Color) } + 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) @@ -326,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 { @@ -349,7 +468,7 @@ func printEntry(entry *Entry, delim bool) error { } } - fmt.Printf("Modified: %s\n", entry.Timestamp.Format("2006-06-02 15:04:05")) + fmt.Printf("Modified: %s\n", entry.Timestamp.Format("2006-01-02 15:04:05")) printDelim(settings.Delim, settings.Color) @@ -385,7 +504,7 @@ func printEntryMinimal(entry *Entry, delim bool) error { 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("Modified: %s\n", entry.Timestamp.Format("2006-01-02 15:04:05")) printDelim(settings.Delim, settings.Color) @@ -472,6 +591,10 @@ func setAssumeYes() error { return nil } +func setType(_type string) { + settingsRider.Type = _type +} + func copyPasswordToClipboard(passwd string) { clipboard.WriteAll(passwd) } @@ -482,3 +605,96 @@ func randomFileName(folder string, suffix string) string { _, 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 + } + +}