bzr branch
/loggerhead/users/lolando/sgeps/trunk
| Line | Revision | Contents |
| 1 | 1 | #! /usr/bin/perl -w |
| 2 | # |
|
| 3 | 3 | # A simple GnuPG-encrypted password store |
| 4 | 1 | # |
| 5 | 12.2.7 | # Allows to store key/{login,password,notes} pairs in a GPG-encrypted file |
| 6 | 1 | # |
| 7 | # Copyright © 2009, Roland Mas <lolando@debian.org> |
|
| 8 | 12.2.7 | # Copyright © 2010, Ben Voui <intrigeri@boum.org> |
| 9 | 1 | # |
| 10 | # This program is free software; you can redistribute it and/or modify |
|
| 11 | # it under the terms of the GNU General Public License as published by |
|
| 12 | # the Free Software Foundation; either version 2 of the License, or |
|
| 13 | # (at your option) any later version. |
|
| 14 | # |
|
| 15 | 4 | # Usage: sgeps --create to create the store |
| 16 | 12.2.9 | # sgeps --add <key> to add a key/value to the store |
| 17 | 12.2.7 | # sgeps --read <key> to read all data of a key from the store |
| 18 | 12.2.9 | # sgeps --copy <key> to copy the login and password of a key to the X clipboard |
| 19 | # sgeps --copy --login <key> to copy the login of a key to the X clipboard |
|
| 20 | # sgeps --copy --password <key> to copy the password of a key to the X clipboard |
|
| 21 | 10 | # sgeps --delete <key> to delete a key from the store |
| 22 | 4 | # sgeps --list to list existing keys |
| 23 | 12 | # sgeps --search <pattern> to list existing keys matching the pattern |
| 24 | 4 | # sgeps --add --overwrite <key> to replace a key/value |
| 25 | 12.2.11 | # sgeps --add --batch <key> to add a key/value to the store with no interactive terminal |
| 26 | 12.2.10 | # sgeps --edit <key> to edit the data for a key |
| 27 | 1 | # |
| 28 | 4 | # Sample config file, to be stored in ~/.config/sgeps.conf (unless |
| 29 | 1 | # overridden with --config=.../foo.conf): |
| 30 | # ,---- |
|
| 31 | 7 | # | store = ~/s1kr3t/sgeps.gpg |
| 32 | 1 | # | keyid = 0x12345678 |
| 33 | # `---- |
|
| 34 | 12.2.9 | # |
| 35 | # Non-Perl dependencies: GnuPG, xclip. |
|
| 36 | 12.2.12 | # |
| 37 | # TODO: |
|
| 38 | # - add status messages for --copy, to ease frontends development. |
|
| 39 | 1 | |
| 40 | use strict ; |
|
| 41 | ||
| 42 | 12.2.3 | use Storable qw/ nfreeze thaw / ; |
| 43 | 1 | use File::Temp qw/ tempdir / ; |
| 44 | use Getopt::Long ; |
|
| 45 | use Config::Simple ; |
|
| 46 | 12.2.5 | use Term::ReadLine; |
| 47 | 1 | |
| 48 | 12.2.10 | use vars qw/ %database %flags $config_file %config / ; |
| 49 | 1 | |
| 50 | 4 | $config_file = "$ENV{HOME}/.config/sgeps.conf" ; |
| 51 | 1 | |
| 52 | my $result = GetOptions ("read" => \$flags{read}, |
|
| 53 | 12.2.9 | "copy" => \$flags{copy}, |
| 54 | "login" => \$flags{login}, |
|
| 55 | "password" => \$flags{password}, |
|
| 56 | "both" => \$flags{both}, |
|
| 57 | 1 | "add" => \$flags{add}, |
| 58 | 10 | "delete" => \$flags{delete}, |
| 59 | 1 | "overwrite" => \$flags{overwrite}, |
| 60 | 12.2.11 | "batch" => \$flags{batch}, |
| 61 | 12.2.10 | "edit" => \$flags{edit}, |
| 62 | 1 | "list" => \$flags{list}, |
| 63 | 12 | "search" => \$flags{search}, |
| 64 | 1 | "create" => \$flags{create}, |
| 65 | 12.1.1 | "xclip" => \$flags{xclip}, |
| 66 | 1 | "config=s" => \$config_file, |
| 67 | ) ; |
|
| 68 | ||
| 69 | 6 | $config_file = glob $config_file ; |
| 70 | ||
| 71 | 5 | tie %config, "Config::Simple", $config_file or die Config::Simple->error() ; |
| 72 | 1 | |
| 73 | 7 | $config{store} = glob $config{store} ; |
| 74 | ||
| 75 | 1 | SWITCH: { |
| 76 | |
|
| 77 | 12.2.9 | ($flags{read} || $flags{copy}) && do { |
| 78 | 1 | my $kw = $ARGV[0] ; |
| 79 | |
|
| 80 | 9 | die "No key specified" unless (defined $kw and $kw ne '') ; |
| 81 | 8 | |
| 82 | 1 | &readfile ; |
| 83 | ||
| 84 | 12.2.9 | die "No defined value for $kw" unless defined $database{$kw} ; |
| 85 | ||
| 86 | my $v = $database{$kw} ; |
|
| 87 | ||
| 88 | # update the entry to the new format |
|
| 89 | if (! ref($v)) { |
|
| 90 | $database{$kw} = { |
|
| 91 | login => $kw, |
|
| 92 | password => $v, |
|
| 93 | notes => '', |
|
| 94 | } ; |
|
| 95 | &writefile ; |
|
| 96 | $v = $database{$kw} ; |
|
| 97 | } |
|
| 98 | ||
| 99 | 12.2.14 | if ($flags{read} && ! $flags{xclip}) { |
| 100 | 12.2.9 | print "login: " . $v->{login} . "\n" ; |
| 101 | print "password: " . $v->{password} . "\n" ; |
|
| 102 | print "notes: " . $v->{notes} . "\n" ; |
|
| 103 | } |
|
| 104 | 12.2.14 | elsif ($flags{read} && $flags{xclip}) { |
| 105 | ©andwait($v->{password}) ; |
|
| 106 | &emptyclipboard ; |
|
| 107 | } |
|
| 108 | 12.2.9 | elsif ($flags{copy}) { |
| 109 | if ($flags{login}) { |
|
| 110 | ©andwait($v->{login}) ; |
|
| 111 | &emptyclipboard ; |
|
| 112 | } |
|
| 113 | elsif ($flags{password}) { |
|
| 114 | ©andwait($v->{password}) ; |
|
| 115 | &emptyclipboard ; |
|
| 116 | 12.2.5 | } |
| 117 | else { |
|
| 118 | 12.2.9 | ©andwait($v->{login}) ; |
| 119 | &emptyclipboard ; |
|
| 120 | ©andwait($v->{password}) ; |
|
| 121 | &emptyclipboard ; |
|
| 122 | 12.2.5 | } |
| 123 | 12.2.9 | } |
| 124 | 1 | last SWITCH ; |
| 125 | } ; |
|
| 126 | ||
| 127 | 12 | ($flags{list} || $flags{search}) && do { |
| 128 | my $pattern = $ARGV[0] ; |
|
| 129 | ||
| 130 | if (defined $flags{search}) { |
|
| 131 | die "No pattern specified" unless (defined $pattern and $pattern ne '') ; |
|
| 132 | } |
|
| 133 | |
|
| 134 | 1 | &readfile ; |
| 135 | 12 | |
| 136 | print "Defined keys" ; |
|
| 137 | print " (looking for pattern $pattern)" if (defined $flags{search}) ; |
|
| 138 | print ":\n" ; |
|
| 139 | |
|
| 140 | foreach my $key (sort {$a cmp $b} keys %database) { |
|
| 141 | if (defined $flags{search}) { |
|
| 142 | print "$key\n" if ($key =~ m/$pattern/) ; |
|
| 143 | } else { |
|
| 144 | print "$key\n" ; |
|
| 145 | } |
|
| 146 | 1 | } |
| 147 | last SWITCH ; |
|
| 148 | } ; |
|
| 149 | ||
| 150 | 12.2.10 | ($flags{add} || $flags{edit}) && do { |
| 151 | 1 | my $kw = $ARGV[0] ; |
| 152 | ||
| 153 | 9 | die "No key specified" unless (defined $kw and $kw ne '') ; |
| 154 | 8 | |
| 155 | 12.2.2 | &readfile ; |
| 156 | ||
| 157 | 12.2.10 | if (defined ($database{$kw}) && !$flags{overwrite} && !$flags{edit}) { |
| 158 | die "Value already defined for $kw, use --overwrite or --edit" ; |
|
| 159 | 12.2.2 | } |
| 160 | ||
| 161 | 12.2.10 | my %defaults = ( |
| 162 | login => '', |
|
| 163 | password => '', |
|
| 164 | notes => '', |
|
| 165 | ); |
|
| 166 | if ($flags{add}) { |
|
| 167 | $defaults{login} = $kw ; |
|
| 168 | } |
|
| 169 | elsif ($flags{edit}) { |
|
| 170 | %defaults = %{$database{$kw}} |
|
| 171 | } |
|
| 172 | 12.2.5 | |
| 173 | 12.2.10 | $database{$kw} = promptdata($kw, %defaults) ; |
| 174 | 12.2.5 | |
| 175 | 1 | &writefile ; |
| 176 | last SWITCH ; |
|
| 177 | } ; |
|
| 178 | ||
| 179 | 10 | $flags{delete} && do { |
| 180 | my $kw = $ARGV[0] ; |
|
| 181 | ||
| 182 | die "No key specified" unless (defined $kw and $kw ne '') ; |
|
| 183 | |
|
| 184 | print "Are you sure? (y/N) "; |
|
| 185 | my $val = <STDIN>; |
|
| 186 | chomp $val; |
|
| 187 | last SWITCH if $val ne 'y'; |
|
| 188 | ||
| 189 | &readfile ; |
|
| 190 | ||
| 191 | print "Removing $kw from the store\n" ; |
|
| 192 | delete $database{$kw}; |
|
| 193 | &writefile ; |
|
| 194 | last SWITCH ; |
|
| 195 | } ; |
|
| 196 | ||
| 197 | 1 | $flags{create} && do { |
| 198 | if (-e $config{store}) { |
|
| 199 | 9 | die "Store already exists" ; |
| 200 | 1 | } |
| 201 | %database = () ; |
|
| 202 | &writefile ; |
|
| 203 | last SWITCH ; |
|
| 204 | } ; |
|
| 205 | } |
|
| 206 | ||
| 207 | exit 0 ; |
|
| 208 | ||
| 209 | sub readfile () { |
|
| 210 | my $clear = qx! gpg -q -d $config{store} ! ; |
|
| 211 | if ($? != 0) { |
|
| 212 | 9 | die "Error while opening store" ; |
| 213 | 1 | } |
| 214 | %database = %{thaw $clear} ; |
|
| 215 | } |
|
| 216 | ||
| 217 | sub writefile () { |
|
| 218 | 12.2.1 | my $clear = nfreeze \%database ; |
| 219 | 1 | my $tmpdir = tempdir "$config{store}.XXXXXX" ; |
| 220 | my $tmp = "$tmpdir/store" ; |
|
| 221 | 12.2.4 | open W, "| gpg --encrypt --armor --recipient $config{keyid} --output $tmp 2> /dev/null" |
| 222 | or die "Error opening a pipe to gpg" ; |
|
| 223 | print W $clear or die "Error writing to the gpg pipe" ; |
|
| 224 | close W or die "Error closing pipe to gpg" ; |
|
| 225 | rename $tmp, $config{store} or die "Error moving temporary file to " . $config{store} ; |
|
| 226 | 1 | rmdir $tmpdir ; |
| 227 | } |
|
| 228 | 12.2.9 | |
| 229 | # copy passed string to the X clipboard, then wait for it to have been pasted, |
|
| 230 | # then empty the clipboard |
|
| 231 | sub copyandwait () { |
|
| 232 | my $string = shift ; |
|
| 233 | # bug in xclip: removing -verbose breaks -loops functionality |
|
| 234 | open XCLIP, "| xclip -loops 1 -verbose >/dev/null 2>&1" |
|
| 235 | or die "Error opening pipe to xclip" ; |
|
| 236 | print XCLIP $string or die "Error copying data to X clipboard" ; |
|
| 237 | close XCLIP or die "Error closing pipe to xclip" ; |
|
| 238 | ||
| 239 | &emptyclipboard; |
|
| 240 | } |
|
| 241 | ||
| 242 | sub emptyclipboard () { |
|
| 243 | open XCLIP, "| xclip" or die "Error opening pipe to xclip" ; |
|
| 244 | print XCLIP "" or die "Error copying data to X clipboard" ; |
|
| 245 | close XCLIP or die "Error closing pipe to xclip" ; |
|
| 246 | } |
|
| 247 | 12.2.10 | |
| 248 | 12.2.11 | sub promptdata { |
| 249 | 12.2.10 | my $key = shift ; |
| 250 | 12.2.11 | my %default = @_ ; |
| 251 | ||
| 252 | if ($flags{batch}) { |
|
| 253 | my ($login, $password, $notes) = (<STDIN>, <STDIN>, <STDIN>); |
|
| 254 | chomp ($login, $password, $notes); |
|
| 255 | return { |
|
| 256 | login => $login, |
|
| 257 | password => $password, |
|
| 258 | notes => $notes, |
|
| 259 | } ; |
|
| 260 | } |
|
| 261 | else { |
|
| 262 | my $term = Term::ReadLine->new('sgeps') ; |
|
| 263 | return { |
|
| 264 | login => $term->readline("Login: ", $default{login}), |
|
| 265 | password => $term->readline("Password: ", $default{password}), |
|
| 266 | notes => $term->readline("Notes: ", $default{notes}), |
|
| 267 | } ; |
|
| 268 | } |
|
| 269 | 12.2.10 | } |
Loggerhead 1.17 is a web-based interface for Bazaar branches