Encrypting and decrypting#
One of the features I needed for a full declarative setup was a way to restore data back to the system.
If I were not using a separate file system mounted like /var or /lib I would need a way to restore data.
With restoration of data I needed a way to encrypt files with Age and then decrypt the files with Age as well.
Below is the Systemd service and Golang program I wrote to solve for this problem.
It is named smanager for “secrets manager”.
The secrets manager program is able to encrypt and decrypt a file that contains environtment variables for restoring services Restic. Using restic I am able to restor data to anywhere I need.
{...}: {
environment.etc."smanager/agefile-pass".source = ../smanager/bin/agefile-pass;
environment.etc."smanager/env.age".source = ../secrets/env.age;
environment.etc."smanager/README.md".source = ../README.md;
# Ensure /var/lib folders exist
systemd.tmpfiles.rules = [
"d /var/lib/nik3sx/smanager 0755 root root -"
# Optional: copy the binary here (could also run from Nix store directly)
"L /var/lib/nik3sx/smanager/agefile-pass 0755 root root - /etc/smanager/agefile-pass"
"L /var/lib/nik3sx/smanager/env.age 0755 root root - /etc/smanager/env.age"
"L /var/lib/nik3sx/smanager/README.md 0755 root root - /etc/smanager/README.md"
"L /root/README.md 0755 root root - /etc/smanager/README.md"
];
}# Example
./smanager/bin/agefile-pass -decrypt --in ./secrets/env.age --out ./secrets/env
# On the remote system
/var/lib/nik3sx/smanager/agefile-pass -decrypt --in /var/lib/nik3sx/smanager/env.age --out /var/lib/nik3sx/smanager/env
source /var/lib/nik3sx/smanager/envIn a quick 130ish lines we have the following tool able to encrypt and decrypt .age files
package main
import (
"flag"
"fmt"
"io"
"os"
"filippo.io/age"
"filippo.io/age/armor"
"golang.org/x/term"
)
func fatal(err error) {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
func readPassword(prompt string) string {
fmt.Fprint(os.Stderr, prompt)
pw, err := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Fprintln(os.Stderr)
if err != nil {
fatal(err)
}
return string(pw)
}
func readPasswordConfirm() string {
pw1 := readPassword("Password: ")
pw2 := readPassword("Confirm password: ")
if pw1 != pw2 {
fatal(fmt.Errorf("passwords do not match"))
}
if pw1 == "" {
fatal(fmt.Errorf("password cannot be empty"))
}
return pw1
}
func encrypt(inPath, outPath string) {
password := readPasswordConfirm()
recipient, err := age.NewScryptRecipient(password)
if err != nil {
fatal(err)
}
recipient.SetWorkFactor(20)
in, err := os.Open(inPath)
if err != nil {
fatal(err)
}
defer in.Close()
out, err := os.Create(outPath)
if err != nil {
fatal(err)
}
defer out.Close()
armorWriter := armor.NewWriter(out)
w, err := age.Encrypt(armorWriter, recipient)
if err != nil {
fatal(err)
}
if _, err := io.Copy(w, in); err != nil {
fatal(err)
}
if err := w.Close(); err != nil {
fatal(err)
}
if err := armorWriter.Close(); err != nil {
fatal(err)
}
}
func decrypt(inPath, outPath string) {
password := readPassword("Password: ")
identity, err := age.NewScryptIdentity(password)
if err != nil {
fatal(err)
}
in, err := os.Open(inPath)
if err != nil {
fatal(err)
}
defer in.Close()
out, err := os.Create(outPath)
if err != nil {
fatal(err)
}
defer out.Close()
armorReader := armor.NewReader(in)
r, err := age.Decrypt(armorReader, identity)
if err != nil {
fatal(err)
}
if _, err := io.Copy(out, r); err != nil {
fatal(err)
}
}
func main() {
var (
encryptFlag = flag.Bool("encrypt", false, "encrypt input file")
decryptFlag = flag.Bool("decrypt", false, "decrypt input file")
inPath = flag.String("in", "", "input file path")
outPath = flag.String("out", "", "output file path")
)
flag.Parse()
if *inPath == "" || *outPath == "" {
fatal(fmt.Errorf("--in and --out are required"))
}
switch {
case *encryptFlag:
encrypt(*inPath, *outPath)
case *decryptFlag:
decrypt(*inPath, *outPath)
default:
fatal(fmt.Errorf("must specify --encrypt or --decrypt"))
}
}