Concise GoLang — Part 2

Vinay
21 min readJul 28, 2020

--

In this series, you will find a concise, hands-on approach to learning the Go language.

In Part 1, we saw the basics of installing Go compiler, running Go programs and theGo module system.

In Part 2, we will develop a password manager program and learn about several language features and packages from standard library.

The password manager allows you to store user names and passwords of accounts. Each account has a name, user name and password. The manager provides a way to save, retrieve the accounts

In the terminal, change to your workspace directory and create a ‘pwmanager’ directory within that.

cd /Users/vinay/concisego
mkdir pwmanager
cd pwmanager

Create a module

go mod init antosara.com/pwmanager

Add the local resolver for our module to go.mod

module antosara.com/pwmanagerreplace antosara.com/pwmanager => ../pwmanagergo 1.14

Create Database

The first functionality we build is creation of database. The manager allows creation of accounts and stores them in a database. We need a utility (library) to help with these operations of reading and writing to this database.

The database format we choose here is CSV file. GoLang has a library to read and write to CSV files: https://golang.org/pkg/encoding/csv/. It’s basically a format where each record is comma separated strings in a line.

Create a directory “util” under pwmanager directory.

mkdir util
cd util

Create the file database.go. We add all the database related methods in this file.

package utilimport (
"os"
"encoding/csv"
)
  • The package name is the same as directory of the file
  • The imported libraries are part of GoLang standard libraries
  • “os” package provides operating system functionality (read/write file etc)
  • “encoding/csv” package helps dealing with CSV functionality

Each account in our database(a CSV file) goes into a line in the format: account name, user name, password. We could represent this as an array of 3 strings. But there is a better way. Go has ‘struct’ type that is a collection of fields. Below we define a struct called Account.

type Account struct {
AccountName string
UserName string
Password string
}

And our database is essentially an array of accounts. Let us also define a struct for that.

type Database struct {
Accounts []Account
}

Accounts above is actually what Go calls “slice”. Arrays in Go have fixed length. A slice is a dynamic view of an array that Go manages behind the scenes. Here, we do not know the size yet. The notation “[] type” defines a slice of the given type. We mostly work with slices in this series.

Notice that the fields start with upper case. This is required to make these fields available outside this package.

Now, we need a way to read and write these accounts from a file. For this project we have decided to use CSV format. But later you may choose to write to JSON or just plain bytes. The same structures above can be represented is several formats on the disk. If we are only interested in reads/writes, it makes sense to use an abstraction for these read/write operations. Go provides the concept of interface. An interface is a set of method signatures. A method is a function that is associated with a type. In other languages, you have classes and methods in the class that operate on the data encapsulated by the class. It’s similar in concept. Go doesn’t have classes, but provides a way of having functions bound to a type which become methods. A method has access to the fields of the type to which it is bound to. The method signature is slightly different from a function signature which we’ll see later. Let’s define an interface for read/write operations. Later, we also demonstrate the usage of interface type.

type DatabaseAccessor interface {
Write(string, *Account) error
Read(string) error
}

Notice the second argument to the Write() method. It’s pointer to Account type. A pointer holds the memory address of a value. If we define without a pointer, Go passes a copy of the Account value which is not necessary. We just need a reference to the original value to read the Account fields.

Now we have a data structure and operations defined. Note that we haven’t implemented any of concrete operations on the data. We now implement for CSV representation of data.

type CSVDatabase Database

We defined a CSVDatabase as a kind of Database. This is to do a specific read/write (accessor) implementation of database in CSV format.

func (db *CSVDatabase) Write(db_file string, account *Account) error {
file, err := os.OpenFile(db_file, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0755)
if err != nil {
return err
}

writer := csv.NewWriter(file)
data := []string { account.AccountName, account.UserName, account.Password }
if err := writer.Write(data); err !=nil {
file.Close()
return err
}

writer.Flush()
file.Close()
if err := writer.Error(); err != nil {
return err
}
return nil
}

