From eb879665ea46776169b3fbf966f0fa8e79ad2958 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Fri, 27 Mar 2015 03:10:05 +0100 Subject: Add a 'git' command. --- cli/icevault | 51 ++++++++++++++++++++++++++++++++++++++++++++++++--- cli/icevault.1 | 47 +++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 89 insertions(+), 9 deletions(-) diff --git a/cli/icevault b/cli/icevault index c93a608..67e06ec 100755 --- a/cli/icevault +++ b/cli/icevault @@ -35,7 +35,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, @GPG); +my (%CONFIG, @GIT, @GPG); my $SOCKET; @@ -146,6 +146,12 @@ sub loadConfig() { error "C<%s> must contain %%s, %%h and %%i placeholders", 'template' unless $CONFIG{template} =~ /%s/ and $CONFIG{template} =~ /%h/ and $CONFIG{template} =~ /%i/; + $CONFIG{'git-dir'} //= '.git'; + $CONFIG{'git-dir'} =~ s#\A~/#$ENV{HOME}#; + $CONFIG{'git-dir'} = "$CONFIG{store}/$CONFIG{'git-dir'}" unless $CONFIG{'git-dir'} =~ /\A\//; + $CONFIG{'git-dir'} =~ /\A(\/\p{Print}+)\z/ or error "Insecure C<%s>", $CONFIG{'git-dir'}; + $CONFIG{'git-dir'} = $1; # untaint $CONFIG{'git-dir'} + $CONFIG{socket} //= 'socket'; $CONFIG{socket} =~ s#\A~/#$ENV{HOME}#; @@ -169,6 +175,7 @@ sub loadConfig() { error "Insecure C<%s>", $CONFIG{gpg} unless $CONFIG{gpg} =~ /\A([-_\@a-zA-Z0-9\.%\/= ]+)\z/; $CONFIG{gpg} = $1; + @GIT = ('git', '--work-tree='.$CONFIG{store}, '--git-dir='.$CONFIG{'git-dir'}); @GPG = split / /, $CONFIG{gpg}; } @@ -453,8 +460,6 @@ sub saveIdentityFile($$) { unlink $outfh->filename or error "Can't unlink C<%s>: %s", $outfh->filename, $!; error "Can't move C<%s> to C<%s>: %s", $outfh->filename, $filename, $r; } - - # TODO: git add $filename; git commit } # Get the visible form list from the server, and croak if it's empty. @@ -580,6 +585,25 @@ sub sha256_file($) { return $sha256->digest; } +# Add the given @filenames to the index and if there are any staged +# changes for these files, commit them with the given message. +sub commit($@) { + my $msg = shift; + my @filenames = @_; + + return unless -d $CONFIG{'git-dir'} and @filenames; + mysystem @GIT, 'add', @filenames; + + # check if there are any staged changes on @filenames + system {$GIT[0]} @GIT, 'diff', '--quiet', '--staged', '--', @filenames; + return unless $?; # exit value 0: nothing staged + error "C<%s> exited with value %d", $GIT[0], ($? >> 8) if $? and $? != -1 and $? != 256; + + $msg =~ /\A(\p{Print}*)\z/ or error "Insecure C<%s>", $msg; + $msg = $1; # untaint $msg + mysystem @GIT, 'commit', '-m', $msg, '--', @filenames; +} + ####################################################################### @@ -594,6 +618,7 @@ my @USAGE = ( clip => "scheme://hostname/identity", dump => "[-p, --show-passwords] scheme://hostname/identity", edit => "scheme://hostname/identity", + git => "GIT-COMMAND [GIT-ARG ...]", insert => "[-f, --force] [-s, --socket=PATH] [identity]", ls => "[-0, --zero] [scheme://[hostname/[identity]]]", ); @@ -771,6 +796,7 @@ elsif ($COMMAND eq 'insert') { undef @{$form->{fields}}[@dontsave] if @dontsave; # remove the field we don't want to save myprintf "Saving identity C<%s>", "$uri/$id"; saveIdentityFile $form, $filename; + commit "Add new identity $uri/$id", $filename; } elsif ($COMMAND eq 'fill') { @@ -921,6 +947,7 @@ elsif ($COMMAND eq 'fill') { if ($changed) { my $r = promptYN "Save changes?", 0; saveIdentityFile ($myform, $filename) if $r; + commit "Save imported changes for $id", $filename; } } @@ -982,6 +1009,7 @@ elsif ($COMMAND eq 'edit') { } elsif (defined $fh2) { myprintf "Saving user changes for identity C<%s>", $id; saveIdentityFile($fh2, $filename); # use the FH we opened before unlinking + commit "Save manual (using $EDITOR) changes for $id", $filename; } else { print "Aborting\n"; exit 1; @@ -1039,6 +1067,23 @@ elsif ($COMMAND eq 'ls') { print $LOCALE->encode($_), $delim foreach sort keys %matches; } +elsif ($COMMAND eq 'git') { + getopts(); + usage() unless @ARGV; + + unless (-d $CONFIG{store}) { + require 'File/Path.pm'; + File::Path::make_path($CONFIG{store}); + myprintf "Created directory C<%s>", $CONFIG{store}; + } + + for (my $i = 0; $i <= $#ARGV; $i++) { + $ARGV[$i] =~ /\A(\p{print}*)\z/ or die; + $ARGV[$i] = $1; + } + exec {$GIT[0]} @GIT, @ARGV; +} + else { print STDERR "Usage: $NAME [COMMAND] [OPTION ...] [ARG ...]\n"; error "Unknown command C<%s>. Try C<%s> for more information.", $COMMAND, "$NAME --help"; diff --git a/cli/icevault.1 b/cli/icevault.1 index 0768b68..906cc8d 100644 --- a/cli/icevault.1 +++ b/cli/icevault.1 @@ -22,12 +22,14 @@ browser and the \fBicevault\fR client. Each form is stored in a separate file, encrypted separately with \fIgpg\fR(1); cleartext are never stored on disk. Form history can be kept -track of by adding the encrypted files to a VCS as binary blobs. File -paths are of the form ".../\fIscheme\fR/\fIhostname\fR/\fIidentity\fR" -where \fIidentity\fR is an arbitrary user-chosen value (allowing -multiple identities for a given site); since the URI of the active tab -can be retrieved from the socket and since the URI of a stored form can -be recovered from its file path, phishing attacks are easily detected. +track of by versioning the encrypted files to a Git repository as binary +blobs. (Modification of the stored forms are then automatically +committed to said repository.) File paths are of the form +".../\fIscheme\fR/\fIhostname\fR/\fIidentity\fR" where \fIidentity\fR is +an arbitrary user-chosen value (allowing multiple identities for a given +site); since the URI of the active tab can be retrieved from the socket +and since the URI of a stored form can be recovered from its file path, +phishing attacks are easily detected. Like Firefox's builtin password manager, IceVault has some heuristics to detect signup and password changing pages. In these cases, and if the @@ -76,6 +78,33 @@ if EDITOR is unset). Upon exit, the file is reencrypted if the SHA-256 digest of its content differs. Note that formatting and comments may not be preserved by subsequent updates of the \fIidentity\fR file. +.TP +.B git\fR \fIGIT-COMMAND\fR [\fIGIT-ARG\fR...] +Pass \fIGIT-COMMAND\fR [\fIGIT-ARG\fR...] as arguments to \fIgit\fR(1) +using the configuration value for \fIstore\fR and that for \fIgit-dir\fR +as the Git working tree and Git repository, respectively. +\fIstore\fR is automatically created if it is not an existing directory. + +It is recommended to initialize the repository as follows: + + \fBicevault git\fR init + echo '*.gpg diff=gpg' >"${XDG_DATA_HOME:-$HOME/.local/share}/icevault/.gitattributes" + \fBicevault git\fR add .gitattributes + \fBicevault git\fR commit \-m 'Add Git attributes for .gpg binary files.' + \fBicevault git\fR config diff.gpg.binary true + \fBicevault git\fR config diff.gpg.textconv 'gpg2 \-o \- \-\-decrypt' + +The textconv config option enable on-the-fly decryption prior to Git +operations such as \fIdiff\fR or \fIgrep\fR, see \fIgitattributes\fR(5). +For instance, grep'ing through the cleartext becomes trivial: + + \fBicevault git\fR grep \-\-textconv \fIpattern\fR + +Signing each commit can be achieved as follows, see \fIgit-config\fR(1): + + \fBicevault git\fR config commit.gpgsign true + \fBicevault git\fR config user.signingkey 0x39278DA8109E6244 + .TP .B insert\fR [\fB-f\fR, \fB--force\fR] [\fB-s\fR, \fB--socket=\fR\fIPATH\fR] [\fIidentity\fR] Create a new \fIscheme\fR://\fIhostname\fR/\fIidentity\fR URI available @@ -121,6 +150,12 @@ XDG_CONFIG_HOME is unset. Empty lines and comments (starting with a "#" characters are ignored). Valid options are: +.TP +.I git-dir +Path to the Git directory. Can be an absolute path or a path relative +to the working directory (specified with \fIstore\fR). +(Default: ".git") + .TP .I gpg The \fIgpg\fR(1) command to use. Note that users of GnuPG 1.4.x will -- cgit v1.2.3