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
-
So Long Neofetch and Thanks for the Info
Today is a day that every Linux user who enjoys bragging about their system(s) will mourn, as Neofetch has come to an end.
-
Ubuntu 24.04 Comes with a “Flaw"
If you're thinking you might want to upgrade from your current Ubuntu release to the latest, there's something you might want to consider before doing so.
-
Canonical Releases Ubuntu 24.04
After a brief pause because of the XZ vulnerability, Ubuntu 24.04 is now available for install.
-
Linux Servers Targeted by Akira Ransomware
A group of bad actors who have already extorted $42 million have their sights set on the Linux platform.
-
TUXEDO Computers Unveils Linux Laptop Featuring AMD Ryzen CPU
This latest release is the first laptop to include the new CPU from Ryzen and Linux preinstalled.
-
XZ Gets the All-Clear
The back door xz vulnerability has been officially reverted for Fedora 40 and versions 38 and 39 were never affected.
-
Canonical Collaborates with Qualcomm on New Venture
This new joint effort is geared toward bringing Ubuntu and Ubuntu Core to Qualcomm-powered devices.
-
Kodi 21.0 Open-Source Entertainment Hub Released
After a year of development, the award-winning Kodi cross-platform, media center software is now available with many new additions and improvements.
-
Linux Usage Increases in Two Key Areas
If market share is your thing, you'll be happy to know that Linux is on the rise in two areas that, if they keep climbing, could have serious meaning for Linux's future.
-
Vulnerability Discovered in xz Libraries
An urgent alert for Fedora 40 has been posted and users should pay attention.