Above looks like function. It is, but did you notice the slight difference? It’s the thing in the parentheses following func keyword. It’s called receiver. This is what differentiates a method from a function. This function is now bound to CSVDatabase type.

  • The method Write takes the name of database file (as db_file) and a pointer to Account value. It returns error if any. ‘error’ is a keyword and used in method syntax here to return error value.
  • os.OpenFile() returns the file handle and any error in case of failure. Note the options that we want to open the file as read-write, create if necessary and append if file exists. We write new records to the end of the file in a new line
  • We always check for errors, because if we cannot write to a file, the method should return the error back to caller immediately
  • csv.NewWriter() is the csv library method that provides the “writer” object to write to the given file.
  • We extract the fields of the provided account value into a string slice “data”. This is because CSV library needs it in that format.
  • Writer.Write() method writes the slice of strings in CSV format
  • Flush() writes any buffered data (used for write optimization) to file
  • We close the file and return any error in the process or nil.
func (db *CSVDatabase) Read(db_file string) error {
file, err := os.Open(db_file)
if err != nil {
return err
}
reader := csv.NewReader(file)
data, err := reader.ReadAll()
if err != nil {
return err
}
db.Accounts = make([]Account, len(data))

for i, d := range data {
db.Accounts[i] = Account{d[0], d[1], d[2]}
}
file.Close()
return nil
}
  • The method Read takes the name of database file and returns any error during read
  • If we can’t read the file, we return empty data and error
  • csv.NewReader() creates a CSV reader wrapping the file.
  • Reader.ReadAll() reads the data into 2-d array where each row contains the fields for each account. It also returns any error in reading.
  • Notice how we get access to the accounts in CSVDatabase struct.
  • make() is a Go built-in function that initializes the given type. Here we use it to create a slice of Accounts of length equal to the number of CSV records we found and assign to db.accounts
  • We then fill the values of the Accounts that we read from database file

Now that we got the basic methods, how do we test if the code is correct? Go has a testing package that aids in this. The convention is to define tests for the function in a file, in this case “database”, in a separate file with suffix “_test”, in this case “database_test”

Create a file “database_test.go” in util which also has database.go.

package utilimport (
"os"
"testing"
)
  • Note the import of package “testing”
func TestRead(t *testing.T) {
var dbAccessor DatabaseAccessor = &CSVDatabase {[]Account {}}
err := dbAccessor.Read("test_db1.csv")

if(err == nil) {
t.Errorf("Expected error not thrown")
}
}
  • TestRead function tests that Read fails when the database file is not available.
  • Note the first line in the function. We created a CSVDatabase with empty data and assigned a pointer to that (The “&” generates a pointer to CSVDatabase) to dbAccessor variable of type DatabaseAccessor. Recall that DatabaseAccessor is an interface. In this test, the actual implementation is immaterial. We are only interested in read functionality. Any implementation should throw an error when file is unavailable. So, we program to the interface.
  • Note the second line where we make a method call. Unlike a function we call the method with a reference to the recipient.
  • We expect an error from Read(). If it is nil, the Read() method did not behave as expected, so we use Errorf function in testing package to mark the test as failure.
func TestWrite(t *testing.T) {
os.Remove("test_db.csv")
newAcc := Account {"acc","user","pw"}
db := CSVDatabase {[]Account {}}
err := db.Write("test_db.csv", &newAcc)
if err != nil {
t.Errorf("Expected write did not happen")
t.Log(err)
}
err = db.Read("test_db.csv")
if err != nil {
t.Errorf("Expected read did not happen")
t.Log(err)
}
acc_len := len(db.Accounts)
if acc_len != 1 {
t.Errorf("Expected accounts length: 1, Got %d", acc_len)
}
os.Remove("test_db.csv")
}
  • TestWrite method tests that we are able to write to the database. We first delete any existing file defensively.
  • We create a new Database with empty data.
  • We call the Write() method with the file name and a test account. We then use the Read() method to read the count of records in the database. The test fails if the number of accounts is a value other than 1.

To run the test(and any tests in the package in general), we run the following:

go test

Main Program

We now build a basic program to interact with the user. It will allow creation, list, deletion operations on accounts.

In pwmanager directory, create a file manager.go with the following code.

package mainimport (
"os"
"fmt"
"bufio"
"antosara.com/pwmanager/util"
"strings"
)
  • This is the main program where the main() function resides. So, the package is “main”
  • Import the packages we are going to use. Note how we import the functions in our own util package and other standard Go packages
const DefaultDbFile = "my_pwds.csv"
  • The default name of the database file is “my_pwds.csv”.
  • “const” declares that a constant value that cannot be changed
