summaryrefslogtreecommitdiff
path: root/util
diff options
context:
space:
mode:
Diffstat (limited to 'util')
-rw-r--r--util/default.nix11
-rwxr-xr-xutil/seacrit123
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";