Commit Diff


commit - d53e6030d6f2909afd87222e4a4203dcd01a6469
commit + 884d5b5ca9e83e3ef7e18370089728e0cae41a7f
blob - 579b567ed9c5b4a7934f856a0b2b44cadb1d3fea
blob + 500c7c6cff3f0f14ddc0d90f4336dfbae20eaa40
--- gpm
+++ gpm
@@ -1,113 +1,214 @@
-#!/bin/sh
+#!/usr/bin/perl
 
-umask 077
+use strict;
+use warnings;
 
-[ "$GPM_DIR" ] || if [ "$XDG_DATA_HOME" ]; then
-	GPM_DIR="$XDG_DATA_HOME/gpm"
-else
-	GPM_DIR="$HOME/.gpm"
-fi
+use IPC::Open2;
+use Getopt::Std;
+use File::Basename;
+use File::Path 'make_path';
 
-[ -d "$GPM_DIR" ] || mkdir -p "$GPM_DIR" || exit $?
+our ($opt_g, $opt_d, $opt_r);
+my $gpg;
 
-# Normal files should be created read-only.
-umask 377
+# usage: print usage information to stderr and exit with error.
+sub usage {
+	my $cmd = basename $0;
+	die
+"usage: $cmd [-g command] [-d dir] add [-m] name\n" .
+"       $cmd [-d dir] rm name ...\n" .
+"       $cmd [-g command] [-d dir] show name\n" .
+"       $cmd [-d dir] mv from to\n" .
+"       $cmd [-d dir] ls\n";
+}
 