func readTrimmed(reader *bufio.Reader) (string, error) {
str, err := reader.ReadString('\n')
return strings.TrimSpace(str), err
}
  • readTrimmed() is a utility function that reads a string from the given reader and returns the string read and error if any
  • The argument “reader” is a pointer to a bufio.Reader which is part of standard Go. We will have a reader created before we call this function. So we use a pointer instead of a copy
  • Reader.ReadString(‘\n’) reads the string from input until end-of-line which allows us to collect input when user presses enter key
  • We trim the spaces around the string entered and return the value
func promptFor(prompt string, reader *bufio.Reader)
(val string, err error) {
fmt.Print(" ", prompt, ": ")
val, err = readTrimmed(reader)
if err != nil {
return "", nil
}
return
}
  • promptFor() is another utility function taking arguments for the text to prompt the user and the reader to read from
  • It prompts the user and returns the trimmed value entered by user
  • Notice the return statement. ‘val’ and ‘err’ are named return values. The naked return statement will automatically return the named return values

Both the above functions are not public. Hence, their names start with lower case.

Now the main method. Follow each line along with the comments. Go supports block comments with “/*…*/” and single line comments with “//”

func main() {
/*
The user can provide a database file name optionally.
If not provided, os.Args will only be of length 1, containing
the program name. If provided, we use the file name.
*/
db_arg := DefaultDbFile
if len(os.Args) > 1 {
// Make sure we discard spaces in input
db_arg = strings.TrimSpace(os.Args[1])
if db_arg == "" {
db_arg = DefaultDbFile
}
}
// We store the file in user's home directory
userHome, _ := os.UserHomeDir()
db_file := userHome + string(os.PathSeparator) + db_arg
// Initialize an empty database
db := util.CSVDatabase {[]util.Account {}}
// Create a buffered reader which optimizes reading the stream
reader := bufio.NewReader(os.Stdin)
// Loop infinitely until a special command to exit the loop
for {
// Prompt the user for a command
fmt.Print("> ")
// Wait for the command
command, err := readTrimmed(reader)
// If error in reading exit
if err != nil {
fmt.Println(err)
os.Exit(0)
}
// "l" for list of accounts
// We use "switch" statement to handle multiple options
switch command {
case "l":
// Load the database
err := db.Read(db_file)
// Error handling
if err != nil {
fmt.Println("Error reading database")
continue
}
// Iterate the accounts to print the account name
// in each line
for _, account := range db.Accounts {
fmt.Println(account.AccountName)
}
case "g": // Handle account display
// Get the account name to fetch
acc_name, _ := promptFor("Account Name", reader)
// Trim spaces and handle empty input
acc_name = strings.TrimSpace(acc_name)
if acc_name == "" {
fmt.Println("Not found")
continue
}
err := db.Read(db_file)
if err != nil {
fmt.Println("Error reading database")
continue
}
index := -1
for i, account := range db.Accounts {
if account.AccountName == acc_name {
index = i
break
}
}
// If account name found, print user name and password
if index != -1 {
acc := db.Accounts[index]
fmt.Println(acc.UserName + ": " + acc.Password)
} else {
fmt.Println("Not found")
}

case "d": // handle deletion
// Get the account name to delete
acc_name, _ := promptFor("Account Name", reader)
// Trim spaces and handle empty input
acc_name = strings.TrimSpace(acc_name)
if acc_name == "" {
fmt.Println("Nothing to delete")
continue
}
// Load DB
err := db.Read(db_file)
if err != nil {
fmt.Println("Error reading database")
continue
}
// Search for matching account name.
// If found, note the index for removal later
index := -1
for i, account := range db.Accounts {
if account.AccountName == acc_name {
index = i
break
}
}
// If account found, we remove that element by appending
// the array slices before the element and after the element
// effectively removing the found element
// Then, clear the current database and write
// the records in memory to file
if index != -1 {
db.Accounts = append(db.Accounts[:index], db.Accounts[index+1:]...)
os.Remove(db_file)
for _, account := range db.Accounts {
newAcc := util.Account { account.AccountName,
account.UserName, account.Password }
db.Write(db_file, &newAcc )
}
} else {
fmt.Println("Nothing to delete")
}
case "a": // to add a new account
// Ask for account name, user name, password
acc_name, err := promptFor("Account Name", reader)
if err != nil || acc_name == "" {
continue
}
user_name, err := promptFor("User Name", reader)
if err != nil {
continue
}
pw, err := promptFor("Password", reader)
if err != nil {
continue
}
// Write the new account to the database
db.Write(db_file, &util.Account {acc_name, user_name, pw} )
case "q": // to quit the program
fmt.Println("Bye")
os.Exit(0)
default:
// nothing
}
}
}
  • The program takes an argument for database file name. It defaults to my_pwds.csv if not provided. The file will be stored in user’s home directory
  • os.Args gets the program arguments. It’s zero-indexed. The first one is the program name itself. The rest of the arguments follow. Here we are interested in value at index 1.
  • We create a reader wrapping the standard input (console)
  • ‘>’ is a prompt character waiting for user input
  • We use a infinite for loop waiting for user command
  • When user enters text, we parse the input
  • We use switch-case statement instead of if-else to handle multiple commands.
  • “l”, “g”, “d”, “a” are special commands to list accounts, get account, delete account, add account respectively
  • For “l”, note how we iterate the array of Accounts in the database. We use a “for each” loop. Each iteration gets the 0-based index and the record at that index
  • For “g”, we search linearly for a matching account name and display the user name and password. This is security risk. Ideally, we may want to use a library to paste the password in the clipboard and expire it after a few seconds, but that is left as an exercise.
  • For “d”, we search linearly in the available accounts for a matching account name. If found, we delete the account by appending the slices before and after the found index and rewriting the resulting slice to database file.
  • Note the notation to append two slices
  • For “a”, we prompt the user for account name, user name, password and append to the database file

