Update TODO.
[clive-utils.git] / clivepass
blob254b7c2de08b11de442ed761e51e8a0c3a6c1831
1 #!/usr/bin/env perl
2 # -*- coding: ascii -*-
3 ###########################################################################
4 # clivepass, the login password utility for clive
6 # Copyright (c) 2008, 2009 Toni Gundogdu <legatvs@gmail.com>
8 # Permission to use, copy, modify, and distribute this software for any
9 # purpose with or without fee is hereby granted, provided that the above
10 # copyright notice and this permission notice appear in all copies.
12 # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
13 # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
14 # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
15 # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
16 # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
17 # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
18 # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19 ###########################################################################
21 use warnings;
22 use strict;
24 use constant VERSION => "2.1.7";
26 binmode( STDOUT, ":utf8" );
27 use Getopt::Long qw(:config bundling);
28 use Config::Tiny;
29 use File::Spec;
30 use Encode;
32 my $CONFIGDIR = $ENV{CLIVEPASS_HOME}
33 || File::Spec->catfile( $ENV{HOME}, ".config/clive-utils" );
34 my $PASSWDFILE = File::Spec->catfile( $CONFIGDIR, "passwd" );
36 my %opts;
37 GetOptions( \%opts, 'create|c', 'add|a=s', 'get|g=s', 'edit|e=s', 'delete|d=s',
38 'show|s', 'help|h', 'version|v' => \&print_version, )
39 or exit(1);
41 if ( $opts{help} ) {
42 require Pod::Usage;
43 Pod::Usage::pod2usage( -exitstatus => 0, -verbose => 1 );
46 main();
48 sub main {
49 require File::Path;
50 File::Path::mkpath( [$CONFIGDIR], 0, 0700 );
52 require Crypt::PasswdMD5;
53 require Crypt::Twofish;
54 require MIME::Base64;
55 if ( $opts{add} ) { add_login(); }
56 elsif ( $opts{create} ) { create_passwd(); }
57 elsif ( $opts{edit} ) { edit_login(); }
58 elsif ( $opts{delete} ) { delete_login(); }
59 elsif ( $opts{get} ) { get_login(); }
60 elsif ( $opts{show} ) { show_logins(); }
61 else {
62 print STDERR "Try --help for more info.\n";
66 sub salt {
67 my $l = shift || 2;
68 return join '',
69 ( '.', '/', 0 .. 9, 'A' .. 'Z', 'a' .. 'z' )
70 [ map { rand 64 } ( 1 .. $l ) ];
73 sub create_passwd {
74 if ( -e $PASSWDFILE ) {
75 print "WARN: $PASSWDFILE exists already.\n"
76 . "WARN: You are about to overwrite the existing file.\n"
77 . "WARN: Hit ctrl-c now if that's not your intention.\n";
79 print "Creating $PASSWDFILE.\n";
81 my ( $phrase, $again );
82 $phrase = getpass("Enter a new passphrase: ") while ( !$phrase );
83 print "WARN: Consider using a longer passphrase.\n"
84 if ( length($phrase) < 8 );
85 $again = getpass("Re-enter the passphrase: ") while ( !$again );
87 print STDERR "error: passphrases did not match\n" and exit
88 unless $phrase eq $again;
90 my $passwd = Config::Tiny->new;
91 $passwd->{_}->{phrase} =
92 Crypt::PasswdMD5::unix_md5_crypt( $phrase, salt(8) );
93 $passwd->write($PASSWDFILE);
95 return ( passwd => $passwd, phrase => $phrase );
98 sub verify_phrase {
99 my ($phrase_hash) = @_;
101 print STDERR "error: $PASSWDFILE: phrase hash not found\n" and exit
102 unless $phrase_hash;
104 my $phrase;
105 $phrase = getpass("Enter passphrase: ") while ( !$phrase );
107 if ( Crypt::PasswdMD5::unix_md5_crypt( $phrase, $phrase_hash ) ne
108 $phrase_hash )
110 print STDERR "error: invalid passphrase\n";
111 exit;
113 return $phrase;
116 sub get_key {
117 my ($dupl_user) = @_;
119 print STDERR "error: $PASSWDFILE does not exist, use --create\n"
120 and exit
121 if ( !-e $PASSWDFILE );
123 my $passwd = Config::Tiny->read($PASSWDFILE);
125 if ($dupl_user) {
126 my ( $id, $pwd ) = lookup_login( $passwd, $dupl_user );
127 print STDERR qq/error: login "$dupl_user" / . "exists already\n"
128 and exit
129 if $pwd;
132 my $phrase = verify_phrase( $passwd->{_}->{phrase} );
133 require Digest::MD5;
134 my $key = Digest::MD5::md5_hex($phrase);
136 return ( $key, $passwd );
139 sub getpass {
140 if ( -t STDOUT ) {
141 system "stty -echo";
142 print shift;
144 chomp( my $passwd = <STDIN> );
146 if ( -t STDOUT ) {
147 print "\n";
148 system "stty echo";
150 return $passwd;
153 sub new_login {
154 my ( $key, $passwd, $user ) = @_;
156 my ( $pwd, $again );
157 $pwd = getpass("Enter password for $user: ") while ( !$pwd );
158 $again = getpass("Re-enter the password: ") while ( !$again );
160 print STDERR "error: passwords did not match\n" and exit
161 unless $pwd eq $again;
163 my $c = Crypt::Twofish->new($key);
165 $passwd->{login}->{$user} =
166 MIME::Base64::encode_base64( $c->encrypt( pack( 'a16', $pwd ) ) );
168 $passwd->write($PASSWDFILE);
171 sub add_login {
172 my ( $key, $passwd ) = get_key( $opts{add} );
173 new_login( $key, $passwd, $opts{add} );
176 sub edit_login {
177 my ( $key, $passwd ) = get_key();
178 my ( $id, $pwd ) = lookup_login( $passwd, $opts{edit} );
180 print STDERR "error: no such login\n" and exit unless $pwd;
181 print qq/WARN: Changing password for the login "$id".\n/;
183 new_login( $key, $passwd, $id );
186 sub get_login {
187 my ( $key, $passwd ) = get_key();
188 my ( $id, $pwd ) = lookup_login( $passwd, $opts{get} );
190 print STDERR "error: no such login\n" and exit unless $pwd;
192 my $c = Crypt::Twofish->new($key);
193 print "login: "
194 . $opts{get} . "="
195 . $c->decrypt( MIME::Base64::decode_base64($pwd) ) . "\n";
198 sub show_logins {
199 my $passwd = Config::Tiny->read($PASSWDFILE);
200 foreach ( $passwd->{login} ) {
201 while ( my ( $id, $pwd ) = each( %{$_} ) ) {
202 printf "%20s = %-32s\n", $id, $pwd;
207 sub delete_login {
208 my ( $key, $passwd ) = get_key();
209 my ( $id, $pwd ) = lookup_login( $passwd, $opts{delete} );
211 print STDERR "error: no such login\n" and exit unless $pwd;
213 print qq/WARN: About to delete the login "$id". /
214 . "Confirm delete (y/N): ";
216 chomp( my $confirm = <STDIN> );
217 exit unless $confirm eq "y";
219 delete $passwd->{login}->{$id};
220 $passwd->write($PASSWDFILE);
223 sub lookup_login {
224 my ( $passwd, $user ) = @_;
226 foreach ( $passwd->{login} ) {
227 while ( my ( $id, $pwd ) = each( %{$_} ) ) {
228 if ( $id eq $user ) {
229 return ( $id, $pwd );
235 sub print_version {
236 my $noexit = shift;
237 my $perl_v = sprintf( "--with-perl=%vd", $^V );
238 my $str =
239 sprintf( "clivepass version %s [%s].\n"
240 . "Copyright (c) 2008-2009 Toni Gundogdu "
241 . "<legatvs\@gmail.com>.\n\n",
242 VERSION, $^O );
243 $str .= "\t$perl_v\n";
244 $str .=
245 "\nclivepass is licensed under the ISC license which is "
246 . "functionally\nequivalent to the 2-clause BSD licence.\n"
247 . "\tReport bugs to <http://code.google.com/p/clive-utils/issues/>.\n";
248 return $str if $noexit;
249 print $str;
250 exit;
253 __END__
255 =head1 SYNOPSIS
257 clivepass [option]...
259 =head1 OPTIONS
261 -h, --help print help and exit
262 -v, --version print version and exit
263 -c, --create create new passwd file
264 -a, --add=USERNAME add new login for USERNAME
265 -e, --edit=USERNAME change login password for USERNAME
266 -g, --get=USERNAME dump USERNAME decrypted login password to stdout
267 -s, --show dump all saved login usernames to stdout
268 -d, --delete=USERNAME delete USERNAME from passwd file