Secret Safe in Go

Eimantas Jazonis · June 30, 2019

Small CLI application project created mainly for Go language learning purpose. The application provides simple and secure way of storing and retrieving your passwords, API keys or any other sensitive and deeply personal information (that’s a joke, seriously, don’t use it for that).

Few words about the implementation

As you already know - the program is written in Go language which provides a very nice and clean way of building CLI applications. The full application process pipeline consists of these steps:

CLI call (cobra library) <--> Main application logic <--> Text encryption/decryption <--> .secrets file

The structure of the application is as follows:

/chiper
    /chiper.go
/cmd
    CLI.go
    /cobra
        delete.go
        list.go
        set.go
        get.go
        root.go
vault.go        
  • chiper.go is responsible for encryption and decryption of the file. Methods in this file takes original reader and writer and wraps encryption/decryption functionality around it, thus providing an easier manipulation with the file. For encryption and decryption Golang’s crypto/cipher library was used (which is a part of Go standard library). An example DecryptReader function is shown in the code block:
// DecryptReader will return a reader that decodes the data from provided reader
func DecryptReader(key string, r io.Reader) (*cipher.StreamReader, error) {
	iv := make([]byte, aes.BlockSize)
	n, err := r.Read(iv)
	if n < len(iv) || err != nil {
		errors.New("DecryptWriter: unable to read the full iv")
	}
	stream, err := decryptStream(key, iv)
	if err != nil {
		return nil, err
	}
	return &cipher.StreamReader{
		S: stream, R: r,
	}, nil

}
  • Using chiper.go functionality, vault.go file was created which holds the main logic of the application. Each secret was defined as a structure. In order to get the secret from the vault we are loading the safe which calls DecryptReader, wrapping decryption functionality within the reader. This type of function chaining was created in order to simplify the reading/writing process and as an introduction to goish way of programming (with which I still sometimes struggle).
type Safe struct {
	encodingKey string
	filePath    string
	mutex       sync.Mutex
	keyValues   map[string]string
}

// Get returns the secret from our super secret Vault
func (s *Safe) Get(key string) (string, error) {
	s.mutex.Lock()
	defer s.mutex.Unlock()
	err := s.load()
	if err != nil {
		return "", err
	}
	value, ok := s.keyValues[key]
	if !ok {
		return "", errors.New("Secret: no value for your key")
	}
	return value, nil
}

func (s *Safe) load() error {
	f, err := os.Open(s.filePath)
	// In case the file is empty
	if err != nil {
		s.keyValues = make(map[string]string)
		return nil
	}
	defer f.Close()

	r, err := cipher.DecryptReader(s.encodingKey, f)
	if err != nil {
		return err
	}
	return s.readKeyValues(r)
}

func (s *Safe) readKeyValues(r io.Reader) error {
	// -->DecryptReader --> {bufferedReader} --> file
	decoder := json.NewDecoder(r) 
	return decoder.Decode(&s.keyValues)
}

If the code above gives you trouble - you are more than welcome to check the files it in my github - cipher.go and vault.go. Things should clear up at least a bit 😉

  • /cmd consists mainly of CLI functionality which was achieved using cobra library (main CLI.go file, action files).

Usage examples

Now, when we get implementation details out of the way - how does it work? Well first, let’s set the twitter password using ./secret set command:

We can then list all the secrets within the safe:

If we want to get the specific encrypted value (password) from the safe:

Or we can delete the existent instance:

If you think typing a password every time is a dumb idea - you can skip it. By default, the empty string will be used as a password. If we take a closer look to our .secret we can see the data inside is encrypted and unreadable:

What can be improved in the future?

There are a lot of things that can be improved. At first I was thinking about moving away from file saving structure towards the key-value store like LevelDB or BoltDB. The reason I didn’t do that - I see a value when working with file. For one, we can control it with version control tools like git (that way, even if you delete the file, the data is not lost). Another reason is portability - it is way easier to copy ant move a file to another location or machine, heck we can send it even through Skype.

Additional security steps could be implemented. I don’t particularly like entering password in plain text and keeping it in shell history. One way of solving this would be to use crypto/ssh/terminal package and ReadPassword() function from it (I think it only works on linux/mac os though).

In any case that was a fun, small project to do. I hope to see you in the next one!!!!!

The full code can be be found here.

That is a very cool Gopher with beard you’ve made!

Thank you, I liked it too! 😀

Twitter, Facebook