Build the program by running:

go build pwmanager

Run pwmanager executable to test the program. Use the supported commands to add, delete, list the accounts and quit the program.

Encryption Utilities

What good is a password manager when data is stored in plain text? In this section, we will add encryption, so no one other than the owner can access your passwords. You may need some basic knowledge of cryptography, but you may look up the terms as needed as you follow the code here.

We need to a strong key to encrypt data. The usual limited length user password is not enough. One way of doing this is to use scrypt to derive a cryptographically strong key from the user password and then use that key to encrypt user data. We will never store this key and we use it in memory for encrypting/decrypting data. The user password should still be strong and be kept secret.

scrypt library is not part of Go standard libs. Run the following to get it.

go get "golang.org/x/crypto"

In pwmanager/util, create a file encryption.go and add the following code.

package util

import (
"crypto/rand"
"golang.org/x/crypto/scrypt"
)

func BuildKey(password []byte, salt []byte) ([]byte, []byte, error) {
if salt == nil {
salt = make([]byte, 16)
_, err := rand.Read(salt)
if err != nil {
return nil, salt, err
}
}
uKey, err := scrypt.Key(password, salt, 32768, 8, 1, 32)
return uKey, salt, err
}
  • “crypto/rand” is a package that provides cryptographically secure random number generator
  • BuildKey() function takes a user password and optionally a array of random bytes (salt). If salt is not provided, the function generates one and returns. The caller should store the salt and reuse it the next time it want to build a key with the same password.
  • make() is a Go built-in function that initializes the given type. Here we get a slice(a view of an array) of 16 bytes that we need for a salt.
  • rand.Read() fills the salt slice with random bytes
  • scrypt.Key() builds the encryption key. Refer to documentation at https://godoc.org/golang.org/x/crypto/scrypt. The last argument is 32 because we need a key of length 32 bytes

Now let us test the function in a new file encryption_test.go

package util

import (
"testing"
)

func TestBuildKey(t *testing.T) {
key, salt, _ := BuildKey([]byte("hello"))
if key == nil || salt == nil {
t.Errorf("Expected write did not happen")
}
}
  • Note how we convert from string to byte array

Run the test with

go test pwmanager/util

For encryption we use a crypto algorithm called AES-GCM. To put it simply, AES is the symmetric key algorithm which means it uses the same key to both encrypt and decrypt. GCM is a mode of operation that provides data integrity and confidentiality. Together AES-GCM provides “authenticated encryption”. Support for this is in Go standard crypto package.

Import cipher packages into encryption.go

“crypto/aes”
“crypto/cipher”

Add the following functions in encryption.go:

