diff options
Diffstat (limited to 'util')
-rw-r--r-- | util/default.nix | 11 | ||||
-rwxr-xr-x | util/seacrit | 123 |
2 files changed, 134 insertions, 0 deletions
diff --git a/util/default.nix b/util/default.nix new file mode 100644 index 0000000..688611b --- /dev/null +++ b/util/default.nix @@ -0,0 +1,11 @@ +{ pkgs ? import <nixpkgs> {} }: + +pkgs.substituteAll { + pname = "seacrit"; + version = "0.0.1"; + + src = ./seacrit; + dir = "bin"; + isExecutable = true; + perl = pkgs.perl.withPackages (p: with p; [ JSON FileTemp ]); +} diff --git a/util/seacrit b/util/seacrit new file mode 100755 index 0000000..2004e0b --- /dev/null +++ b/util/seacrit @@ -0,0 +1,123 @@ +#!@perl@/bin/perl + +use v5.30; +use strict; +use warnings; +use feature 'signatures'; +no warnings 'experimental::signatures'; +use JSON; +use File::Basename; +use File::Temp; +use File::Path qw( make_path ); + +# we deliberately don't attempt to keep decrypted secrets in mlock/dont-dump +# memory because age doesn't either (yet). + +my $identity; +$identity = "$ENV{HOME}/.seacrit/id" if defined $ENV{HOME}; + +sub encryptTo($ipath, $keys, $opath) { + system("age", "-e", (map { ("-r", $_) } @$keys), "-o", $opath, $ipath) == 0 + or die "age encryption failed"; +} + +sub decrypt($ipath) { + open(my $decfh, "-|", "age", "-d", "-i", $identity, $ipath) or die "decryption failed"; + my $result = do { local $/; <$decfh>; }; + close($decfh) or die "age decryption failed"; + $result; +} + +sub parseSecrets() { + my $script = q{ + { secrets, lib }: + builtins.toJSON (lib.mapAttrs (_: a: a ++ secrets.default or []) secrets.secrets) + } =~ s/'/'\\''/gr; + + my $result = qx{ + nix-instantiate --eval --json --expr '$script' \\ + --arg secrets 'import ./secrets.nix' \\ + --arg lib '(import <nixpkgs> {}).lib' + }; + + die "failed to evaluate secrets.nix" if $?; + + decode_json(decode_json($result)); +} + +sub cmdEdit($config, $name, $editor = undef) { + my $recipients = $config->{$name}; + die "no such secret" unless defined $recipients; + my $path = "store/$name"; + + $editor = $ENV{EDITOR} unless defined $editor; + die 'no $EDITOR' unless defined $editor; + + -d dirname($path) || make_path(dirname($path)) or die "failed to create store dir"; + + my $tmp = File::Temp->new; + print $tmp decrypt($path) if -e $path; + system($editor, $tmp) == 0 or die "editing secret failed"; + encryptTo($tmp, $recipients, $path); +} + +sub cmdRekey($config) { + for my $name (keys %$config) { + cmdEdit($config, $name, "true") if -e "store/$name"; + } +} + +sub cmdGenkey($path) { + umask 0077; + -d dirname($path) || mkdir(dirname($path)) or die "could not create key dir"; + system("age-keygen", "-o", $path) == 0 or die "key generation failed"; + system("age-keygen", "-y", "-o", "$path.pub", $path) == 0 or die "key generation failed"; +} + +sub usage() { + print <<~EOT; + usage: + seacrit [ -i FILE ] edit <secret-name> + seacrit [ -i FILE ] rekey + seacrit [ -i FILE ] genkey <key-path> + + General options: + + -h, --help print help + -i, --identity FILE identity to use for decryption + + edit and rekey must be run in the directory of secrets.nix, genkey can be run anywhere. + EOT +} + +sub flop($msg) { + say "error: ", $msg; + say ""; + usage; + exit 1; +} + +while (my $arg = shift @ARGV) { + if ($arg =~ /^-h|--help$/) { + usage; + exit 0; + } elsif ($arg =~ /^-i|--identity$/) { + $identity = shift @ARGV or flop "-i needs an argument"; + } elsif ($arg eq "edit") { + flop "edit needs exactly one argument" unless $#ARGV == 0; + cmdEdit(parseSecrets, $ARGV[0]); + exit 0; + } elsif ($arg eq "rekey") { + flop "too many arguments" unless $#ARGV < 0; + cmdRekey(parseSecrets); + exit 0; + } elsif ($arg eq "genkey") { + flop "too many arguments" unless $#ARGV <= 0; + cmdGenkey($ARGV[0] or $identity); + exit 0; + } else { + flop "unknown argument $arg"; + } +} + +flop "need a command"; |