Suitcase: A self-contained encrypted file

- Hariharan

Recently I was in a situation where I had to share some files containing sensitive data and it had me wondering whether a standalone encrypted file can be created which can be decrypted without any external software. This could be a convenient way to share data securely. So I built a tool to do just that, kind of.

Suitcase CLI tool

Implementation

Go 1.16 introduced an interesting feature, the ability to embed static files in Go binaries. All you have to do is import the “embed” package and add a //go:embed FileName directive to a variable. There were other tools and libraries to embed files before this, but in-built support means you can use it with the existing Go tooling.

// Go embed example
package main

import (
	_ "embed"
	"fmt"
)

//go:embed test.txt
var fileStr string

func main() {
	fmt.Println(fileStr)
}

I used age for encryption because it is secure, creates smaller keys, and is written in Go. It has a very easy-to-use library and a command-line tool.

I created a simple CLI tool called suitcase. You give it a public key and a file. It uses a template to generate a simple Go project. It then encrypts the file which is embedded and compiled into a binary. The template can be edited to add any additional checks and logic if required. The only minor drawback with this approach is, it requires a Go compiler to be present at the time of creating the file container. I was considering embedding a Go compiler into the tool itself, but it was too much work for a weekend project.

When the generated binary file is run, by default, it uses memfd_create to create a temporary in-memory file where the contents are copied after decryption. memfd_create creates an anonymous in-memory file and returns a file descriptor. Once all the references to the file are dropped, it is automatically released. The contents can also be written to a normal file by specifying an output.

func Memfile(name string) (int, error) {
	fd, err := unix.MemfdCreate(name, 0)
	if err != nil {
		return -1, err
	}

	err = unix.Ftruncate(fd, 0)
	if err != nil {
		return -1, err
	}

	return fd, nil
}

func FdtoFile(fd int) *os.File {
	pid := os.Getpid()
	return os.NewFile(uintptr(fd), fmt.Sprintf("/proc/%d/fd/%d", pid, fd))
}

xdg-open is used to open the file in the default application based on its mime type.

Improvements

This was a fun weekend project so I didn’t implement a whole lot of features. Some features I would still like to add include

The code can be found here.