func Encrypt(plaintext []byte, key [] byte) ([]byte, []byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, nil, err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, nil, err
}
nonce := make([]byte, 12)
_, err = rand.Read(nonce)
if err != nil {
return nil, nil, err
}
ciphertext := aesgcm.Seal(nil, nonce, plaintext, nil)
return ciphertext, nonce, nil
}
  • Encrypt takes the plain text bytes and the key bytes and returns the encrypted bytes, a nonce and any error. A nonce is again random bytes like salt which is stored and need not be kept secret but should be unique for each plaintext we encrypt.
  • aes.newCipher() creates the AES block cipher object based on the key
  • cipher.NewGCM() wraps the cipher block with GCM implementation
  • We make a nonce of recommended length 12
  • aesgcm.Seal encrypts and authenticates the plaintext

Next we implement the Decryption which is inverse operation of the above encryption function.

func Decrypt(ciphertext []byte, key []byte, nonce []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
return plaintext, nil
}
  • Decrypt takes the ciphertext bytes, key and the nonce (same as that Encrypt() returns) and returns the plain text bytes
  • aesgcm.Open is the inverse operation of Seal. It authenticates and decrypts the cipher text

Again, let us write a test to test both the above functions.

In encryption_test.go, add the following test:

func TestEncDec(t *testing.T) {
key, _, _ := BuildKey([]byte("my_pw"))
ciphertext, nonce, err := Encrypt([]byte("secret"), key)
if err != nil {
t.Errorf("Encryption failed")
}
plaintext, err := Decrypt(ciphertext, key, nonce)
if err != nil {
t.Errorf("Decryption failed")
}
if string(plaintext) != "secret" {
t.Errorf("Could not retrieve secret")
}
}
  • First create a key from a password
  • Then call Encrypt() with a text to encrypt and the key and store the nonce returned
  • Then call Decrypt() on the cipher text passing the key and nonce returned from Encrypt()
  • Verify that we got the original text back

Run

go build
go test

Tests should pass.

We will need a couple of utility functions. We need access to the nonce to decrypt an encrypted string. Typically, it’ll be easier if we keep the nonce and encrypted string together. For the program, we want to represent the combination as nonce and encrypted string separated by “#”.

Below are high level functions for this purpose.

In encryption.go, add new dependencies:

"encoding/base64"
"strings"

Add the functions:

func EncryptToString(key []byte, str string) string {
enc, nonce, _ := Encrypt([]byte(str), key)
return base64.StdEncoding.EncodeToString(nonce) +
"#" + base64.StdEncoding.EncodeToString(enc)
}
  • Return a combination of nonce and encrypted string
  • Nonce and enc are byte arrays, so we Base64 encode them
func DecryptString(key []byte, enc string) (plainText string) {
splits := strings.Split(enc, "#")
nonce, _ := base64.StdEncoding.DecodeString(splits[0])
cipherText, _ := base64.StdEncoding.DecodeString(splits[1])
plainTextBytes, _ := Decrypt(cipherText, key, nonce)
plainText = string(plainTextBytes)
return
}
  • Take the combination and return the original plain text
  • First decode the strings to byte arrays and decrypt the cipher with the nonce

Run in pwmanager/util

go build
go test

Encrypted Database

Remember we had a database that just stored the input data? We need to now encrypt that data.

The encryption utilities return random bytes in the form of salt for the main password and nonce for each string we encrypt. To decrypt the cipher text back to the original text, we need to pass the same nonces to decrypt function. Where do we store these nonces? In the database of course.

Now, the usual way to store the nonce/salt is with the cipher text itself. Because our database is in text format, we encode the bytes into Base64 strings. We can prefix the encrypted string with nonce string with a ‘#’ separator character which is not part of Base64. For decryption, we split the entire string into nonce and cipher and use the nonce to decrypt the cipher.

Add the packages to database.go

"encoding/base64"
"strings"

We add two functions that support the database Write, Read of encrypted Accounts:

func encryptAccount(key []byte, account *Account) {
account.AccountName = EncryptToString(key, account.AccountName)
account.UserName = EncryptToString(key, account.UserName)
account.Password = EncryptToString(key, account.Password)
}
func DecryptAccount(key []byte, account *Account) {
account.AccountName = DecryptString(key, account.AccountName)
account.UserName = DecryptString(key, account.UserName)
account.Password = DecryptString(key, account.Password)
}
  • encryptAccount takes an Account and encryption key and encrypts the Account.
  • decryptAccount does the inverse. It takes and encrypted Account, decrypts each field in place and reassigns the fields

Now that we wrote the transformations to/from encrypted/decrypted accounts, we modify the existing Read/Write database functions to deal with encrypted data.

