Suitcase: A self-contained encrypted file

Mar 30, 2022

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.

1// Go embed example
2package main
3
4import (
5 _ "embed"
6 "fmt"
7)
8
9//go:embed test.txt
10var fileStr string
11
12func main() {
13 fmt.Println(fileStr)
14}
15

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.

1func Memfile(name string) (int, error) {
2 fd, err := unix.MemfdCreate(name, 0)
3 if err != nil {
4 return -1, err
5 }
6
7 err = unix.Ftruncate(fd, 0)
8 if err != nil {
9 return -1, err
10 }
11
12 return fd, nil
13}
14
15func FdtoFile(fd int) *os.File {
16 pid := os.Getpid()
17 return os.NewFile(uintptr(fd), fmt.Sprintf("/proc/%d/fd/%d", pid, fd))
18}

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

  • Support for passphrases
  • Simple GUI prompt to input the passphrase / private key
  • Ability to embed files larger than 2GB. Currently, Go embed only supports a max size of 2 GB.
  • Windows and Mac support

The code can be found here.

https://blog.trieoflogs.com/feed.xml