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.07";
7 1     1   1550 use strict;
  1         3  
  1         54  
8 1     1   7 use Data::Dumper;
  1         1  
  1         82  
9 1     1   733 use Time::Local;
  1         2012  
  1         84  
10 1     1   318 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 = $this->_getpass();
285             eval {
286             my $vault = new Crypt::PWSafe3(password => $key, file => $this->{dbname});
287             if ($create) {
288             my $rec = new Crypt::PWSafe3::Record();
289             $rec->uuid($record->{uuid});
290             $vault->addrecord($rec);
291             $vault->modifyrecord($record->{uuid}, %{$record});
292             }
293             else {
294             $vault->modifyrecord($record->{uuid}, %{$record});
295             }
296             $vault->save();
297             };
298             if ($@) {
299             print "Exception caught:\n$@\n";
300             exit 1;
301             }
302              
303             eval {
304             flock $fh, LOCK_UN;
305             $fh->close();
306             };
307              
308             # finally re-read the db, so that we always have the latest data
309             $this->_retrieve($key);
310             }
311              
312             sub _retrieve {
313             my ($this, $key) = @_;
314             my $file = $this->{dbname};
315             if (-s $file) {
316             if ($this->filechanged() || $this->{unread}) {
317             my %data;
318             if (! $key) {
319             $key = $this->_getpass();
320             }
321             eval {
322             my $vault = new Crypt::PWSafe3(password => $key, file => $this->{dbname});
323              
324             my @records = $vault->getrecords();
325              
326             foreach my $record (sort { $a->ctime <=> $b->ctime } @records) {
327             my $num = $this->_uuid( $record->uuid );
328             my %entry = (
329             uuid => $record->uuid,
330             title => $record->title,
331             user => $record->user,
332             passwd => $record->passwd,
333             notes => $record->notes,
334             group => $record->group,
335             lastmod=> $record->lastmod,
336             ctime => $record->ctime,
337             );
338             $data{$num}->{note} = \%entry;
339             }
340             };
341             if ($@) {
342             print "Exception caught:\n$@\n";
343             exit 1;
344             }
345              
346             $this->{unread} = 0;
347             $this->{data} = \%data;
348             return %data;
349             }
350             else {
351             return %{$this->{data}};
352             }
353             }
354             else {
355             return ();
356             }
357             }
358              
359             sub _pwsafe3tonote {
360             #
361             # convert pwsafe3 record to note record
362             my ($this, $record) = @_;
363             my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($record->{ctime});
364             my $date = sprintf("%02d.%02d.%04d %02d:%02d:%02d", $mday, $mon+1, $year+1900, $hour, $min, $sec);
365             chomp $date;
366             my $note;
367             if ($record->{group}) {
368             my $group = $record->{group};
369             # convert group separator
370             $group =~ s#\.#/#g;
371             $note = "/$group/\n";
372             }
373              
374             # pwsafe3 uses windows newlines, so convert ours
375             $record->{notes} =~ s/\r\n/\n/gs;
376              
377             #
378             # we do NOT add user and password fields here extra
379             # because if it is contained in the note, from were
380             # it was extracted initially, where it remains anyway
381             $note .= "$record->{title}\n$record->{notes}";
382              
383             return ($date, $note);
384             }
385              
386             sub _notetopwsafe3 {
387             #
388             # convert note record to pwsafe3 record
389             # only used on create or save
390             #
391             # this one is the critical part, because the two
392             # record types are fundamentally incompatible.
393             # we parse our record and try to guess the values
394             # required for pwsafe3
395             #
396             # expected input for note:
397             # /path/ -> group, optional
398             # any text -> title
399             # User: xxx -> user
400             # Password: xxx -> passwd
401             # anything else -> notes
402             #
403             # expected input for date:
404             # 23.02.2010 07:56:27
405             my ($this, $num, $text, $date) = @_;
406             my ($group, $title, $user, $passwd, $notes, $ts, $content);
407             if ($text =~ /^\//) {
408             ($group, $title, $content) = split /\n/, $text, 3;
409             }
410             else {
411             ($title, $content) = split /\n/, $text, 2;
412             }
413              
414             if(!defined $content) { $content = ""; }
415             if(!defined $group) { $group = ""; }
416              
417             $user = $passwd = '';
418             if ($content =~ /(user|username|login|account|benutzer):\s*(.+)/i) {
419             $user = $2;
420             }
421             if ($content =~ /(password|pass|passwd|kennwort|pw):\s*(.+)/i) {
422             $passwd = $2;
423             }
424              
425             # 1 2 3 4 5 6
426             if ($date =~ /^(\d\d)\.(\d\d)\.(\d{4}) (\d\d):(\d\d):(\d\d)$/) {
427             # timelocal($sec,$min,$hour,$mday,$mon,$year);
428             $ts = timelocal($6, $5, $4, $1, $2-1, $3-1900);
429             }
430              
431             # make our topics pwsafe3 compatible groups
432             $group =~ s#^/##;
433             $group =~ s#/$##;
434             $group =~ s#/#.#g;
435              
436             # pwsafe3 uses windows newlines, so convert ours
437             $content =~ s/\n/\r\n/gs;
438             my %record = (
439             uuid => $this->_getuuid($num),
440             user => $user,
441             passwd => $passwd,
442             group => $group,
443             title => $title,
444             ctime => $ts,
445             lastmod=> $ts,
446             notes => $content,
447             );
448             return %record;
449             }
450              
451             sub _uuid {
452             my ($this, $uuid) = @_;
453             if (exists $this->{uuidnum}->{$uuid}) {
454             return $this->{uuidnum}->{$uuid};
455             }
456              
457             my $max = 0;
458              
459             if (exists $this->{numuuid}) {
460             $max = (sort { $b <=> $a } keys %{$this->{numuuid}})[0];
461             }
462              
463             my $num = $max + 1;
464              
465             $this->{uuidnum}->{$uuid} = $num;
466             $this->{numuuid}->{$num} = $uuid;
467              
468             return $num;
469             }
470              
471             sub _getuuid {
472             my ($this, $num) = @_;
473             return $this->{numuuid}->{$num};
474             }
475              
476             sub _getpass {
477             #
478             # We're doing this here ourselfes
479             # because the note way of handling encryption
480             # doesn't work with pwsafe3, we can't hold a cipher
481             # structure in memory, because pwsafe3 handles this
482             # itself.
483             # Instead we ask for the password everytime we want
484             # to fetch data from the actual file OR want to write
485             # to it. To minimize reads, we use caching by default.
486             my($this) = @_;
487              
488             if ($this->{key}) {
489             return $this->{key};
490             }
491             else {
492             my $key;
493             print STDERR "pwsafe password: ";
494             eval {
495             local($|) = 1;
496             local(*TTY);
497             open(TTY,"/dev/tty") or die "No /dev/tty!";
498             system ("stty -echo
499             chomp($key = );
500             print STDERR "\r\n";
501             system ("stty echo
502             close(TTY);
503             };
504             if ($@) {
505             $key = <>;
506             }
507             if ($this->{keepkey}) {
508             $this->{key} = $key;
509             }
510             return $key;
511             }
512             }
513              
514             1; # keep this!
515              
516             __END__