Modify the DatabaseAccessor to following

type DatabaseAccessor interface {
/* Write a new account into database */
Write(string, *Account, []byte) error
/* Read all accounts from the database */
Read(string, []byte) error
}
  • Change the interface to add the key as additional argument. Now we would need the key to these operations

Change the Write function to add the call to encrypt account.

func (db *CSVDatabase) Write(db_file string, account *Account, key []byte) error {
file, err := os.OpenFile(db_file, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0755)
if err != nil {
return err
}
writer := csv.NewWriter(file)
encryptAccount(key, account)
data := []string { account.AccountName, account.UserName, account.Password }
if err := writer.Write(data); err !=nil {
file.Close()
return err
}
writer.Flush()
file.Close()
if err := writer.Error(); err != nil {
return err
}
return nil
}
  • Write now takes additional ‘key’ parameter confirming to the interface
  • Calls encryptAccount before actual write
func (db *CSVDatabase) Read(db_file string, key []byte) error {
file, err := os.Open(db_file)
if err != nil {
return err
}
reader := csv.NewReader(file)
data, err := reader.ReadAll()
if err != nil {
return err
}

db.Accounts = make([]Account, len(data))
for i, d := range data {
db.Accounts[i] = Account{d[0], d[1], d[2]}
decryptAccount(key, &db.Accounts[i])
}
file.Close()
return nil
}
  • Read now takes additional ‘key’ parameter confirming to the interface
  • Calls decryptAccount() for each account after loading the accounts

We modify the tests in database_test.go to verify the account name value after write and read:

func TestWrite(t *testing.T) {
os.Remove("test_db.csv")
newAcc := Account {"acc","user","pw"}
db := CSVDatabase {[]Account {}}
key, _, _ := BuildKey([]byte("my_pw"), nil)
err := db.Write("test_db.csv", &newAcc, key)
if err != nil {
t.Errorf("Expected write did not happen")
t.Log(err)
}
err = db.Read("test_db.csv", key)
if err != nil {
t.Errorf("Expected read did not happen")
t.Log(err)
}
acc_len := len(db.Accounts)
if acc_len != 1 {
t.Errorf("Expected accounts length: 1, Got %d", acc_len)
}
if db.Accounts[0].AccountName != "acc" {
t.Errorf("Expected account name to be 'acc'")
}
os.Remove("test_db.csv")
}
  • Generate a key to be used for the test and pass to Read/Write methods
  • We fail the test if cannot read back the account name value
func TestRead(t *testing.T) {
var dbAccessor DatabaseAccessor = &CSVDatabase {[]Account {}}
key, _, _ := BuildKey([]byte("my_pw"), nil)
err := dbAccessor.Read("test_db1.csv", key)
if(err == nil) {
t.Errorf("Expected error not thrown")
}
}
  • Generate a key to be used for the test and pass to Read

Run

go test

Note that we didn’t yet modify the main program to use the modified interface. So, if you build pwmanager, you’ll get build failures. We’ll fix it in the next section.

Revisit Main Program

We have now done all the background work to store our secrets. But we didn’t yet ask the user for password based on which we generate the key.

Let us modify our main program to ask for the user password while startup. There are several changes to the main program. So follow along.

Once we get the password, we generate the key in memory and use it as long as the program lives. Remember the key generation function BuildKey() also returns the salt. We need to store it somewhere. If salt changes, the encryption key generated with the same password will change.

Also, because we don’t store the password, how do we authenticate the user? If a new password is provided, the key will change and we cannot decrypt a previously encrypted data with a different password. For this, we will store a known reference string encrypted. If we are able to decrypt that reference encrypted string back to its original value, it means we have the correct password.

If we let the password as a program argument, it may be available in the command history of the terminal. We instead prompt the user for a password and make it non-printable. There’s a sub-package in the Go extended crypto package which let’s us do that. You have already installed it. But there’s a dependency which you have to install.

Run

go get golang.org/x/sys

In manager.go, import the packages below. Several new packages are required. We will see the usage later.

import (
"os"
"fmt"
"bufio"
"io/ioutil"
"antosara.com/pwmanager/util"
"strings"
"bytes"
"encoding/base64"
"syscall"
"golang.org/x/crypto/ssh/terminal"
)

Next a function that reads the password from user and optionally confirm the password:

