From 4db324d6089cca44478fbeab594512f4b51540ef Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Fri, 27 Mar 2015 01:55:51 +0100 Subject: Configuration validation; separate store/template. --- cli/icevault | 122 +++++++++++++++++++++++++++++++++++---------------------- cli/icevault.1 | 32 +++++++++------ 2 files changed, 96 insertions(+), 58 deletions(-) (limited to 'cli') 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/(?\Q$h\E)/ : (defined $h and $h ne '') ? qr/(?\Q$h\E[^\P{Graph}\/]*)/ : qr/(?[^\P{Graph}\/]+)/; $pi = (defined $i and $i ne '') ? qr/(?\Q$i\E[^\P{Print}\/]*)/ : qr/(?[^\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 -- cgit v1.2.3