aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xcli/icevault122
-rw-r--r--cli/icevault.132
2 files changed, 96 insertions, 58 deletions
diff --git a/cli/icevault b/cli/icevault
index cc95729..3b69b3a 100755
--- a/cli/icevault
+++ b/cli/icevault
@@ -34,7 +34,7 @@ delete @ENV{qw/IFS CDPATH ENV BASH_ENV/};
my $LANGINFO = I18N::Langinfo::langinfo(I18N::Langinfo::CODESET());
my $LOCALE = Encode::find_encoding $LANGINFO;
-my %CONFIG;
+my (%CONFIG, @GPG);
my $SOCKET;
@@ -130,10 +130,14 @@ sub delete($$) {
#######################################################################
-# Load the given configuration file
-sub loadConfig($) {
- my $configFile = shift;
- error "Configuration file C<%s> doesn't exist", $configFile unless -f $configFile;
+# Load and validate the given configuration file
+sub loadConfig() {
+ my $XDG_CONFIG_HOME = $ENV{XDG_CONFIG_HOME} // "$ENV{HOME}/.config";
+ my $XDG_DATA_HOME = $ENV{XDG_DATA_HOME} // "$ENV{HOME}/.local/share";
+
+ # load config
+ my $configFile = "$XDG_CONFIG_HOME/icevault";
+ error "Missing configuration file C<%s>", $configFile unless -f $configFile;
open my $CONFIG, '<', $configFile or error "Can't open C<%s>: %s", $configFile, $!;
while (<$CONFIG>) {
chomp;
@@ -143,6 +147,42 @@ sub loadConfig($) {
$CONFIG{$1} //= $2;
}
close $CONFIG;
+
+ # set config defaults and validate
+ $CONFIG{store} //= 'icevault';
+ $CONFIG{store} =~ s#\A~/#$ENV{HOME}#;
+ $CONFIG{store} = "$XDG_DATA_HOME/$CONFIG{store}" unless $CONFIG{store} =~ /\A\//;
+ $CONFIG{store} =~ /\A(\/\p{Print}+)\z/ or error "Insecure C<%s>", $CONFIG{store};
+ $CONFIG{store} = $1; # untaint $CONFIG{store}
+
+ $CONFIG{template} //= '%s/%h/%i.gpg';
+ error "C<%s> must contain %%s, %%h and %%i placeholders", 'template'
+ unless $CONFIG{template} =~ /%s/ and $CONFIG{template} =~ /%h/ and $CONFIG{template} =~ /%i/;
+
+ $CONFIG{socket} //= 'socket';
+ $CONFIG{socket} =~ s#\A~/#$ENV{HOME}#;
+
+ error "Missing keyid in configuration file" unless defined $CONFIG{keyid};
+ $CONFIG{keyid} = [ map { /^((?:0x)?\p{AHex}{16}|\p{AHex}{40})$/
+ or error "C<%s> is not a 64-bits key ID or fingerprint", $_;
+ $1 }
+ split /,/, $CONFIG{keyid} ];
+
+ $CONFIG{'max-password-length'} //= 32;
+ error "C<%s> must be a positive integer", 'max-password-length'
+ unless $CONFIG{'max-password-length'} =~ /\A\d+\z/ and $CONFIG{'max-password-length'} > 0;
+
+ $CONFIG{pwgen} //= 'pwgen -s -cyn %d';
+ $CONFIG{pwgen} =~ s#\A~/#$ENV{HOME}#;
+ error "Insecure C<%s>", $CONFIG{pwgen} unless $CONFIG{pwgen} =~ /\A([-_\@a-zA-Z0-9\.%\/= ]+)\z/;
+ $CONFIG{pwgen} = $1;
+
+ $CONFIG{gpg} //= 'gpg';
+ $CONFIG{gpg} =~ s#\A~/#$ENV{HOME}#;
+ error "Insecure C<%s>", $CONFIG{gpg} unless $CONFIG{gpg} =~ /\A([-_\@a-zA-Z0-9\.%\/= ]+)\z/;
+ $CONFIG{gpg} = $1;
+
+ @GPG = split / /, $CONFIG{gpg};
}
# Connect to the given socket and assign the IO::Socket object to
@@ -235,16 +275,18 @@ sub sendCommand(@) {
# $LOCALE-decode'ed.
sub myglob(;$$$) {
my ($s, $h, $i) = @_;
- my $glob = $LOCALE->encode($CONFIG{store});
+ my $store = $CONFIG{store};
+ my $template = $CONFIG{template};
require 'File/Glob.pm';
- $glob =~ s/([\\\[\]\{\}\*\?\~])/\\$1/g;
- $glob =~ s{\%(.)}{ $1 eq '%' ? '%' :
- $1 eq 's' ? $s // '*' :
- $1 eq 'h' ? $h // '*' :
- $1 eq 'i' ? $i // '*' :
- die "Invalid placeholder %$1" }ge;
+ s/([\\\[\]\{\}\*\?\~])/\\$1/g foreach ($store, $template);
+ $template =~ s{\%(.)}{ $1 eq '%' ? '%' :
+ $1 eq 's' ? $s // '*' :
+ $1 eq 'h' ? $h // '*' :
+ $1 eq 'i' ? $i // '*' :
+ die "Invalid placeholder %$1" }ge;
+ my $glob = "$store/$template";
myprintf \*STDERR, "Using glob pattern C<%s>", $glob if $CONFIG{debug};
return File::Glob::bsd_glob($glob);
}
@@ -256,7 +298,6 @@ sub complete($;$) {
my $prefix = shift // '';
my $all = shift;
- my $pat = $CONFIG{store};
my ($s, $h, $i); # extract URI components from the prefix
if ($prefix =~ /\A([A-Za-z0-9-]+):\/\/([^\P{Graph}:\/]+(?::\d+)?)\/([^\P{Print}\/]*)\z/) {
($s, $h, $i) = ($1, $2, $3);
@@ -283,20 +324,21 @@ sub complete($;$) {
$ph = defined $i ? qr/(?<h>\Q$h\E)/ : (defined $h and $h ne '') ? qr/(?<h>\Q$h\E[^\P{Graph}\/]*)/ : qr/(?<h>[^\P{Graph}\/]+)/;
$pi = (defined $i and $i ne '') ? qr/(?<i>\Q$i\E[^\P{Print}\/]*)/ : qr/(?<i>[^\P{Print}\/]+)/;
- $pat =~ s/(\%.)([^\%]*)\z/$1.quotemeta($2)/e;
- $pat =~ s{(.*?)\%(.)}{$2 eq '%' ? '%' :
- $2 eq 's' ? quotemeta($1).$ps :
- $2 eq 'h' ? quotemeta($1).$ph :
- $2 eq 'i' ? quotemeta($1).$pi :
- die "Invalid placeholder %$1"}ge;
- $pat = qr/\A$pat\z/;
-
- myprintf \*STDERR, "Using regexp C<%s>", "$pat" if $CONFIG{debug};
+ my $store = $LOCALE->encode($CONFIG{store});
+ my $template = $LOCALE->encode($CONFIG{template});
+ $template =~ s/(\%.)([^\%]*)\z/$1.quotemeta($2)/e;
+ $template =~ s{(.*?)\%(.)}{$2 eq '%' ? '%' :
+ $2 eq 's' ? quotemeta($1).$ps :
+ $2 eq 'h' ? quotemeta($1).$ph :
+ $2 eq 'i' ? quotemeta($1).$pi :
+ die "Invalid placeholder %$1"}ge;
+ my $pattern = qr/\A\Q$store\/\E$template\z/;
+ myprintf \*STDERR, "Using regexp C<%s>", "$pattern" if $CONFIG{debug};
my @matches;
foreach my $filename (myglob($gs, $gh, $gi)) {
next unless -f $filename;
- $LOCALE->decode($filename) =~ $pat or die "$filename doesn't match $pat";
+ $LOCALE->decode($filename) =~ $pattern or die "$filename doesn't match $pattern";
push @matches, "$+{s}://$+{h}/$+{i}";
}
return @matches if $all or $#matches < 1;
@@ -349,13 +391,13 @@ sub getIdentityFile($) {
or error "Invalid identity C<%s>", $id;
my ($s, $h, $i) = ($1, $2, $3);
- my $filename = $CONFIG{store};
- $filename =~ s{\%(.)}{ $1 eq '%' ? '%' :
+ my $template = $LOCALE->encode($CONFIG{template});
+ $template =~ s{\%(.)}{ $1 eq '%' ? '%' :
$1 eq 's' ? $s :
$1 eq 'h' ? $h :
$1 eq 'i' ? $i :
die "Invalid placeholder %$1" }ge;
- return $filename;
+ return "$CONFIG{store}/$template";
}
# Decrypt the given identity file. In scalar context, return the
@@ -369,11 +411,11 @@ sub loadIdentityFile($;$) {
require 'IPC/Open2.pm';
my $pid = IPC::Open2::open2( (defined wantarray ? $fh : ">&".$fh->fileno)
, "<&".fileno($NULL)
- , $CONFIG{gpg}, qw/-o - --decrypt --/, $filename)
+ , @GPG, qw/-o - --decrypt --/, $filename)
or error "Can't fork: %s", $!;
my $str = do { local $/ = undef; <$fh> } if defined wantarray;
waitpid $pid, 0;
- error "C<%s> exited with value %d", $CONFIG{gpg}, ($? >> 8) if $? and $? != -1;
+ error "C<%s> exited with value %d", $GPG[0], ($? >> 8) if $? and $? != -1;
close $fh;
return unless defined wantarray;
@@ -407,13 +449,14 @@ sub saveIdentityFile($$) {
my $infh = "<&".fileno($form) if ref $form eq 'GLOB';
my $outfh = File::Temp->new(SUFFIX => '.gpg', UNLINK => 0, TMPDIR => 1) or die;
my $pid = IPC::Open2::open2( ">&".$outfh->fileno, $infh
- , $CONFIG{gpg}, qw/-o - --no-encrypt-to --recipient/, $CONFIG{keyid}
+ , @GPG, qw/-o - --no-encrypt-to/
+ , (map {('--recipient', $_)} @{$CONFIG{keyid}})
, '--encrypt' )
or error "Can't fork: %s", $!;
print $infh $form unless ref $form;
close $infh;
waitpid $pid, 0;
- error "C<%s> exited with value %d", $CONFIG{gpg}, ($? >> 8) if $? and $? != -1;
+ error "C<%s> exited with value %d", $GPG[0], ($? >> 8) if $? and $? != -1;
$outfh->close;
my $parent_dir = $filename =~ s/\/[^\/]+$//r;
@@ -553,26 +596,13 @@ sub sha256_file($) {
#######################################################################
+usage(1) unless @ARGV;
+@ARGV = map { $LOCALE->decode($_) } @ARGV;
+my $confFilename = ($ENV{XDG_CONFIG_HOME} // "$ENV{HOME}/.data") . "/icevault";
GetOptions(\%CONFIG, qw/debug show-passwords|p socket|s=s help|? zero|0/) or usage(1);
usage(0) if $CONFIG{help};
# Load configuration
-my $XDG_CONFIG_HOME = $ENV{XDG_CONFIG_HOME} // "$ENV{HOME}/.config";
-my $XDG_DATA_HOME = $ENV{XDG_DATA_HOME} // "$ENV{HOME}/.data";
-loadConfig "$XDG_CONFIG_HOME/icevault";
-
-# Default options
-$CONFIG{gpg} //= 'gpg';
-$CONFIG{socket} //= 'S.IceVault';
-$CONFIG{store} //= "$XDG_DATA_HOME/icevault/%s/%h/%i.gpg";
-$CONFIG{pwgen} //= 'pwgen -s -cyn %d';
-$CONFIG{'max-password-length'} //= 32;
-$CONFIG{keyid} // error "Missing keyid in configuration file";
-error "C<%s> is not a 64-bits key ID or fingerprint", $CONFIG{keyid}
- unless $CONFIG{keyid} =~ /^(?:(?:0x)?\p{AHex}{16}|\p{AHex}{40})$/;
-
-usage(1) unless @ARGV;
-@ARGV = map { $LOCALE->decode($_) } @ARGV;
my $command = $ARGV[0] =~ /\A[A-Za-z0-9-]+:\/\//aa ? 'fill' : shift;
# Process the commands
diff --git a/cli/icevault.1 b/cli/icevault.1
index 0eac11f..7db6be9 100644
--- a/cli/icevault.1
+++ b/cli/icevault.1
@@ -117,9 +117,9 @@ output. This flags turns off this behavior.
.TP
.B \-s\fR \fIsockpath\fR, \fB\-\-socket=\fR\fIsockpath\fR
Specify the path of the UNIX socket used to communicate with the
-browser. If the path does not start with a slash "/", it is assumed to
-be relative to the default Firefox profile (or first profile found if
-there is no default profile) in the "~/.mozilla/firefox" directory.
+browser. Can be an absolute path or a path relative to the default
+Firefox profile (or first profile found if there is no default profile)
+in the "~/.mozilla/firefox" directory.
The socket path and permissions can be configured on the
Iceweasel/Firefox side with the "extensions.icevault.socketPath" and
"extensions.icevault.socketPerms" preferences in "about:config",
@@ -144,12 +144,14 @@ Valid options are:
.TP
.I gpg
-The \fIgpg\fR(1) binary to use. (Default: "gpg".)
+The \fIgpg\fR(1) command to use. Note that users of GnuPG 1.4.x will
+probably want to add the \fB--use-agent\fR option. (Default: "gpg".)
.TP
.I keyid
-The OpenPGP key ID used as encryption recipient. Must be given a
-64-bits keyid or full fingerprint.
+A comma-separated list of OpenPGP key ID(s) used as encryption
+recipient(s). Each component must be given as 64-bits keyid or full
+fingerprint.
.TP
.I max-password-length
@@ -166,10 +168,10 @@ is not considered part of the password.
.TP
.I socket
-The path of the UNIX socket used to communicate with the browser. If
-the path does not start with a slash "/", it is assumed to be relative
-to the default Firefox profile (or first profile found if there is no
-default profile) in the "~/.mozilla/firefox" directory.
+The path of the UNIX socket used to communicate with the browser. Can
+be an absolute path or a path relative to the default Firefox profile
+(or first profile found if there is no default profile) in the
+"~/.mozilla/firefox" directory.
The socket path and permissions can be configured on the
Iceweasel/Firefox side with the "extensions.icevault.socketPath" and
"extensions.icevault.socketPerms" preferences in "about:config",
@@ -178,12 +180,18 @@ respectively.
.TP
.I store
+The working directory. Can be an absolute path or a path relative
+to \fI$XDG_CONFIG_HOME\fR (or \fI~/.local/share\fR if XDG_CONFIG_HOME is
+unset).
+(Default: "icevault".)
+
+.TP
+.I template
The template mapping \fIscheme\fR://\fIhostname\fR/\fIidentity\fR URIs
to (encrypted) files on disk. Must contain "%s", "%h", and "%i", which
respectively expand to the \fIscheme\fR, \fIhostname\fR and
\fIidentity\fR parts of the URI.
-(Default: "$XDG_DATA_HOME/icevault/%s/%h/%i.gpg", or
-"~/.data/icevault/%s/%h/%i.gpg" if $XDG_DATA_HOME is unset.)
+(Default: "%s/%h/%i.gpg".)
.SH AUTHOR
Guilhem Moulin <guilhem@fripost.org>