File Coverage

blib/lib/NOTEDB/pwsafe3.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


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