func readPassword(confirm bool) []byte {
fmt.Print("Enter Password: ")
pw, err := terminal.ReadPassword(int(syscall.Stdin))
if err != nil {
fmt.Println("Unable to get password. Exiting")
os.Exit(0)
}
if !confirm {
return pw
}
fmt.Println()
fmt.Print("Repeat Password: ")
pw2, err := terminal.ReadPassword(int(syscall.Stdin))
if err != nil {
fmt.Println("Unable to get password. Exiting")
os.Exit(0)
}
if bytes.Equal(pw, pw2) {
return pw
}
fmt.Println("Passwords do not match. Exiting")
os.Exit(0)
return nil
}
  • The function takes a boolean argument if password needs to be confirmed and returns the password read
  • If password is not provided or we can’t read we cannot proceed further
  • Note how we use the terminal package to hide the password as user types in.
  • ReadPassword() reads a line of input from a terminal without local echo.
  • When confirmation is used, if passwords don’t match we exit. The bytes.Equal() method can be used to compare contents of two byte arrays

Next, a function to detect first time users:

func firstTime() bool {
userHome, _ := os.UserHomeDir()
_, err := os.Stat(userHome + string(os.PathSeparator) + "my_pwds_ver")
return os.IsNotExist(err)
}
  • We look for a file “my_pwds_auth” where we some store special data. If it doesn’t exist, it means a first time user. We will see later what we write in this and usage of it

Next, a function to initialize the user.

func initialize() []byte {
userHome, _ := os.UserHomeDir()
userPw := readPassword(true)
userKey, salt, _ := util.BuildKey(userPw, nil)
ref := util.EncryptToString(userKey, "hello")
ref_file, _ := os.Create(userHome + string(os.PathSeparator) + "my_pwds_ver")
ref_file.WriteString(base64.StdEncoding.EncodeToString(salt) + "#" + ref)
ref_file.Close()
return userKey
}
  • Read the password from the user
  • Call BuildKey() to generate a new key and salt
  • Encrypt a reference string “hello” with the generated key
  • In a file “my_pwds_auth”, we concatenate and store the key’s salt and reference encrypted string with a ‘#’ separator
  • Return the key which we reuse later

Next, a function to authenticate a user. This is useful when the user returns to open a database. We need to make sure that only the owner can read the data.

func authenticate() []byte {
userPw := readPassword(false)
userHome, _ := os.UserHomeDir()
content, err := ioutil.ReadFile(userHome + string(os.PathSeparator) + "my_pwds_ver")

if err != nil {
fmt.Println("Unable to get password. Exiting")
os.Exit(0)
}
str := string(content)
parts := strings.SplitN(str, "#", 2)
salt, _ := base64.StdEncoding.DecodeString(parts[0])
userKey, _, _ := util.BuildKey(userPw, salt)
pt := util.DecryptString(userKey, parts[1])
if err != nil || pt != "hello" {
fmt.Println("Incorrect password. Exiting")
fmt.Println(err)
os.Exit(0)
}
return userKey
}
  • First, get the password from the user
  • We then get the content of the “my_pwds_auth” file. If this file exists, we would have written 2 pieces of data: salt used while generating key, the encrypted “hello” string combined with the nonce used for encryption as described in initialize() method. The third argument of strings.SplitN() specifies the number of substrings to return. In this case we need two. The second substring will be handled by the DecryptString() call later
  • We use the salt in combination of the password to regenerate the user key. The key will be the same if the password and salt haven’t changed
  • We decrypt the encrypted string we have read using the key
  • If the same key was used for encryption, the resulting decrypted string should be “hello”. We make sure that’s the case. If yes, return the key for subsequent use.

In main() function, add the following after the line that gets the user’s home directory.

...
userHome, _ := os.UserHomeDir()
var userKey []byte
if firstTime() {
userKey = initialize()
} else {
userKey = authenticate()
}
...
  • We initialize a first time user. If not first time user, we authenticate.
  • In either case, either we succeed and get the key or exit the program.

Now, change all util.Read() and util.Write() calls to pass an additional argument which is the userKey.

Run

go install

Verify that the executable “pwmanager” is installed in $GOPATH.

Run $GOPATH/bin/pwmanager.

Congratulations!! You have finished a long journey with the basics of Go lang and cryptography and rewarded yourself with your own password manager.

We will continue learning Go with Part 3 where we will build an exciting game.

--

--

Vinay

All accomplishment is transient. Strive unremittingly.