A Go password manager for the terminal
Armoring for Transfer
The O_TRUNC
option rewinds the write location in an existing password file to the beginning so that subsequent print commands simply overwrite it. Now, the binary data in the encrypted file might confuse editors, or a network transfer program might be tempted to restructure binary sequences while transferring the content over to a remote location. This is why line 20 sets up an armor
type writer to re-encode the binary data, which will then still appear scrambled, but at least as lines of manageable length and without non-printable characters (Figure 3).
The functions used in Listing 2 are a good illustration of the writer mechanism so often used in Go. A writer always takes data from somewhere and then writes it somewhere else. For example, OpenFile()
in line 15 opens a file and returns a writer object named out
. The armor mechanism from the armor package takes this writer object and returns its own writer object, armorWriter
. This object, in turn, is taken by the Encrypt()
function in line 22, which then returns another writer w
, writing encrypted data, to which line 27 then starts writing to using io.WriteString()
.
In other words, the code implements a contraption of nested functions reminiscent of a Unix pipe. The first stage takes the clear-text data, and the armored and encrypted data drops out at the end. Thanks to the writer interface supported by Go, the functions in the chain don't have to worry about the type of data they are transporting: As long as every link in the chain supports the writer interface, everything runs like clockwork.
In our case, the Age functions even use the WriteCloser
interface, which supports both the Write()
and Close()
calls. The Close()
calls are extremely important for buffered output. If the Close()
call is left out, any memory caches used in the pipe may not be cleared at the end, and a truncated and therefore unreadable mess of data will quickly accumulate at the end of the pipe.
Encrypted Read
Conversely, readEnc()
reads data from the encrypted password file starting in line 32 and returns the clear-text data as a string. To do this, the function takes the master password for symmetric encryption as a string and upgrades it to an Identity
object, for the call to the Decrypt()
function later on in line 44.
But again, the reader must first get through the armor of the encrypted data. This is done by the armor
type Reader
in line 43 which in turn receives another reader object for the open password file as a parameter. To read the data from the armor breaker's reader, decrypt it, and store it in a string for return, line 48 uses io.Copy()
to vacuum up all the reader data and drop the data into the waiting out
bytes
buffer. The buffer's String()
method turns the byte array into a string, while line 51 returns the clear-text data to the caller.
I don't want the entries in the password viewer's listbox to come up immediately in full glory when the UI appears later. Rather, I only want to be able to read the first word of each line, while asterisks obfuscate everything else. To do this, the mask()
utility function in Listing 3 takes a string, iterates over its characters, and replaces them with an asterisk if the tomask
flag is set. Initially, this is not the case until line 11 detects a space in the string. The algorithm then thinks it has reached the position to the right of the first word. When it gets there, it sets tomask
to true
and hides the rest of the string under asterisks.
Listing 3
util.go
01 package main 02 func mask(s string) string { 03 masked := []byte(s) 04 tomask := false 05 for i := 0; i < len(s); i++ { 06 if tomask { 07 masked[i] = '*' 08 } else { 09 masked[i] = s[i] 10 } 11 if s[i] == ' ' { 12 tomask = true 13 } 14 } 15 return string(masked) 16 }
The main()
function in Listing 4 checks for the optional --add
command-line argument using the flag package. If the flag is set, the if
block in line 25 jumps to the code starting in line 26, which collects a new user password entry from standard input and appends it to the text of the previously decrypted password file.
Listing 4
pv.go
01 package main 02 import ( 03 "bufio" 04 "errors" 05 "flag" 06 "fmt" 07 "golang.org/x/crypto/ssh/terminal" 08 "os" 09 "strings" 10 ) 11 func main() { 12 add := flag.Bool("add", false, "Add new password entry") 13 flag.Parse() 14 fmt.Printf("Password: ") 15 password, err := terminal.ReadPassword(int(os.Stdin.Fd())) 16 if err != nil { 17 panic(err) 18 } 19 txt, err := readEnc(string(password)) 20 if err != nil { 21 if !errors.Is(err, os.ErrNotExist) { 22 panic(err) 23 } 24 } 25 if *add { 26 fmt.Printf("\rNew entry: ") 27 reader := bufio.NewReader(os.Stdin) 28 entry, _ := reader.ReadString('\n') 29 txt = txt + entry 30 writeEnc(txt, string(password)) 31 return 32 } 33 lines := strings.Split(strings.TrimSuffix(txt, "\n"), "\n") 34 runUI(lines) 35 }
No File, No Problem
To do this, line 14 displays the Password:
prompt for collecting the master password. Line 15 reads it using the standard terminal package via its ReadPassword()
function. The ReadPassword()
function turns off the terminal's echo, so the user can type the password without it being displayed. If the password does not match the one originally set for the password file, readEnc()
in line 19 fails and panic()
in line 22 aborts the program. But if readEnc()
fails because the password file does not yet exist, line 21 traps this and tells the program to continue until either a new entry is appended later or the empty file is displayed in the UI.
On the text of the decrypted file, line 33 uses TrimSuffix()
to remove the last newline character and then employs Split()
to split the whole blob into an array of line strings; both functions are from the standard strings package. Line 34 then passes the array to the runUI()
function, telling it to launch the UI. The UI keeps running until the user hits the quit button, ending the main program, and shutting down the UI.
« Previous 1 2 3 Next »
Buy this article as PDF
(incl. VAT)
Buy Linux Magazine
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters
Support Our Work
Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.

News
-
First Release Candidate for Linux Kernel 6.14 Now Available
Linus Torvalds has officially released the first release candidate for kernel 6.14 and it includes over 500,000 lines of modified code, making for a small release.
-
System76 Refreshes Meerkat Mini PC
If you're looking for a small form factor PC powered by Linux, System76 has exactly what you need in the Meerkat mini PC.
-
Gnome 48 Alpha Ready for Testing
The latest Gnome desktop alpha is now available with plenty of new features and improvements.
-
Wine 10 Includes Plenty to Excite Users
With its latest release, Wine has the usual crop of bug fixes and improvements, along with some exciting new features.
-
Linux Kernel 6.13 Offers Improvements for AMD/Apple Users
The latest Linux kernel is now available, and it includes plenty of improvements, especially for those who use AMD or Apple-based systems.
-
Gnome 48 Debuts New Audio Player
To date, the audio player found within the Gnome desktop has been meh at best, but with the upcoming release that all changes.
-
Plasma 6.3 Ready for Public Beta Testing
Plasma 6.3 will ship with KDE Gear 24.12.1 and KDE Frameworks 6.10, along with some new and exciting features.
-
Budgie 10.10 Scheduled for Q1 2025 with a Surprising Desktop Update
If Budgie is your desktop environment of choice, 2025 is going to be a great year for you.
-
Firefox 134 Offers Improvements for Linux Version
Fans of Linux and Firefox rejoice, as there's a new version available that includes some handy updates.
-
Serpent OS Arrives with a New Alpha Release
After months of silence, Ikey Doherty has released a new alpha for his Serpent OS.