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/env

In 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"))
	}
}