commit 884d5b5ca9e83e3ef7e18370089728e0cae41a7f from: Alexander Arkhipov date: Sun Dec 31 09:58:51 2023 UTC rewrite gpm completely in perl Aside from the language change, the changelist is pretty large: - Added support for creating directories of arbitrary depth within GPM_DIR - Added support for GPM_GPG environment variable and -g flag - The GPM_RECIPIENT environment variable and -r flag are now optional - Added support for multiline secrets - Added the -m flag for the add command - The add command no longer forces the .gpg suffix - Added support for the GPM_LSCMD environment variable - The rm command now asks before each removal (for forced removal use "cd $GPM_DIR && rm -f $file") - No longer trying to be "clever" about newlines 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 = ; + print "\n"; + print "Repeat:"; + my $sec2 = ; + print "\n"; + die "Sorry\n" if $sec ne $sec2; - exit $ret + system "stty echo"; + } else { while () { $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 <= 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? "; + =~ 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 .