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:
type Storage struct {
path string
}
func GetStorage(path string) *Storage {
s := Storage{path: path}
return &s
}
func (s *Storage) Init() {
if err := os.Mkdir(s.path, 0755); err != nil {
panic(err)
}
}
func (s *Storage) Add(password, identifier string) 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)
if err3 := os.Mkdir(dirPath, 0755); err3 != nil {
return err3
}
}
absPath := filepath.Join(s.path, identifier)
if err2 := os.WriteFile(absPath, []byte(password), 0600); err2 != nil {
log.Println("Error writing file:", err2)
return err2
}
return nil
}
func (s *Storage) Show(identifier string) (string, error) {
absPath := filepath.Join(s.path, identifier)
password, err := os.ReadFile(absPath)
if err != nil {
return "", err
}
return string(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:
func main() {
path := os.Getenv("HOME") + "/.dost"
storage := internal.GetStorage(path)
generateCmd := flag.NewFlagSet("generate", flag.ExitOnError)
flag.Parse()
if len(os.Args) < 2 {
printHelp()
os.Exit(1)
}
switch os.Args[1] {
case "generate":
if storage.IsReady() {
password, passwordName := internal.Generate(generateCmd)
if err := 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])
if err != 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:
func printHelp() {
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:
func generatePassword(length int, noSymbols bool) (string, error) {
allChars := uppercaseLetters + lowercaseLetters + digits
var password []string
password = append(password, selectRandomCharacter(uppercaseLetters))
password = append(password, selectRandomCharacter(lowercaseLetters))
password = append(password, selectRandomCharacter(digits))
if !noSymbols {
allChars += specialChars
password = append(password, selectRandomCharacter(specialChars))
}
length -= len(password)
for i := 0; i < length; i++ {
randomIndex, err := rand.Int(rand.Reader, big.NewInt(int64(len(allChars))))
if err != nil {
return "", err
}
password = append(password, string(allChars[randomIndex.Int64()]))
}
password = shuffle(password)
return strings.Join(password, ""), nil
}
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:
func TestStorageInitReady(t *testing.T) {
path := getRandomPath()
defer cleanUpPath(path)
storage := GetStorage(path)
storage.Init()
if !storage.IsReady() {
t.Errorf("Expected directory: %s to be created on storage.Init()", path)
}
}
func TestStorageAddShow(t *testing.T) {
path := getRandomPath()
defer cleanUpPath(path)
storage := GetStorage(path)
storage.Init()
identifier := "email/sri@example.com"
password := "someRandomPassword"
addErr := storage.Add(password, identifier)
if addErr != nil {
t.Errorf("Did not expect an error when calling storage.Add: \n%v", addErr)
}
passwordFromFile, showErr := storage.Show(identifier)
if showErr != nil {
t.Errorf("Did not expect an error when calling storage.Show: \n%v", showErr)
}
if passwordFromFile != 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:
path := os.Getenv("HOME") + "/.dost"
storage := internal.GetStorage(path)
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!
Happy coding, and stay secure! 🔐💻
dost
dost is a CLI password manager written in Go.
Inspired by (Pass)[https://www.passwordstore.org/]
Features
- Generate random passwords of configurable length
- Copy generated passwords to clipboard automatically
- Skip using symbols
Usage
> go build -o dost main.go
Generating password:
> ./dost generate email/vema@example.com
Generated Password: );XE,7-Dv?)Aa+&<{V-|pKuq5
Generating password with specified length (default is 25):
> ./dost generate email/vema@example.com 12
Generated Password: si<yJ=5/lEb3
Copy generated password to clipboard without printing:
> ./dost generate -c email/vema@example.com
Copied to clipboard! ✅
Avoid symbols for generating passwords:
> ./dost generate -n email/vema@example.com
Generated Password: E2UST}^{Ac[Fb&D|cD%;Eij>H
Under development
- Insert a new password manually
- Show an existing password
- List all entries
- Password storage
- GPG Key based encryption
License
MIT
Top comments (0)