├── db └── migration │ ├── 000001_create_table.down.sql │ └── 000001_create_table.up.sql ├── .env.example ├── store ├── store.go ├── db_helpers.go ├── store_test.go └── db_store.go ├── Makefile ├── .gitignore ├── go.mod ├── LICENSE ├── password ├── encrypt.go └── decrypt.go ├── main.go ├── README.md ├── program └── program.go ├── go.sum └── auth ├── auth.go └── first_run.go /db/migration/000001_create_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS info; -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | HOST = localhost 2 | PORT = 5432 3 | DB_USER = postgres 4 | DB_PASSWORD = yourpassword 5 | 6 | # generally ill-advised to change DB_NAME 7 | DB_NAME = passwords 8 | -------------------------------------------------------------------------------- /db/migration/000001_create_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS info ( 2 | id SERIAL PRIMARY KEY, 3 | key VARCHAR(128) NOT NULL, 4 | encrypted_pw VARCHAR(128) NOT NULL); 5 | -------------------------------------------------------------------------------- /store/store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | type Store interface { 4 | SaveCreds([]byte) error 5 | 6 | RetrieveCreds(string, string, []byte) ([]map[string]string, error) 7 | 8 | ViewCreds(string, []byte) error 9 | 10 | EditCreds(string, []byte) error 11 | 12 | DeleteCreds(string, []byte) error 13 | } 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include .env 2 | export 3 | 4 | migrateup: 5 | migrate -path db/migration -database "postgresql://$(DB_USER):$(DB_PASSWORD)@$(HOST):$(PORT)/$(DB_NAME)?sslmode=disable" -verbose up 6 | 7 | migratedown: 8 | migrate -path db/migration -database "postgresql://$(DB_USER):$(DB_PASSWORD)@$(HOST):$(PORT)/$(DB_NAME)?sslmode=disable" -verbose down -all -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | cli_options.txt 18 | .env 19 | master_pw 20 | encrypted_data -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/good-times-ahead/password-manager-go 2 | 3 | go 1.17 4 | 5 | require github.com/lib/pq v1.10.3 6 | 7 | require ( 8 | github.com/DATA-DOG/go-sqlmock v1.5.0 9 | github.com/joho/godotenv v1.4.0 10 | github.com/stretchr/testify v1.7.2 11 | golang.org/x/crypto v0.0.0-20211115234514-b4de73f9ece8 12 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 13 | ) 14 | 15 | require ( 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/kr/text v0.2.0 // indirect 18 | github.com/pmezard/go-difflib v1.0.0 // indirect 19 | github.com/stretchr/objx v0.1.0 // indirect 20 | golang.org/x/sys v0.0.0-20220317061510-51cd9980dadf // indirect 21 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 22 | gopkg.in/yaml.v3 v3.0.1 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Dhruv 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /password/encrypt.go: -------------------------------------------------------------------------------- 1 | package password 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/rand" 7 | "encoding/base64" 8 | "errors" 9 | "io" 10 | ) 11 | 12 | // Encrypt the plain-text password 13 | func Encrypt(encryptionKey []byte, plainText string) (string, error) { 14 | 15 | password := []byte(plainText) 16 | 17 | generatedCipher, err := aes.NewCipher(encryptionKey) 18 | 19 | if err != nil { 20 | return "", errors.New("error generating cipher") 21 | } 22 | 23 | // GCM is a mode of operation used on block ciphers 24 | gcm, err := cipher.NewGCM(generatedCipher) 25 | 26 | if err != nil { 27 | return "", errors.New("error generating GCM") 28 | } 29 | 30 | nonce := make([]byte, gcm.NonceSize()) 31 | 32 | //populates our byte array with a cryptographically secure sequence 33 | if _, err := io.ReadFull(rand.Reader, nonce); err != nil { 34 | return "", errors.New("error generating secured-sequence") 35 | } 36 | 37 | encryptedPassword := gcm.Seal(nonce, nonce, password, nil) 38 | 39 | // convert the slice of encrypted bytes into a base64 encrypted string to store in the database 40 | b64Password := base64.StdEncoding.EncodeToString(encryptedPassword) 41 | 42 | return b64Password, nil 43 | } 44 | -------------------------------------------------------------------------------- /password/decrypt.go: -------------------------------------------------------------------------------- 1 | package password 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "encoding/base64" 7 | "errors" 8 | "fmt" 9 | ) 10 | 11 | // Decrypt the base64 encoded password 12 | func Decrypt(base64Password string, encryptionKey []byte) (string, error) { 13 | 14 | // decrypting the base64 password string to retrieve our AES-encrypted password 15 | encryptedPassword, err := base64.StdEncoding.DecodeString(base64Password) 16 | 17 | if err != nil { 18 | return "", errors.New("error decoding base64 encrypted password string") 19 | } 20 | 21 | // generate a new cipher using our 32 byte long key 22 | generatedCipher, err := aes.NewCipher(encryptionKey) 23 | 24 | if err != nil { 25 | fmt.Println(err) 26 | return "", errors.New("error generating cipher") 27 | } 28 | 29 | gcm, err := cipher.NewGCM(generatedCipher) 30 | 31 | if err != nil { 32 | return "", errors.New("error generating GCM") 33 | } 34 | 35 | nonce, ciphertext := encryptedPassword[:gcm.NonceSize()], encryptedPassword[gcm.NonceSize():] 36 | 37 | password, err := gcm.Open(nil, nonce, ciphertext, nil) 38 | 39 | if err != nil { 40 | fmt.Println(err) 41 | return "", errors.New("error attempting to decrypt AES-encrypted password") 42 | } 43 | 44 | return string(password), nil 45 | 46 | } 47 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/good-times-ahead/password-manager-go/auth" 8 | "github.com/good-times-ahead/password-manager-go/program" 9 | "github.com/good-times-ahead/password-manager-go/store" 10 | "github.com/joho/godotenv" 11 | _ "github.com/lib/pq" 12 | ) 13 | 14 | func main() { 15 | 16 | // Path to hashed master password file, encrypted data and SQL file 17 | pwFilePath := "./master_pw" 18 | encInfoPath := "./encrypted_data" 19 | sqlFilePath := "./setup.sql" 20 | 21 | if err := godotenv.Load(); err != nil { 22 | log.Fatal( 23 | fmt.Errorf("error reading from .env file: %s", err), 24 | ) 25 | } 26 | 27 | dbStore, err := store.NewDBStore() 28 | 29 | if err != nil { 30 | log.Fatal( 31 | fmt.Errorf("error initializing DBStore: %s", err), 32 | ) 33 | } 34 | 35 | // Get a new struct instance 36 | cli := program.New(dbStore) 37 | 38 | // Init runs all initial checks and also sets up the database connection through store.DBStore 39 | err = cli.Init(pwFilePath, encInfoPath, sqlFilePath) 40 | 41 | if err != nil { 42 | log.Fatal(err) 43 | } 44 | 45 | // This phase is only run after ensuring that encryption key, table and master password have been generated 46 | encryptionKey, err := auth.Run(encInfoPath, pwFilePath) 47 | 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | 52 | if err := cli.Prompt(encryptionKey); err != nil { 53 | log.Fatal(err) 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /store/db_helpers.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "syscall" 9 | 10 | "golang.org/x/term" 11 | ) 12 | 13 | // PrintEntries outputs data fetched from queries 14 | func printEntries(acctList []map[string]string) { 15 | 16 | for _, usrInfo := range acctList { 17 | 18 | response := fmt.Sprintf("ID No: %s, Key: %s, Password: %s", usrInfo["id"], usrInfo["key"], usrInfo["password"]) 19 | 20 | fmt.Println(response) 21 | fmt.Println() // empty line to clean up the output 22 | } 23 | } 24 | 25 | // GetInput receives user input in a streamlined fashion. 26 | func GetInput(argument string) string { 27 | 28 | // Emulate a while loop to receive user input and ensure its' validity 29 | reader := bufio.NewReader(os.Stdin) 30 | 31 | isEmpty := true 32 | 33 | for isEmpty { 34 | 35 | fmt.Print(argument) 36 | 37 | usrInput, err := reader.ReadString('\n') 38 | 39 | // We want to keep re-iterating over the loop so we can leave the error as is(I think) 40 | if err != nil { 41 | fmt.Println("Invalid input or method!") 42 | } 43 | 44 | switch len(usrInput) { 45 | 46 | case 1: 47 | fmt.Printf("Empty input!\n\n") 48 | default: 49 | fmt.Println() 50 | return strings.TrimSpace(usrInput) 51 | 52 | } 53 | 54 | } 55 | 56 | return "" 57 | 58 | } 59 | 60 | // GetPassInput receives user's input securely, 61 | // it makes use of the syscall library to achieve this 62 | func GetPassInput(argument string) []byte { 63 | 64 | // Emulate a while loop to receive user input and ensure its' validity 65 | isEmpty := true 66 | 67 | for isEmpty { 68 | 69 | fmt.Print(argument) 70 | 71 | usrInput, err := term.ReadPassword(int(syscall.Stdin)) 72 | 73 | if err != nil { 74 | fmt.Println("Invalid input or method!") 75 | } 76 | 77 | switch len(usrInput) { 78 | 79 | case 0: 80 | fmt.Printf("Empty input!\n\n") 81 | default: 82 | return usrInput 83 | 84 | } 85 | 86 | } 87 | 88 | return nil 89 | 90 | } 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | ### `NOTE: The application should NOT be used to store sensitive data of any kind in real life. ` 3 | 4 | This is my first project in Go, a password manager application. A humble attempt at execution of an idea I've had for some time now. The application can save, view, edit and delete credentials. You can add multiple credentials for a single website. A simplistic textual interface is used to get things done. 5 | 6 | # How Does It Work 7 | When you first start the application you are prompted to make a master password. The app has four main functions: 8 | 9 | 1) Adding credentials to the database 10 | 2) Displaying saved credentials for a particular website 11 | 3) Modifying saved credentials 12 | 4) Deleting saved credentials 13 | 14 | # Security 15 | The master password the user creates is hashed and saved to disk. The user is prompted to re-enter the master password each time they open the application and their entered text is compared to the saved hash of the password. The user is let through if the entry matches the hash. 16 | 17 | User credentials are kept in the table "info", with the password being secured by an encryption key. 18 | 19 | The encryption key is 32 bytes long generated using the GO library's "crypto/rand" package. It is then sealed using a 24 byte long nonce and master password hash. The sealed output is finally saved to disk, ready for future use. 20 | The encryption key's purpose is to encrypt and subsequently decrypt passwords being saved to the database. 21 | 22 | # Setup 23 | We will be using PostgreSQL for the database so please ensure that you have it installed before proceeding. 24 | 25 | Create a .env file and set the environment variables using the ".env.example" file included in the project. If you decide that you do not want the databases' name to be "passwords", change it to the name of your choice opposite the "DB_NAME" variable. 26 | 27 | Once done with the .env file, go ahead and create a database for the application. The table will be created automatically alongside the master password and encryption key on application first run. 28 | 29 | Finally, open your terminal and use the command in the project directory: ```go run main.go``` 30 | -------------------------------------------------------------------------------- /program/program.go: -------------------------------------------------------------------------------- 1 | package program 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | 9 | "github.com/good-times-ahead/password-manager-go/auth" 10 | "github.com/good-times-ahead/password-manager-go/store" 11 | ) 12 | 13 | type Program struct { 14 | store store.Store 15 | } 16 | 17 | func New(dbStore *store.DBStore) *Program { 18 | return &Program{store: dbStore} 19 | } 20 | 21 | // Init injects the store interface into Program 22 | func (p *Program) Init(pwFilePath, encInfoPath, sqlFilePath string) error { 23 | 24 | // running necessary checks and the like 25 | checkEncData := auth.CheckEncryptedData(encInfoPath) 26 | 27 | if !checkEncData { 28 | 29 | // running the drop table migration first to ensure consistency 30 | migrateDown := exec.Command("make", "migratedown") 31 | err := migrateDown.Run() 32 | if err != nil { 33 | return err 34 | } 35 | 36 | // now running the migration to create the table 37 | migrateUp := exec.Command("make", "migrateup") 38 | err = migrateUp.Run() 39 | if err != nil { 40 | return err 41 | } 42 | 43 | if err := auth.FirstRun(encInfoPath, pwFilePath); err != nil { 44 | return err 45 | } 46 | 47 | } 48 | 49 | return nil 50 | 51 | } 52 | 53 | func (p *Program) Prompt(encryptionKey []byte) error { 54 | 55 | if p.store == nil { 56 | return errors.New("Store not initialized") 57 | } 58 | 59 | appPersist := true 60 | 61 | for appPersist { 62 | 63 | for appPersist { 64 | mainMsg := `Hello, what would you like to do? 65 | 1. Save a password to the DB 66 | 2. View a saved password 67 | 3. Edit a saved password 68 | 4. Delete a saved password 69 | 0: Exit the application: ` 70 | 71 | usrInput := store.GetInput(mainMsg) 72 | 73 | if err := p.controller(usrInput, encryptionKey); err != nil { 74 | return err 75 | } 76 | // adding new lines to keep the interface clean and readable 77 | fmt.Println() 78 | 79 | } 80 | 81 | } 82 | 83 | return nil 84 | } 85 | 86 | func (p *Program) controller(usrInput string, encryptionKey []byte) error { 87 | 88 | switch usrInput { 89 | 90 | case "1": 91 | return p.store.SaveCreds(encryptionKey) 92 | 93 | case "2": 94 | askForKey := "Enter the key to retrieve accounts for: " 95 | key := store.GetInput(askForKey) 96 | 97 | return p.store.ViewCreds(key, encryptionKey) 98 | 99 | case "3": 100 | askForKey := "Enter the key to edit credentials for: " 101 | key := store.GetInput(askForKey) 102 | 103 | return p.store.EditCreds(key, encryptionKey) 104 | 105 | case "4": 106 | askForKey := "Enter the website to delete credentials for: " 107 | key := store.GetInput(askForKey) 108 | 109 | return p.store.DeleteCreds(key, encryptionKey) 110 | 111 | case "0": 112 | os.Exit(0) 113 | 114 | default: 115 | fmt.Println("Invalid input!") 116 | 117 | } 118 | 119 | return nil 120 | 121 | } 122 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= 2 | github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= 3 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= 8 | github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 9 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 10 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 11 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 12 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 13 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 14 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 15 | github.com/lib/pq v1.10.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg= 16 | github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 17 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 18 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 19 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 20 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 21 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 22 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 23 | github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= 24 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 25 | golang.org/x/crypto v0.0.0-20211115234514-b4de73f9ece8 h1:5QRxNnVsaJP6NAse0UdkRgL3zHMvCRRkrDVLNdNpdy4= 26 | golang.org/x/crypto v0.0.0-20211115234514-b4de73f9ece8/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 27 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 28 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 29 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 30 | golang.org/x/sys v0.0.0-20220317061510-51cd9980dadf h1:Fm4IcnUL803i92qDlmB0obyHmosDrxZWxJL3gIeNqOw= 31 | golang.org/x/sys v0.0.0-20220317061510-51cd9980dadf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 32 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 33 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= 34 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 35 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 36 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 37 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 38 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 39 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 40 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 41 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 42 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 43 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 44 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 45 | -------------------------------------------------------------------------------- /auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "os" 9 | 10 | "github.com/good-times-ahead/password-manager-go/store" 11 | "golang.org/x/crypto/argon2" 12 | "golang.org/x/crypto/nacl/secretbox" 13 | ) 14 | 15 | func CheckEncryptedData(encInfoPath string) bool { 16 | 17 | // Check for file's existance, returns an error if unable to open 18 | checkFile, err := os.Open(encInfoPath) 19 | 20 | if err != nil { 21 | return false 22 | } 23 | 24 | defer checkFile.Close() 25 | 26 | readBytes, err := checkFile.Read(make([]byte, 64)) 27 | 28 | // If not, return false since either the file has been tampered with 29 | if err != nil || readBytes == 0 { 30 | return false 31 | } 32 | 33 | return true 34 | 35 | } 36 | 37 | // Check whether master password file exists already 38 | func CheckMasterPassword(pwFilePath string) bool { 39 | 40 | // Check for file's existence, returns an error if unable to open 41 | checkFile, err := os.Open(pwFilePath) 42 | 43 | if err != nil { 44 | return false 45 | } 46 | 47 | defer checkFile.Close() 48 | 49 | // Read file to confirm there are 32 bytes of the hashed master password 50 | readBytes, err := checkFile.Read(make([]byte, 32)) 51 | 52 | // If not, return false since either the file has been tampered with 53 | if err != nil || readBytes == 0 { 54 | return false 55 | } 56 | 57 | return true 58 | 59 | } 60 | 61 | func LoadEncryptedInfo(encInfoPath string) ([][]byte, error) { 62 | 63 | file, err := os.Open(encInfoPath) 64 | 65 | if err != nil { 66 | return nil, fmt.Errorf("error reading encrypted data: %s", err) 67 | } 68 | 69 | // defer file.Close() 70 | 71 | scanner := bufio.NewScanner(file) 72 | 73 | count := 0 74 | 75 | var salt, sealedKey []byte 76 | 77 | // order: 1. salt, 2. sealedkey 78 | for scanner.Scan() { 79 | 80 | count += 1 81 | 82 | switch count { 83 | 84 | case 1: 85 | salt = []byte(scanner.Text()) 86 | case 2: 87 | sealedKey = []byte(scanner.Text()) 88 | default: 89 | 90 | } 91 | 92 | } 93 | 94 | values := [][]byte{salt, sealedKey} 95 | 96 | // returning file.Close() since the function will return an error. Deferring it means ignoring any errors that might occur, which can lead to data loss while the program continues functioning under the assumption that everything went well. 97 | return values, file.Close() 98 | } 99 | 100 | // Take users' master password input and compare it to the stored hash, allowing access if match 101 | func AuthorizeUser(pwFilePath string, values [][]byte) error { 102 | 103 | // Load master password file 104 | hashedPassword, err := os.ReadFile(pwFilePath) 105 | 106 | if err != nil { 107 | return fmt.Errorf("error reading master password: %s", err) 108 | } 109 | 110 | // infinite loop till user enters the correct value 111 | for { 112 | 113 | // Take user input 114 | prompt := "\nEnter the Master Password:" 115 | usrPassword := store.GetPassInput(prompt) 116 | // convert the received slice of bytes to string 117 | usrInput := string(usrPassword) 118 | 119 | compare := argon2.IDKey([]byte(usrInput), values[0], 1, 64*1024, 4, 32) 120 | 121 | // if the stored hash matches the produced/current hash, allow the user to go through 122 | if bytes.Equal(compare, hashedPassword) { 123 | break 124 | } 125 | 126 | // adding new lines to keep the interface clean and readable 127 | fmt.Printf("\nThe passwords do not match! Try again.\n\n") 128 | 129 | } 130 | 131 | return nil 132 | 133 | } 134 | 135 | func UnsealEncryptionKey(pwFilePath string, values [][]byte) ([]byte, error) { 136 | 137 | // declare needed encryption variables from the slice 138 | var sealedKey []byte 139 | var nonce [24]byte 140 | 141 | for index, value := range values { 142 | 143 | if index == 1 { 144 | sealedKey = value 145 | } 146 | 147 | } 148 | 149 | // the nonce is stored in the first 24 bytes 150 | copy(nonce[:], sealedKey[:24]) 151 | 152 | // read master password hash to use as the key for secretbox.Open 153 | hashedPassword, err := os.ReadFile(pwFilePath) 154 | 155 | if err != nil { 156 | return nil, fmt.Errorf("error when trying to read master password: %s", err) 157 | } 158 | 159 | // the main data is stored from the 25th byte onwards 160 | encKey, ok := secretbox.Open(nil, sealedKey[24:], &nonce, (*[32]byte)(hashedPassword)) 161 | 162 | if !ok { 163 | return nil, errors.New("error while unsealing encryption key") 164 | } 165 | 166 | return encKey, nil 167 | } 168 | 169 | func Run(encInfoPath, pwFilePath string) ([]byte, error) { 170 | 171 | // load encrypted data to use when dealing with credentials later 172 | encData, err := LoadEncryptedInfo(encInfoPath) 173 | if err != nil { 174 | return nil, err 175 | } 176 | 177 | err = AuthorizeUser(pwFilePath, encData) 178 | if err != nil { 179 | return nil, err 180 | } 181 | 182 | // now, to unseal the encryption key 183 | encryptionKey, err := UnsealEncryptionKey(pwFilePath, encData) 184 | 185 | if err != nil { 186 | return nil, err 187 | } 188 | 189 | return encryptionKey, nil 190 | 191 | } 192 | -------------------------------------------------------------------------------- /auth/first_run.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/good-times-ahead/password-manager-go/store" 10 | "golang.org/x/crypto/argon2" 11 | "golang.org/x/crypto/nacl/secretbox" 12 | ) 13 | 14 | // This file contains functions that will be executed if it is the users' run of the app 15 | 16 | // Ask the user to enter their master password 17 | func GetMasterPassword() string { 18 | 19 | fmt.Println(` 20 | Hello and welcome to the Password Manager GO application. If you are seeing this message then this must be your first time using the application. 21 | To get started, you must first create a master password which will be used to authenticate you each time you run the application. 22 | Set a secure password and remember it since there will be no way to recover it! 23 | `) 24 | 25 | // get users' desired master password in plain text, will be hashed later 26 | // use App packages' GetPassInput function 27 | var usrInput string 28 | 29 | for { 30 | 31 | prompt := "Enter desired Master Password (should contain a combination of atleast 1 lowercase, 1 uppercase letter and a number;minimum length: 8 characters):" 32 | 33 | usrPassword := store.GetPassInput(prompt) 34 | usrInput = string(usrPassword) 35 | 36 | checkInput := CheckPasswordStrength(usrInput) 37 | 38 | if !checkInput { 39 | 40 | fmt.Println("Doesn't match required parameters! Please try again.") 41 | fmt.Println("") 42 | 43 | } else { 44 | break 45 | } 46 | 47 | } 48 | 49 | return usrInput 50 | 51 | } 52 | 53 | // CheckPasswordStrength checks the strength of the user-entered input for master password 54 | func CheckPasswordStrength(usrInput string) bool { 55 | 56 | lowercase := "abcdefghijklmnopqrstuvwxyz" 57 | uppercase := "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 58 | nums := "0123456789" 59 | 60 | switch true { 61 | 62 | case !strings.ContainsAny(lowercase, usrInput): 63 | break 64 | case !strings.ContainsAny(uppercase, usrInput): 65 | break 66 | case !strings.ContainsAny(nums, usrInput): 67 | break 68 | case len(usrInput) < 8: 69 | break 70 | default: 71 | return true 72 | 73 | } 74 | 75 | return false 76 | } 77 | 78 | // Argon2 is considered better than bcrypt for securing passwords 79 | func HashMasterPassword(usrInput, pwFilePath string) ([]byte, []byte, error) { 80 | 81 | // generate 32 bytes salt 82 | salt := make([]byte, 32) 83 | 84 | // generate 32 byte long random salt 85 | // rand.Read writes the output to the given slice 86 | if _, err := rand.Read(salt); err != nil { 87 | return nil, nil, err 88 | } 89 | 90 | // generate hashed password using argon2 91 | hashedMasterPassword := argon2.IDKey([]byte(usrInput), salt, 1, 64*1024, 4, 32) 92 | 93 | // returning salt as well as the hashed master password. 94 | // the salt will be written to disk as well 95 | return salt, hashedMasterPassword, nil 96 | } 97 | 98 | func SaveMasterPassword(pwFilePath string, hashedMasterPassword []byte) error { 99 | 100 | // Creates file if doesn't exist; permission code "4" means the file is read-only 101 | err := os.WriteFile(pwFilePath, hashedMasterPassword, 0444) 102 | 103 | if err != nil { 104 | return err 105 | } 106 | 107 | fmt.Println("\n\nSuccessfully saved master password to file! Now you will be asked to enter it each time you run the program.") 108 | 109 | return nil 110 | 111 | } 112 | 113 | func NewEncryptionKey() ([]byte, error) { 114 | 115 | // generate a new encryption key 116 | encryptionKey := make([]byte, 32) 117 | 118 | if _, err := rand.Read(encryptionKey); err != nil { 119 | return nil, err 120 | } 121 | 122 | return encryptionKey, nil 123 | } 124 | 125 | // Seal encryption key for an added layer of protection 126 | func SealEncryptionKey(hashedPassword []byte, encryptionKey []byte) ([]byte, error) { 127 | 128 | // make slice to store rand's generated output 129 | var nonce [24]byte 130 | 131 | // generate nonce to use with secretbox.Seal 132 | if _, err := rand.Read(nonce[:]); err != nil { 133 | return nil, err 134 | } 135 | 136 | // use secretbox to seal the encryption key 137 | sealedEncKey := secretbox.Seal(nonce[:], encryptionKey, &nonce, (*[32]byte)(hashedPassword)) 138 | 139 | return sealedEncKey, nil 140 | 141 | } 142 | 143 | // Save the salt and sealed encryption key to disk 144 | func SaveEncryptionData(encInfoPath string, values [][]byte) error { 145 | 146 | file, err := os.OpenFile(encInfoPath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0444) 147 | 148 | if err != nil { 149 | return err 150 | } 151 | 152 | for _, value := range values { 153 | 154 | if _, err := file.Write(append(value, '\n')); err != nil { 155 | return err 156 | } 157 | 158 | } 159 | 160 | return file.Close() 161 | 162 | } 163 | 164 | func FirstRun(encInfoPath, pwFilePath string) error { 165 | 166 | // generate encryption key at the very start 167 | encKey, err := NewEncryptionKey() 168 | 169 | if err != nil { 170 | return err 171 | } 172 | 173 | // if password doesn't exist yet 174 | usrInput := GetMasterPassword() 175 | 176 | // Hash the master password 177 | salt, hashedPassword, err := HashMasterPassword(usrInput, pwFilePath) 178 | 179 | if err != nil { 180 | return err 181 | } 182 | 183 | // Save the master password to disk 184 | savePasswordErr := SaveMasterPassword(pwFilePath, hashedPassword) 185 | 186 | if savePasswordErr != nil { 187 | return savePasswordErr 188 | } 189 | 190 | // after master password has been generated properly, we will seal our encryption key 191 | sealedEncKey, err := SealEncryptionKey(hashedPassword, encKey) 192 | 193 | // combine sealed key and salt into a slice 194 | values := [][]byte{salt, sealedEncKey} 195 | 196 | saveDataErr := SaveEncryptionData(encInfoPath, values) 197 | 198 | if saveDataErr != nil { 199 | return err 200 | } 201 | 202 | return nil 203 | 204 | } 205 | -------------------------------------------------------------------------------- /store/store_test.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | "regexp" 8 | "testing" 9 | 10 | "github.com/DATA-DOG/go-sqlmock" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func setupForTests() (*sql.DB, sqlmock.Sqlmock, Store) { 15 | // generates a mock *sql.DB connection to use for testing 16 | db, mock, err := sqlmock.New() 17 | 18 | if err != nil { 19 | log.Fatalf("An error encountered when generating new mock connection: %s", err) 20 | } 21 | 22 | var store Store 23 | store = newMockStore(db) 24 | 25 | return db, mock, store 26 | } 27 | 28 | func TestRetrieveCreds(t *testing.T) { 29 | 30 | query := "SELECT * FROM info WHERE key ILIKE ? ORDER BY id ASC" 31 | 32 | db, mock, ms := setupForTests() 33 | defer db.Close() 34 | 35 | mockRows := sqlmock.NewRows([]string{"id", "key", "encrypted_pw"}).AddRow("1", "reddit", "test123").AddRow("2", "Reddit", "testing") 36 | 37 | mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM info WHERE key ILIKE ? ORDER BY id ASC`)).WithArgs("%red%").WillReturnRows(mockRows) 38 | 39 | var want []map[string]string 40 | 41 | want = append(want, map[string]string{"id": "1", "key": "reddit", "password": "test123"}, map[string]string{"id": "2", "key": "Reddit", "password": "testing"}) 42 | 43 | credList, err := ms.RetrieveCreds(query, "red", []byte("test")) 44 | 45 | if err != nil { 46 | t.Log(err) 47 | } 48 | 49 | if err := mock.ExpectationsWereMet(); err != nil { 50 | t.Errorf("expectations unmet: %s", err) 51 | } 52 | 53 | // check if credList matches want 54 | assert.Equal(t, want, credList, "Function returns incorrect values!") 55 | 56 | } 57 | 58 | // TestViewCreds will fail. Since ViewCreds method uses RetrieveCreds as it's 59 | // base, we can test a failing condition here instead of copy-pasting 60 | func TestViewCreds(t *testing.T) { 61 | 62 | db, mock, ms := setupForTests() 63 | defer db.Close() 64 | 65 | // mockRows := sqlmock.NewRows([]string{"id", "key", "encrypted_pw"}).AddRow("1", "reddit", "test123").AddRow("2", "Reddit", "testing") 66 | 67 | mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM info WHERE key ILIKE ? ORDER BY id ASC`)).WithArgs("%spotify%").WillReturnError(fmt.Errorf("no accounts found")) 68 | 69 | _ = ms.ViewCreds("spotify", []byte("test")) 70 | 71 | if err := mock.ExpectationsWereMet(); err != nil { 72 | t.Errorf("unmet expectatations: %s", err) 73 | } 74 | 75 | // the query returns the error that we had specified, means our test passed 76 | 77 | } 78 | 79 | func TestEditCreds(t *testing.T) { 80 | 81 | db, mock, ms := setupForTests() 82 | defer db.Close() 83 | 84 | mockRows := mock.NewRows([]string{"id", "key", "encrypted_pw"}). 85 | AddRow("10", "spotify", "randomPassword") 86 | 87 | mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM info`)).WithArgs("%spot%").WillReturnRows(mockRows) 88 | 89 | usrInfo := map[string]string{ 90 | "id": "10", "key": "spotify", "encrypted_pw": "randomPassword", 91 | } 92 | 93 | mock.ExpectExec(regexp.QuoteMeta(`UPDATE info`)). 94 | WithArgs(usrInfo["key"], usrInfo["encrypted_pw"], usrInfo["id"]). 95 | WillReturnResult(sqlmock.NewResult(0, 1)) 96 | 97 | if err := ms.EditCreds("spot", []byte("test")); err != nil { 98 | t.Log(err) 99 | } 100 | 101 | if err := mock.ExpectationsWereMet(); err != nil { 102 | t.Errorf("expectations unmet: %s", err) 103 | } 104 | 105 | } 106 | 107 | func TestDeleteCreds(t *testing.T) { 108 | 109 | db, mock, ms := setupForTests() 110 | defer db.Close() 111 | 112 | mockRows := mock.NewRows([]string{"id", "key", "encrypted_pw"}). 113 | AddRow("10", "spotify", "randomPassword") 114 | 115 | mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM info`)).WithArgs("%spot%").WillReturnRows(mockRows) 116 | 117 | // sample data for testing 118 | usrInfo := map[string]string{ 119 | "id": "10", "key": "spotify", "encrypted_pw": "randomPassword", 120 | } 121 | 122 | mock.ExpectExec(`DELETE FROM info`).WithArgs(usrInfo["id"]).WillReturnResult(sqlmock.NewResult(1, 1)) 123 | 124 | if err := ms.DeleteCreds("spot", []byte("test")); err != nil { 125 | t.Log(err) 126 | } 127 | 128 | if err := mock.ExpectationsWereMet(); err != nil { 129 | t.Errorf("unmet expectations: %s", err) 130 | } 131 | 132 | } 133 | 134 | // the mockStore struct doesn't need anything except the Conn field 135 | type mockStore struct { 136 | Conn *sql.DB 137 | } 138 | 139 | // The passed Conn argument is our mock DB connection 140 | func newMockStore(Conn *sql.DB) *mockStore { 141 | return &mockStore{ 142 | Conn: Conn, 143 | } 144 | } 145 | 146 | func (ms *mockStore) SaveCreds(encryptionKey []byte) error { 147 | 148 | usrInfo := make(map[string]string, 2) 149 | usrInfo["key"], usrInfo["password"] = "reddit", "test123" 150 | 151 | query := "INSERT INTO info (key, encrypted_pw) VALUES (?, ?)" 152 | 153 | if _, err := ms.Conn.Exec(query, 154 | usrInfo["key"], usrInfo["password"]); err != nil { 155 | return err 156 | } 157 | 158 | return nil 159 | 160 | } 161 | 162 | func (ms *mockStore) RetrieveCreds(query string, key string, encryptionKey []byte) ([]map[string]string, error) { 163 | 164 | rows, err := ms.Conn.Query(query, "%"+key+"%") 165 | 166 | if err != nil { 167 | return nil, err 168 | } 169 | 170 | var credList []map[string]string 171 | 172 | for rows.Next() { 173 | 174 | usrInfo := make(map[string]string, 3) 175 | var id, key, password string 176 | 177 | err := rows.Scan(&id, &key, &password) 178 | 179 | if err != nil { 180 | return nil, fmt.Errorf("error reading query data: %s", err) 181 | } 182 | 183 | usrInfo["id"], usrInfo["key"], usrInfo["password"] = id, key, password 184 | 185 | credList = append(credList, usrInfo) 186 | } 187 | 188 | return credList, nil 189 | 190 | } 191 | 192 | func (ms *mockStore) ViewCreds(key string, encryptionKey []byte) error { 193 | // this is basically the same as RetrieveCreds since it just uses 194 | // that to get its work done. RetrieveCreds serves as the common 195 | // helper function for all DB queries. 196 | query := "SELECT * FROM info WHERE key ILIKE ? ORDER BY id ASC" 197 | 198 | credList, err := ms.RetrieveCreds(query, "spotify", encryptionKey) 199 | 200 | if err != nil || len(credList) == 0 { 201 | return err 202 | } 203 | 204 | return nil 205 | 206 | } 207 | 208 | func (ms *mockStore) EditCreds(key string, encryptionKey []byte) error { 209 | 210 | query := "SELECT * FROM info WHERE key ILIKE ? ORDER BY id ASC" 211 | 212 | _, err := ms.Conn.Query(query, "%"+key+"%") 213 | 214 | if err != nil { 215 | return fmt.Errorf("error executing query: %s", err) 216 | } 217 | 218 | usrInfo := map[string]string{ 219 | "id": "10", "key": "spotify", "encrypted_pw": "randomPassword", 220 | } 221 | 222 | editQuery := "UPDATE info SET key=?, encrypted_pw=? WHERE id=?" 223 | 224 | _, err = ms.Conn.Exec(editQuery, usrInfo["key"], usrInfo["encrypted_pw"], usrInfo["id"]) 225 | 226 | if err != nil { 227 | return fmt.Errorf("error executing update query: %s", editQuery) 228 | } 229 | 230 | return nil 231 | } 232 | 233 | func (ms *mockStore) DeleteCreds(key string, encryptionKey []byte) error { 234 | 235 | query := "SELECT * FROM info WHERE key ILIKE ? ORDER BY id ASC" 236 | 237 | _, err := ms.Conn.Query(query, "%"+key+"%") 238 | 239 | if err != nil { 240 | return fmt.Errorf("error executing query: %s", err) 241 | } 242 | 243 | usrInfo := map[string]string{ 244 | "id": "10", "key": "spotify", "encrypted_pw": "randomPassword", 245 | } 246 | 247 | deleteQuery := "DELETE FROM info WHERE id=?" 248 | 249 | _, err = ms.Conn.Exec(deleteQuery, usrInfo["id"]) 250 | 251 | if err != nil { 252 | return fmt.Errorf("error executing deletion query: %s", err) 253 | } 254 | 255 | return nil 256 | } 257 | -------------------------------------------------------------------------------- /store/db_store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "bufio" 5 | "database/sql" 6 | "fmt" 7 | "os" 8 | "strings" 9 | 10 | "github.com/good-times-ahead/password-manager-go/password" 11 | ) 12 | 13 | type DBStore struct { 14 | Conn *sql.DB 15 | host string 16 | port string 17 | user string 18 | password string 19 | dbname string 20 | } 21 | 22 | func NewDBStore() (*DBStore, error) { 23 | // Initializing the DBStore struct that is our interface as well 24 | db := DBStore{ 25 | host: os.Getenv("HOST"), 26 | port: os.Getenv("PORT"), 27 | user: os.Getenv("DB_USER"), 28 | password: os.Getenv("DB_PASSWORD"), 29 | dbname: os.Getenv("DB_NAME"), 30 | } 31 | 32 | // Prepare Postgres connection parameters 33 | psqlInfo := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", db.host, db.port, db.user, db.password, db.dbname) 34 | 35 | conn, err := sql.Open("postgres", psqlInfo) 36 | 37 | if err != nil { 38 | return &DBStore{}, fmt.Errorf("error connecting to postgres database: %s", err) 39 | } 40 | 41 | // Ping to confirm whether connection works 42 | if err := conn.Ping(); err != nil { 43 | return &DBStore{}, fmt.Errorf("error pinging postgres database: %s", err) 44 | 45 | } 46 | 47 | fmt.Println("Connected to the database successfully!") 48 | 49 | // The struct field now carries the network connection 50 | db.Conn = conn 51 | 52 | return &db, nil 53 | 54 | } 55 | 56 | // SaveCreds saves the user-entered credentials to the database 57 | func (db *DBStore) SaveCreds(encryptionKey []byte) error { 58 | // define needed prompts 59 | promptKey := "Enter key: " 60 | promptPassword := "Enter your password(it will be encrypted before saving): " 61 | 62 | // initialize the variable to save the creds to 63 | usrInfo := make(map[string]string, 3) 64 | 65 | // write user input to the map/dict 66 | usrInfo["key"] = GetInput(promptKey) 67 | usrInfo["password"] = string(GetPassInput(promptPassword)) 68 | 69 | // encrypt the plaintext password 70 | encryptedPassword, err := password.Encrypt(encryptionKey, usrInfo["password"]) 71 | 72 | if err != nil { 73 | return err 74 | } 75 | 76 | // save the credentials to the database 77 | if err := db.InsertIntoDB(encryptedPassword, usrInfo); err != nil { 78 | return err 79 | } 80 | 81 | return nil 82 | 83 | } 84 | 85 | // RetrieveCreds retrieves userdata from the database given respective query 86 | func (db *DBStore) RetrieveCreds(query, key string, encryptionKey []byte) ([]map[string]string, error) { 87 | 88 | // implementing ILIKE search using the 2 "%" signs 89 | rows, err := db.Conn.Query(query, "%"+key+"%") 90 | 91 | if err != nil { 92 | return nil, fmt.Errorf("error executing query: %s", err) 93 | } 94 | 95 | // prepare a slice of maps to store retrieved credentials 96 | var credList []map[string]string 97 | 98 | for rows.Next() { 99 | // each fetched row gets its own "usrInfo" map of length 3 100 | usrInfo := make(map[string]string, 3) 101 | var id, key, base64Password string 102 | 103 | // write scanned values using the variable's pointers 104 | err := rows.Scan(&id, &key, &base64Password) 105 | 106 | if err != nil { 107 | return nil, fmt.Errorf("error reading query data: %s", err) 108 | 109 | } 110 | 111 | // now, to decrypt the b64 string 112 | password, err := password.Decrypt(base64Password, encryptionKey) 113 | 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | // finally, we populate the map we created at the beginning of the loop 119 | usrInfo["id"], usrInfo["key"], usrInfo["password"] = id, key, password 120 | 121 | // Append the map to the slice of maps 122 | credList = append(credList, usrInfo) 123 | } 124 | 125 | return credList, nil 126 | } 127 | 128 | func (db *DBStore) ViewCreds(key string, encryptionKey []byte) error { 129 | // get all accounts associated with the website 130 | query := "SELECT * FROM info WHERE key ILIKE $1 ORDER BY id ASC;" 131 | 132 | credList, err := db.RetrieveCreds(query, key, encryptionKey) 133 | 134 | if err != nil { 135 | return err 136 | } 137 | 138 | // no such key in db if slice is empty 139 | if len(credList) == 0 { 140 | fmt.Println("Nothing found for that search entry!") 141 | } else { 142 | printEntries(credList) 143 | } 144 | 145 | return nil 146 | 147 | } 148 | 149 | func (db *DBStore) EditCreds(key string, encryptionKey []byte) error { 150 | 151 | query := "SELECT * FROM info WHERE key ILIKE $1 ORDER BY id ASC;" 152 | 153 | // retrieve the saved account list first 154 | credList, err := db.RetrieveCreds(query, key, encryptionKey) 155 | 156 | if err != nil { 157 | return err 158 | } 159 | 160 | // if no accounts found, 161 | if len(credList) == 0 { 162 | fmt.Println("No accounts found for that key!") 163 | return nil 164 | } else { 165 | printEntries(credList) 166 | } 167 | 168 | selectID := false 169 | usrInput := "" 170 | selection := make(map[string]string, 3) 171 | 172 | for !selectID { 173 | 174 | // Get users' input to find the entry they want to modify 175 | msg := "Enter the ID No. of the entry you want to modify: " 176 | 177 | usrInput = GetInput(msg) 178 | 179 | for _, entry := range credList { 180 | 181 | if entry["id"] == usrInput { 182 | selectID = true 183 | selection = entry 184 | break 185 | } 186 | } 187 | 188 | if !selectID { 189 | fmt.Println("Entered ID outside range!") 190 | } 191 | 192 | } 193 | 194 | // now, we have the users' choice of entry, allow them to edit whatever field they want 195 | fmt.Println("Your current 'key' entry is: ", selection["key"]) 196 | 197 | fmt.Println("Enter new key (leave field blank if no changes): ") 198 | 199 | // using bufio since GetInput doesn't allow empty input 200 | reader := bufio.NewReader(os.Stdin) 201 | 202 | newKey, err := reader.ReadString('\n') 203 | 204 | if err != nil { 205 | return err 206 | } 207 | 208 | // trim away whitespace left over by reader 209 | newKey = strings.TrimSpace(newKey) 210 | 211 | // update the map if newKey has been modified else let it be 212 | if newKey != "" { 213 | selection["key"] = newKey 214 | } 215 | 216 | fmt.Println("Your current password is: ", selection["password"]) 217 | 218 | newPassPrompt := "Enter new password: " 219 | 220 | newPassword := string(GetPassInput(newPassPrompt)) 221 | 222 | // prepare to encrypt password 223 | b64Password, err := password.Encrypt(encryptionKey, newPassword) 224 | 225 | if err != nil { 226 | return err 227 | } 228 | 229 | // now, update the selection dict/map and send it to the database 230 | selection["password"] = b64Password 231 | 232 | updateQuery := "UPDATE info SET key = $1, encrypted_pw = $2 WHERE id= $3" 233 | 234 | _, err = db.Conn.Exec(updateQuery, selection["key"], selection["password"], selection["id"]) 235 | 236 | if err != nil { 237 | return fmt.Errorf("error updating credentials: %s", err) 238 | } 239 | 240 | fmt.Println("Updated your credentials successfully!") 241 | 242 | return nil 243 | } 244 | 245 | func (db *DBStore) DeleteCreds(key string, encryptionKey []byte) error { 246 | 247 | query := "SELECT * FROM info WHERE key ILIKE $1 ORDER BY id ASC;" 248 | 249 | // retrieve list of matching credentials 250 | credList, err := db.RetrieveCreds(query, key, encryptionKey) 251 | 252 | if err != nil { 253 | return err 254 | } 255 | 256 | if len(credList) == 0 { 257 | return nil 258 | } else { 259 | printEntries(credList) 260 | } 261 | 262 | selectID := false 263 | var usrInput string 264 | selection := make(map[string]string, 3) 265 | 266 | for !selectID { 267 | 268 | // Get users' input to find the entry they want to delete 269 | msg := "Enter the ID No. of the entry you want to delete: " 270 | 271 | usrInput = GetInput(msg) 272 | 273 | for _, entry := range credList { 274 | 275 | if entry["id"] == usrInput { 276 | selectID = true 277 | selection = entry 278 | break 279 | } 280 | } 281 | 282 | if !selectID { 283 | fmt.Println("Entered ID outside range!") 284 | } 285 | 286 | } 287 | 288 | deletionQuery := "DELETE FROM info WHERE ID=$1;" 289 | 290 | _, err = db.Conn.Exec(deletionQuery, selection["id"]) 291 | 292 | if err != nil { 293 | return fmt.Errorf("error deleting entry: %s", err) 294 | } 295 | 296 | fmt.Println("Successfully deleted selected entry!") 297 | 298 | return nil 299 | 300 | } 301 | 302 | // Now adding functions that shall help us insert new and update exisitng data in the table 303 | 304 | func (db *DBStore) InsertIntoDB(encryptedPassword string, creds map[string]string) error { 305 | 306 | query := "INSERT INTO info (key, encrypted_pw) VALUES ($1, $2)" 307 | 308 | _, err := db.Conn.Exec(query, creds["key"], encryptedPassword) 309 | 310 | if err != nil { 311 | return fmt.Errorf("unable to insert into DB: %s", err) 312 | } 313 | 314 | fmt.Println("Saved your credentials to the database!") 315 | fmt.Println() 316 | 317 | return nil 318 | 319 | } 320 | --------------------------------------------------------------------------------