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.
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.