diff options
| -rwxr-xr-x | cli/icevault | 122 | ||||
| -rw-r--r-- | cli/icevault.1 | 32 | 
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> | 