-cd "$GPM_DIR" || exit 1
-
-err() {
-	eval=$1
-	shift
-	echo $0: "$@" >&2
-	exit $eval
+# getrecipient: return string to be used with gpg's -r option.
+#
+# Past versions of gpm (written in shell) required explicit recipient, set by
+# option -r, or the GPM_RECIPIENT environment variable. This is completely
+# unnecessary now, due to GPM_GPG and -g, but legacy syntax is still maintained.
+sub getrecipient {
+	my $r = $opt_r;
+	defined $r or $r = $ENV{GPM_RECIPIENT};
+	return $r;
 }
 
-g() {
-	[ "$gpg" ] || if which gpg >/dev/null 2>&1; then
-		gpg=gpg
-	elif which gpg2 >/dev/null 2>&1; then
-		gpg=gpg2
-	else
-		echo "couldn't find gpg" 2>/dev/null
-		exit 1
-	fi
-
-	$gpg "$@"
+# ckpath $path
+#
+# Return 1 if $path doesn't start with /, or contain .. files, and 0 otherwise.
+sub ckpath {
+	my ($p) = @_;
+	return $p !~ m,^/, && $p ne ".." && $p !~ m,^\.\./, && $p !~ m,/\.\./,
+	    && $p !~ m,/\.\.$,;
 }
 
-gpgname() {
-	if printf %s\\n "$1" | grep -q '\.gpg$'; then
-		printf %s\\n "$1"
-	else
-		printf %s\\n "$1.gpg"
-	fi
+# cklegacy $f
+#
+# Returns the correct path for $f (with or without the .gpg suffix), or dies
+# if no such file exists.
+sub cklegacy {
+	my ($f) = @_;
+	if (!-f $f) {
+		my $f2 = $f . ".gpg";
+		-f $f2 or die "neither $f, nor $f2 exist as regular files\n";
+		return $f2;
+	}
+	return $f;
 }
 
-gpgbasename() {
-	printf %s\\n $(gpgname $(basename "$1"))
+# prunetree $d: remove empty directories, starting from $d, and going up.
+sub prunetree {
+	for (my ($d) = @_; $d ne "."; $d = dirname $d) {
+		rmdir $d or last;
+	}
 }
 
-add() {
-	ret=0
+# add
+#
+# Encrypt the secret from stdin, and store the ciphertext in file specified
+# on the command line.
+sub add {
+	our $opt_m;
+	my $r = getrecipient;
+	my ($cmd, $sec);
 
-	[ "$GPM_RECIPIENT" ] ||
-	    err 1 "please, set GPM_RECIPIENT, or use the -r option"
+	$cmd .= "$gpg -e";
+	$cmd .= " -r $r" if defined $r;
 
-	[ "$1" ] || usage
-	out=$(gpgbasename "$1")
+	getopts('m') or usage;
+	$#ARGV >= 0 or usage;
 
-	[ -e "$out" ] && err 1 "$1 already exists"
+	my $outfile = $ARGV[0];
+	ckpath $outfile or die "bad path: $outfile\n";
 
-	if [ -t 0 ]; then
-		stty -echo
-		printf %s secret:; IFS= read -r sec; echo
-		printf %s confirm:; IFS= read -r confirm; echo
-		stty echo
-		[ "$sec" = "$confirm" ] || err 1 "confirmation failed"
-	else
-		IFS= read -r sec
-	fi
+	-e $outfile and die "$outfile already exists\n";
 
-	printf %s "$sec" | g -e -r "$GPM_RECIPIENT" >"$out" ||
-	    { rm -f "$out"; ret=1; }
+	if (-t STDIN && !$opt_m) {
+		system "stty -echo";
 
-	# The script might be sourced.
-	sec=
-	confirm=
+		print "Secret:";
+		$sec = <STDIN>;
+		print "\n";
+		print "Repeat:";
+		my $sec2 = <STDIN>;
+		print "\n";
+		die "Sorry\n" if $sec ne $sec2;
 
-	exit $ret
+		system "stty echo";
+	} else { while (<STDIN>) { $sec .= $_; } }
+
+	my $pid = open2(my $reader, my $writer, $cmd);
+	print $writer $sec;
+	close($writer);
+	my $out;
+	while (<$reader>) { $out .= $_; }
+	waitpid $pid, 0;
+	$? == 0 or exit 1;
+
+	my $d = dirname $outfile;
+	make_path($d, {mode => 0700});
+	umask 0377;
+	unless (open FH, ">$outfile") {
+		prunetree($d);
+		die "couldn't open $outfile for writing: $!\n";
+	}
+	unless (print FH $out) {
+		prunetree($d);
+		die "couldn't write to $outfile: $!\n";
+	}
 }
 
-move() {
-	[ "$2" ] || usage
-	to=$(gpgbasename "$2")
-	[ -e "$to" ] && err "file $to already exists; aborting"
-	mv $(gpgbasename "$1") "$to"
+# ls
+#
+# Produce a listing of files in the gpm directory, using the GPM_LSCMD
+# environment variable if defined.
+sub ls {
+	my $cmd = $ENV{GPM_LSCMD};
+	unless (defined $cmd) {
+		opendir my $dh, "." or die "couldn't open directory .: $!\n";
+		while (readdir $dh) {
+			if ($_ ne "." && $_ ne ".." && -d $_) {
+				$cmd = "find . -type f | sed 's,^\./,,'";
+				last;
+			}
+		}
+		$cmd = "ls" unless defined $cmd;
+	}
+	system($cmd);
+	$? == 0 or exit 1;
 }
 
-usage() {
-	cat >&2 <<EOF
-usage:	$0 [-d dir] [-r recipient] {add | rm | show} name
-	$0 [-d dir] [-r recipient] mv from to
-	$0 [-d dir] [-r recipient] ls
-EOF
-	exit 1
+# mv: safely rename $ARGV[0] to $ARGV[1].
+sub mv {
+	$#ARGV >= 1 || usage;
+	my ($from, $to) = @ARGV;
+	ckpath $from or die "bad path $from\n";
+	ckpath $to or die "bad path $to\n";
+	-e $to and die "$to already existst\n";
+	$from = cklegacy $from;
+
+	make_path(dirname $to, {mode => 0700});
+	rename $from, $to;
+	prunetree(dirname $from);
 }
 
-while getopts r:d: s; do
-	case $s in
-	d)	GPM_DIR="$OPTARG" ;;
-	r)	GPM_RECIPIENT="$OPTARG" ;;
-	*)	usage ;;
-	esac
-done
-shift $((OPTIND-1))
+# rm: unlink arguments, asking each time.
+sub rm {
+	$#ARGV >= 0 || usage;
+	for (@ARGV) {
+		my $f = $_;
+		unless (ckpath $f) {
+			print STDERR "bad path $f\n";
+			next;
+		}
+		$f = cklegacy $f;
+		print "Really remove $f? ";
+		<STDIN> =~ m/^[Yy]/ and unlink $f;
+		prunetree(dirname $f);
+	}
+}
 
-case $1 in
-a*) shift; add "$@" ;;
-l*) ls ;;
-m*) shift; move "$@" ;;
-r*) rm -f $(gpgbasename "$2") ;;
-s*) g -qd $(gpgbasename "$2") | if [ -t 1 ]; then awk {print}; else cat; fi ;;
- *) usage ;;
-esac
+# show: decrypt file, and print plaintext to stdout.
+sub show {
+	$#ARGV >= 0 or usage;
+	my $file = $ARGV[0];
+	ckpath $file or die "bad path $file\n";
+	$file = cklegacy $file;
+
+	system("$gpg -d $file");
+	$? == 0 or exit 1;
+}
+
+getopts('g:d:r') or usage;
+
+$#ARGV >= 0 or usage;
+my $cmd = $ARGV[0];
+shift @ARGV;
+
+$gpg = $opt_g;
+$gpg = $ENV{GPM_GPG} unless defined $gpg;
+$gpg = "gpg" unless defined $gpg;
+
+my $gpmd = $opt_d;
+defined $gpmd or $gpmd = $ENV{GPM_DIR};
+unless (defined $gpmd) {
+	if (defined $ENV{XDG_DATA_HOME}) {
+		$gpmd = $ENV{XDG_DATA_HOME} . "/gpm";
+	} elsif (defined $ENV{HOME}) {
+		$gpmd = $ENV{HOME} . "/.gpm";
+	} else {
+		die "couldn't determine the gpm directory\n";
+	}
+}
+
+make_path($gpmd, {mode => 0700});
+chdir $gpmd or die "couldn't change directory to $gpmd: $!\n";
+
+for ($cmd) {
+if (/^a/) { add; last; }
+if (/^l/) { ls; last; }
+if (/^m/) { mv; last; }
+if (/^r/) { rm; last; }
+if (/^s/) { show; last; }
+	usage;
+}
blob - 9507dd936eebf8c1b751dcbb16eca751629b553a
blob + 179401690311d277066e3a6275211704518b6350
--- gpm.1
+++ gpm.1
@@ -1,84 +1,95 @@
-.Dd May 30, 2023
+.Dd December 31, 2023
 .Dt GPM 1
 .Os
 .Sh NAME
 .Nm gpm
