Suitcase: A self-contained encrypted file
- HariharanRecently 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.
![](/images/suitcase.gif)
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
- 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.