File Coverage

blib/lib/NOTEDB/pwsafe3.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             # Perl module for note
2             # pwsafe3 backend. see docu: perldoc NOTEDB::pwsafe3
3              
4             package NOTEDB::pwsafe3;
5              
6             $NOTEDB::pwsafe3::VERSION = "1.08";
7 1     1   815 use strict;
  1         2  
  1         24  
8 1     1   4 use Data::Dumper;
  1         1  
  1         38  
9 1     1   277 use Time::Local;
  1         1605  
  1         76  
10 1     1   70 use Crypt::PWSafe3;
  0            
  0            
11              
12             use NOTEDB;
13              
14             use Fcntl qw(LOCK_EX LOCK_UN);
15              
16             use Exporter ();
17             use vars qw(@ISA @EXPORT);
18             @ISA = qw(NOTEDB Exporter);
19              
20              
21              
22              
23              
24             sub new {
25             my($this, %param) = @_;
26              
27             my $class = ref($this) || $this;
28             my $self = {};
29             bless($self,$class);
30              
31             $self->{dbname} = $param{dbname} || File::Spec->catfile($ENV{HOME}, ".notedb");
32              
33             $self->{mtime} = $self->get_stat();
34             $self->{unread} = 1;
35             $self->{data} = {};
36             $self->{LOCKFILE} = $param{dbname} . "~LOCK";
37             $self->{keepkey} = 0;
38              
39             return $self;
40             }
41              
42              
43             sub DESTROY {
44             # clean the desk!
45             }
46              
47             sub version {
48             my $this = shift;
49             return $NOTEDB::pwsafe3::VERSION;
50             }
51              
52             sub get_stat {
53             my ($this) = @_;
54             if(-e $this->{dbname}) {
55             return (stat($this->{dbname}))[9];
56             }
57             else {
58             return time;
59             }
60             }
61              
62             sub filechanged {
63             my ($this) = @_;
64             my $current = $this->get_stat();
65              
66             if ($current > $this->{mtime}) {
67             $this->{mtime} = $current;
68             return $current;
69             }
70             else {
71             return 0;
72             }
73             }
74              
75             sub set_del_all {
76             my $this = shift;
77             unlink $this->{dbname};
78             open(TT,">$this->{dbname}") or die "Could not create $this->{dbname}: $!\n";
79             close (TT);
80             }
81              
82              
83             sub get_single {
84             my($this, $num) = @_;
85             my($address, $note, $date, $n, $t, $buffer, );
86              
87             my %data = $this->get_all();
88              
89             return ($data{$num}->{note}, $data{$num}->{date});
90             }
91              
92              
93             sub get_all {
94             my $this = shift;
95             my($num, $note, $date, %res);
96             if ($this->unchanged) {
97             return %{$this->{cache}};
98             }
99              
100             my %data = $this->_retrieve();
101              
102             foreach my $num (keys %data) {
103             ($res{$num}->{date}, $res{$num}->{note}) = $this->_pwsafe3tonote($data{$num}->{note});
104             }
105              
106             $this->cache(%res);
107             return %res;
108             }
109              
110             sub import_data {
111             my ($this, $data) = @_;
112              
113             my $fh;
114              
115             if (-s $this->{dbname}) {
116             $fh = new FileHandle "<$this->{dbname}" or die "could not open $this->{dbname}\n";
117             flock $fh, LOCK_EX;
118             }
119              
120             my $key = $this->_getpass();
121              
122             eval {
123             my $vault = new Crypt::PWSafe3(password => $key, file => $this->{dbname});
124              
125             foreach my $num (keys %{$data}) {
126             my $checksum = $this->get_nextnum();
127             my %record = $this->_notetopwsafe3($checksum, $data->{$num}->{note}, $data->{$num}->{date});
128              
129             my $rec = new Crypt::PWSafe3::Record();
130             $rec->uuid($record{uuid});
131             $vault->addrecord($rec);
132             $vault->modifyrecord($record{uuid}, %record);
133             }
134              
135             $vault->save();
136             };
137             if ($@) {
138             print "Exception caught:\n$@\n";
139             exit 1;
140             }
141              
142             eval {
143             flock $fh, LOCK_UN;
144             $fh->close();
145             };
146              
147             $this->{keepkey} = 0;
148             $this->{key} = 0;
149             }
150              
151             sub get_nextnum {
152             my $this = shift;
153             my($num, $te, $me, $buffer);
154              
155             my $ug = new Data::UUID;
156              
157             $this->{nextuuid} = unpack('H*', $ug->create());
158             $num = $this->_uuid( $this->{nextuuid} );
159              
160             return $num;
161             }
162              
163             sub get_search {
164             my($this, $searchstring) = @_;
165             my($buffer, $num, $note, $date, %res, $t, $n, $match);
166              
167             my $regex = $this->generate_search($searchstring);
168             eval $regex;
169             if ($@) {
170             print "invalid expression: \"$searchstring\"!\n";
171             return;
172             }
173             $match = 0;
174              
175             if ($this->unchanged) {
176             foreach my $num (keys %{$this->{cache}}) {
177             $_ = $this->{cache}{$num}->{note};
178             eval $regex;
179             if ($match) {
180             $res{$num}->{note} = $this->{cache}{$num}->{note};
181             $res{$num}->{date} = $this->{cache}{$num}->{date}
182             }
183             $match = 0;
184             }
185             return %res;
186             }
187              
188             my %data = $this->get_all();
189              
190             foreach my $num(sort keys %data) {
191             $_ = $data{$num}->{note};
192             eval $regex;
193             if($match)
194             {
195             $res{$num}->{note} = $data{$num}->{note};
196             $res{$num}->{date} = $data{$num}->{data};
197             }
198             $match = 0;
199             }
200              
201             return %res;
202             }
203              
204              
205              
206              
207             sub set_edit {
208             my($this, $num, $note, $date) = @_;
209              
210             my %data = $this->_retrieve();
211              
212             my %record = $this->_notetopwsafe3($num, $note, $date);
213              
214             if (exists $data{$num}) {
215             $data{$num}->{note} = \%record;
216             $this->_store(\%record);
217             }
218             else {
219             %record = $this->_store(\%record, 1);
220             }
221              
222             $this->changed;
223             }
224              
225              
226             sub set_new {
227             my($this, $num, $note, $date) = @_;
228             $this->set_edit($num, $note, $date);
229             }
230              
231              
232             sub set_del {
233             my($this, $num) = @_;
234              
235             my $uuid = $this->_getuuid($num);
236             if(! $uuid) {
237             print "Note $num does not exist!\n";
238             return;
239             }
240              
241             my $fh = new FileHandle "<$this->{dbname}" or die "could not open $this->{dbname}\n";
242             flock $fh, LOCK_EX;
243              
244             my $key = $this->_getpass();
245             eval {
246             my $vault = new Crypt::PWSafe3(password => $key, file => $this->{dbname});
247             delete $vault->{record}->{$uuid};
248             $vault->markmodified();
249             $vault->save();
250             };
251             if ($@) {
252             print "Exception caught:\n$@\n";
253             exit 1;
254             }
255              
256             eval {
257             flock $fh, LOCK_UN;
258             $fh->close();
259             };
260              
261             # finally re-read the db, so that we always have the latest data
262             $this->_retrieve($key);
263             $this->changed;
264             return;
265             }
266              
267             sub set_recountnums {
268             my($this) = @_;
269             # unsupported
270             return;
271             }
272              
273              
274             sub _store {
275             my ($this, $record, $create) = @_;
276              
277             my $fh;
278              
279             if (-s $this->{dbname}) {
280             $fh = new FileHandle "<$this->{dbname}" or die "could not open $this->{dbname}\n";
281             flock $fh, LOCK_EX;
282             }
283              
284             my $key;
285             my $prompt = "pwsafe password: ";
286              
287             foreach my $try (1..5) {
288             if($try > 1) {
289             $prompt = "pwsafe password ($try retry): ";
290             }
291             $key = $this->_getpass($prompt);
292             eval {
293             my $vault = new Crypt::PWSafe3(password => $key, file => $this->{dbname});
294             if ($create) {
295             my $rec = new Crypt::PWSafe3::Record();
296             $rec->uuid($record->{uuid});
297             $vault->addrecord($rec);
298             $vault->modifyrecord($record->{uuid}, %{$record});
299             }
300             else {
301             $vault->modifyrecord($record->{uuid}, %{$record});
302             }
303             $vault->save();
304             };
305             if ($@) {
306             if($@ =~ /wrong pass/i) {
307             $key = '';
308             next;
309             }
310             else {
311             print "Exception caught:\n$@\n";
312             exit 1;
313             }
314             }
315             else {
316             last;
317             }
318             }
319             eval {
320             flock $fh, LOCK_UN;
321             $fh->close();
322             };
323              
324             if(!$key) {
325             print STDERR "Giving up after 5 failed password attempts.\n";
326             exit 1;
327             }
328              
329             # finally re-read the db, so that we always have the latest data
330             $this->_retrieve($key);
331             }
332              
333             sub _retrieve {
334             my ($this, $key) = @_;
335             my $file = $this->{dbname};
336             if (-s $file) {
337             if ($this->filechanged() || $this->{unread}) {
338             my %data;
339             if (! $key) {
340             $key = $this->_getpass();
341             }
342             eval {
343             my $vault = new Crypt::PWSafe3(password => $key, file => $this->{dbname});
344              
345             my @records = $vault->getrecords();
346              
347             foreach my $record (sort { $a->ctime <=> $b->ctime } @records) {
348             my $num = $this->_uuid( $record->uuid );
349             my %entry = (
350             uuid => $record->uuid,
351             title => $record->title,
352             user => $record->user,
353             passwd => $record->passwd,
354             notes => $record->notes,
355             group => $record->group,
356             lastmod=> $record->lastmod,
357             ctime => $record->ctime,
358             );
359             $data{$num}->{note} = \%entry;
360             }
361             };
362             if ($@) {
363             print "Exception caught:\n$@\n";
364             exit 1;
365             }
366              
367             $this->{unread} = 0;
368             $this->{data} = \%data;
369             return %data;
370             }
371             else {
372             return %{$this->{data}};
373             }
374             }
375             else {
376             return ();
377             }
378             }
379              
380             sub _pwsafe3tonote {
381             #
382             # convert pwsafe3 record to note record
383             my ($this, $record) = @_;
384             my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($record->{ctime});
385             my $date = sprintf("%02d.%02d.%04d %02d:%02d:%02d", $mday, $mon+1, $year+1900, $hour, $min, $sec);
386             chomp $date;
387             my $note;
388             if ($record->{group}) {
389             my $group = $record->{group};
390             # convert group separator
391             $group =~ s#\.#/#g;
392             $note = "/$group/\n";
393             }
394              
395             # pwsafe3 uses windows newlines, so convert ours
396             $record->{notes} =~ s/\r\n/\n/gs;
397              
398             #
399             # we do NOT add user and password fields here extra
400             # because if it is contained in the note, from were
401             # it was extracted initially, where it remains anyway
402             $note .= "$record->{title}\n$record->{notes}";
403              
404             return ($date, $note);
405             }
406              
407             sub _notetopwsafe3 {
408             #
409             # convert note record to pwsafe3 record
410             # only used on create or save
411             #
412             # this one is the critical part, because the two
413             # record types are fundamentally incompatible.
414             # we parse our record and try to guess the values
415             # required for pwsafe3
416             #
417             # expected input for note:
418             # /path/ -> group, optional
419             # any text -> title
420             # User: xxx -> user
421             # Password: xxx -> passwd
422             # anything else -> notes
423             #
424             # expected input for date:
425             # 23.02.2010 07:56:27
426             my ($this, $num, $text, $date) = @_;
427             my ($group, $title, $user, $passwd, $notes, $ts, $content);
428             if ($text =~ /^\//) {
429             ($group, $title, $content) = split /\n/, $text, 3;
430             }
431             else {
432             ($title, $content) = split /\n/, $text, 2;
433             }
434              
435             if(!defined $content) { $content = ""; }
436             if(!defined $group) { $group = ""; }
437              
438             $user = $passwd = '';
439             if ($content =~ /(user|username|login|account|benutzer):\s*(.+)/i) {
440             $user = $2;
441             }
442             if ($content =~ /(password|pass|passwd|kennwort|pw):\s*(.+)/i) {
443             $passwd = $2;
444             }
445              
446             # 1 2 3 4 5 6
447             if ($date =~ /^(\d\d)\.(\d\d)\.(\d{4}) (\d\d):(\d\d):(\d\d)$/) {
448             # timelocal($sec,$min,$hour,$mday,$mon,$year);
449             $ts = timelocal($6, $5, $4, $1, $2-1, $3-1900);
450             }
451              
452             # make our topics pwsafe3 compatible groups
453             $group =~ s#^/##;
454             $group =~ s#/$##;
455             $group =~ s#/#.#g;
456              
457             # pwsafe3 uses windows newlines, so convert ours
458             $content =~ s/\n/\r\n/gs;
459             my %record = (
460             uuid => $this->_getuuid($num),
461             user => $user,
462             passwd => $passwd,
463             group => $group,
464             title => $title,
465             ctime => $ts,
466             lastmod=> $ts,
467             notes => $content,
468             );
469             return %record;
470             }
471              
472             sub _uuid {
473             my ($this, $uuid) = @_;
474             if (exists $this->{uuidnum}->{$uuid}) {
475             return $this->{uuidnum}->{$uuid};
476             }
477              
478             my $max = 0;
479              
480             if (exists $this->{numuuid}) {
481             $max = (sort { $b <=> $a } keys %{$this->{numuuid}})[0];
482             }
483              
484             my $num = $max + 1;
485              
486             $this->{uuidnum}->{$uuid} = $num;
487             $this->{numuuid}->{$num} = $uuid;
488              
489             return $num;
490             }
491              
492             sub _getuuid {
493             my ($this, $num) = @_;
494             return $this->{numuuid}->{$num};
495             }
496              
497             sub _getpass {
498             #
499             # We're doing this here ourselfes
500             # because the note way of handling encryption
501             # doesn't work with pwsafe3, we can't hold a cipher
502             # structure in memory, because pwsafe3 handles this
503             # itself.
504             # Instead we ask for the password everytime we want
505             # to fetch data from the actual file OR want to write
506             # to it. To minimize reads, we use caching by default.
507             my($this, $prompt) = @_;
508              
509             if ($this->{key}) {
510             return $this->{key};
511             }
512             else {
513             my $key;
514             print STDERR $prompt ? $prompt : "pwsafe password: ";
515             eval {
516             local($|) = 1;
517             local(*TTY);
518             open(TTY,"/dev/tty") or die "No /dev/tty!";
519             system ("stty -echo
520             chomp($key = );
521             print STDERR "\r\n";
522             system ("stty echo
523             close(TTY);
524             };
525             if ($@) {
526             $key = <>;
527             }
528             if ($this->{keepkey}) {
529             $this->{key} = $key;
530             }
531             return $key;
532             }
533             }
534              
535             1; # keep this!
536              
537             __END__