-.Nd gpg-based password manager
+.Nd gpg-based secret (or password) manager
 .Sh SYNOPSIS
 .Nm
 .Op Fl d Ar dir
-.Op Fl r Ar recipient
+.Op Fl g Ar command
 .Ar command
 .Op Ar arg ...
 .Sh DESCRIPTION
 The utility
 .Nm
-is a password manager.
-Passwords are stored in a single directory as files, encrypted with
-.X/ gpg 1 .
+is a secret manager.
+Secrets are stored in a directory tree as files, encrypted with
+.Xr gpg 1 .
 .Nm
-provides several commands for adding, removing and manipulating passwords.
+provides several commands for manipulating secrets.
 Commands may be specified by their shortest unique prefix (all characters
 after are ignored).
 Commands may accept additional arguments.
 Commands may be preceeded by global options as follows:
 .Bl -tag -width Ds
 .It Fl d Ar dir
-The directory to store and retrieve passwords from.
+The directory to store and retrieve secrets from.
 Overrides
 .Ev GPM_DIR .
-.It Fl r Ar recipient
-User id for whom the passwords are encrypted.
+.It Fl g Ar command
+The
+.Xr gpg 1
+command.
 Overrides
-.Ev GPM_RECIPIENT .
-See the
-.Fl r
-flag for
-.Xr gpg 1 .
+.Ev GPM_GPG .
 .El
 .Pp
 The
 .Nm
 commands are as follows:
 .Bl -tag -width Ds
-.It Cm add Ar name
-Create a new password
+.It Xo
+.Cm add
+.Op Fl m
+.Ar name
+.Xc
+Create a new secret
 .Ar name .
-The new password will be read from stdin.
-If used from a TTY, a confirmation will be requested.
+The new secret is read from stdin.
+If used from a TTY without the
+.Fl m
+flag, a single line is read twice, and not echoed.
+Otherwise, an arbitrary amount of lines is read normally once.
 .It Cm ls
-List existing passwords.
+List existing secrets, using the command in
+.Ev GPM_LSCMD
+if set.
 .It Cm mv Ar from Ar to
-Rename password
+Rename secret
 .Ar from
 to
 .Ar to .
-.It Cm rm Ar name
-Remove password
-.Ar name .
+.It Cm rm Ar name ...
+Remove secrets specified on the command line.
+.Nm
+will ask for confirmation before each removal.
 .It Cm show Ar name
-Decrypt the password
-.Ar name
-and print it to stdout.
+Decrypt the secret
+.Ar name ,
+and print plaintext to stdout.
 .El
 .Sh ENVIRONMENT
-.Bl -tag -width GPM_RECIPIENT
+.Bl -tag -width XDG_DATA_HOME
 .It Ev GPM_DIR
-Directory in which the passwords are stored.
+Directory in which the secrets are stored.
+.It Ev GPM_LSCMD
+Shell command used for the
+.Nm
+command
+.Cm ls .
 .It Ev XDG_DATA_HOME
 If
 .Ev XDG_DATA_HOME
 is set, but
 .Ev GPM_DIR
-isn't, the default password directory will be
+isn't, the default secret directory is
 .Pa $XDG_DATA_HOME/gpm .
-.It Ev GPM_RECIPIENT
-The recipient to whom passwords are encrypted.
 .El
 .Sh FILES
 .Bl -tag -width Ds
 .It Pa $HOME/.gpm
-The default password directory if neither
+The default secret directory if neither
 .Ev GPM_DIR ,
 nor
 .Ev XDG_DATA_HOME
@@ -91,11 +102,11 @@ Tab-completion may be set, e.g. with
 .Xr ksh 1 :
 .Bd -literal -offset indent
 set -A complete_gpm_1 -- add ls mv rm show
-set -A complete_gpm -- $(gpm l)
+set -A complete_gpm -- $(gpm ls)
 .Ed
 .Sh SEE ALSO
 .Xr gpg 1 ,
 .Xr gpg2 1 ,
 .Xr pm 1
 .Sh AUTHORS
-.An Alexander Arkhipov Aq Mt aa@manpager.net .
+.An Alexander Arkhipov Aq Mt aa@manpager.org .