About: I am a software engineer at product consultancy, hedgehog lab. Right now I am focused on learning interesting things and writing about them in public.
Location:
Newcastle, UK
Joined:
Jun 29, 2017
Building a Password Manager in Go: Part 3
Publish Date: Oct 7 '24
1 0
Welcome back to our journey of building a password manager in Go! In this third installment, we've made significant strides in functionality and usability. Let's dive deep into the exciting new features and improvements.
1. Implementing Password Storage
One of the most crucial additions to our password manager is the ability to store and retrieve passwords. We've implemented a Storage struct that handles these operations:
typeStoragestruct{pathstring}funcGetStorage(pathstring)*Storage{s:=Storage{path:path}return&s}func(s*Storage)Init(){iferr:=os.Mkdir(s.path,0755);err!=nil{panic(err)}}func(s*Storage)Add(password,identifierstring)error{directory:=filepath.Dir(identifier)dirPath:=filepath.Join(s.path,directory)if_,err1:=os.Stat(dirPath);os.IsNotExist(err1){log.Printf("%s does not exist, creating.",dirPath)iferr3:=os.Mkdir(dirPath,0755);err3!=nil{returnerr3}}absPath:=filepath.Join(s.path,identifier)iferr2:=os.WriteFile(absPath,[]byte(password),0600);err2!=nil{log.Println("Error writing file:",err2)returnerr2}returnnil}func(s*Storage)Show(identifierstring)(string,error){absPath:=filepath.Join(s.path,identifier)password,err:=os.ReadFile(absPath)iferr!=nil{return"",err}returnstring(password),nil}func(s*Storage)IsReady()bool{_,err:=os.Stat(s.path)return!os.IsNotExist(err)}
Let's break down these methods:
GetStorage: Creates a new Storage instance with a specified path.
Init: Initializes the storage directory.
Add: Stores a password, creating directories as needed.
Show: Retrieves a stored password.
IsReady: Checks if the storage has been initialized.
This implementation allows for a hierarchical storage structure, enabling users to organize passwords in categories (e.g., email/work@example.com).
2. Enhanced Command-Line Interface
We've significantly improved our CLI, adding new commands and better user guidance:
funcmain(){path:=os.Getenv("HOME")+"/.dost"storage:=internal.GetStorage(path)generateCmd:=flag.NewFlagSet("generate",flag.ExitOnError)flag.Parse()iflen(os.Args)<2{printHelp()os.Exit(1)}switchos.Args[1]{case"generate":ifstorage.IsReady(){password,passwordName:=internal.Generate(generateCmd)iferr:=storage.Add(password,passwordName);err!=nil{fmt.Printf("%v",err)}}else{fmt.Println("dost is not initialized. Run `dost init`")os.Exit(1)}case"init":storage.Init()case"show":password,err:=storage.Show(os.Args[2])iferr!=nil{fmt.Printf("Something went wrong: %v",err)}else{fmt.Println(password)}default:printHelp()}}
This new structure allows for three main commands:
init: Initializes the password store.
generate: Generates a new password and optionally stores it.
show: Retrieves a stored password.
3. User-Friendly Help Function
To make our tool more user-friendly, we've added a printHelp() function:
funcprintHelp(){fmt.Println("Invalid command to run dost password manager.")fmt.Println("Please choose one of the following options:")fmt.Println("dost init")fmt.Println("dost generate [-c] [-n] <password-name>")fmt.Println("dost show <password-name>")}
This function is called when users provide invalid input, guiding them on how to use the password manager correctly.
4. Improved Password Generation
We've refined our password generation algorithm to ensure a good mix of character types:
This approach guarantees that each password includes at least one uppercase letter, one lowercase letter, one digit, and (unless specified otherwise) one special character. The shuffle function ensures that these mandatory characters are not always in the same position.
5. Comprehensive Testing
We've expanded our test suite to cover new functionalities:
funcTestStorageInitReady(t*testing.T){path:=getRandomPath()defercleanUpPath(path)storage:=GetStorage(path)storage.Init()if!storage.IsReady(){t.Errorf("Expected directory: %s to be created on storage.Init()",path)}}funcTestStorageAddShow(t*testing.T){path:=getRandomPath()defercleanUpPath(path)storage:=GetStorage(path)storage.Init()identifier:="email/sri@example.com"password:="someRandomPassword"addErr:=storage.Add(password,identifier)ifaddErr!=nil{t.Errorf("Did not expect an error when calling storage.Add: \n%v",addErr)}passwordFromFile,showErr:=storage.Show(identifier)ifshowErr!=nil{t.Errorf("Did not expect an error when calling storage.Show: \n%v",showErr)}ifpasswordFromFile!=password{t.Errorf("Password that was added did not match the one from the one that got saved\npassword: %s, passwordFromFile: %s",password,passwordFromFile)}}
These tests ensure that our storage mechanisms work correctly, adding an extra layer of reliability to our password manager. We're using random paths for testing to avoid conflicts and cleaning up after each test.
6. Environment-Aware Storage Location
We've made our password manager more user-friendly by storing passwords in the user's home directory:
This approach ensures that the password store is easily accessible and follows common conventions for user-specific data storage.
What's Next?
While we've made significant progress, there's still room for improvement:
Implement encryption for stored passwords to enhance security.
Add a feature to update existing passwords.
Implement a search functionality for stored passwords.
Add support for importing and exporting passwords for backup purposes.
Conclusion
In this iteration, we've transformed our password manager from a simple password generator to a functional tool for storing and retrieving passwords. We've improved the user experience with better CLI interactions and added robust testing to ensure reliability.
The journey of building this password manager has been an excellent opportunity to explore various aspects of Go programming, from file I/O to testing and CLI development. As we continue to develop this tool, we're not just creating a password manager; we're also honing our Go skills and exploring best practices in software development.
Stay tuned for the next part of our series, where we'll tackle these challenges and continue to evolve our password manager!
Remember, the full source code is available on GitHub. Feel free to clone, fork, and contribute to the project. Your feedback and contributions are always welcome!