File Coverage

blib/lib/App/iTan/Utils.pm
Criterion Covered Total %
statement 44 118 37.2
branch 0 36 0.0
condition 0 3 0.0
subroutine 15 24 62.5
pod 4 4 100.0
total 63 185 34.0


line stmt bran cond sub pod time code
1             # ================================================================
2             package App::iTan::Utils;
3             # ================================================================
4 1     1   1077 use utf8;
  1         1  
  1         6  
5 1     1   32 use Moose::Role;
  1         2  
  1         7  
6 1     1   4664 use MooseX::App::Role;
  1         1472  
  1         4  
7 1     1   3630 use 5.0100;
  1         3  
8              
9 1     1   5 use Path::Class;
  1         2  
  1         99  
10 1     1   582 use Params::Coerce;
  1         1272  
  1         6  
11 1     1   535 use MooseX::Types::Path::Class;
  1         251401  
  1         9  
12 1     1   1542 use File::HomeDir;
  1         5107  
  1         68  
13              
14 1     1   566 use Term::ReadKey;
  1         3874  
  1         1724  
15 1     1   1811 use DBI;
  1         14672  
  1         77  
16 1     1   633 use Crypt::Twofish;
  1         978  
  1         31  
17 1     1   953 use DateTime;
  1         78648  
  1         604  
18              
19             =head1 NAME
20              
21             App::iTan::Utils - Utility methods role
22              
23             =head1 METHODS
24              
25             =head2 Accessors
26              
27             =head3 database
28              
29             Path to the database as a L<Path::Class::File> object.
30              
31             =head3 dbh
32              
33             Active database handle
34              
35             =head3 cipher
36              
37             L<Crypt::Twofish> cipher object
38              
39             =head2 Methods
40              
41             =head3 get
42              
43             my $tandata = $self->get($index);
44              
45             Fetches a valid iTan with the given index.
46              
47             =head3 mark
48              
49             $self->mark($index[,$memo]);
50              
51             =head3 crypt_string
52              
53             my $crypt = $self->crypt_string($string);
54              
55             Encrpyts a string
56              
57             =head3 decrypt_string
58              
59             my $string = $self->decrypt_string($crypt);
60              
61             Decrpyts a string
62              
63             =cut
64              
65             option 'database' => (
66             is => 'ro',
67             isa => 'Path::Class::File',
68             required => 1,
69             coerce => 1,
70             documentation => q[Path to the iTAN database file. Defaults to ~/.itan],
71             default => sub {
72             return Path::Class::File->new( File::HomeDir->my_home, '.itan' );
73             },
74             );
75              
76             has 'dbh' => (
77             is => 'ro',
78             lazy => 1,
79             isa => 'DBI::db',
80             builder => '_build_dbh'
81             );
82              
83             has 'cipher' => (
84             is => 'rw',
85             lazy => 1,
86             isa => 'Crypt::Twofish',
87             builder => '_build_cipher'
88             );
89              
90             #sub DEMOLISH {
91             # my ($self) = @_;
92             #
93             # $self->dbh->disconnect();
94             # return;
95             #}
96              
97             sub _build_dbh {
98 0     0     my ($self) = @_;
99              
100 0 0         unless ( -e -f $self->database->stringify ) {
101 0           $self->database->touch
102             }
103              
104 0 0         my $dbh = DBI->connect("dbi:SQLite:dbname=" .$self->database->stringify,"","",{
105             RaiseError => 1,
106             }) or die "ERROR: Cannot connect: " . $DBI::errstr;
107              
108              
109 0           my @list;
110 0           my $sth = $dbh->prepare('SELECT name
111             FROM sqlite_master
112             WHERE type=?
113             ORDER BY name');
114 0           $sth->execute('table');
115 0           while (my $name = $sth->fetchrow_array) {
116 0           push @list,$name;
117             }
118 0           $sth->finish();
119            
120 0 0         unless ( grep { $_ eq 'itan' } @list ) {
  0            
121 0           say "Initializing iTAN database ...";
122              
123 0           my $password = $self->_get_password();
124 0           $self->cipher(Crypt::Twofish->new($password));
125 0           my $crypted = $self->crypt_string($password);
126            
127 0 0         $dbh->do(
128             q[CREATE TABLE itan (
129             tindex INTEGER NOT NULL,
130             itan VARCHAR NOT NULL,
131             imported VARCHAR NOT NULL,
132             used VARCHAR,
133             valid VARCHAR,
134             memo VARCHAR
135             )]
136             ) or die "ERROR: Cannot execute: " . $dbh->errstr();
137            
138 0 0         $dbh->do(
139             q[CREATE TABLE system (
140             name VARCHAR NOT NULL,
141             value VARCHAR NOT NULL
142             )]
143             ) or die "ERROR: Cannot execute: " . $dbh->errstr();
144            
145 0           my $sth = $dbh->prepare(q[INSERT INTO system (name,value) VALUES (?,?)]);
146 0           $sth->execute('password',$crypted);
147 0           $sth->execute('version',$App::iTan::VERSION);
148 0           $sth->finish;
149             }
150              
151             # $dbh->{'csv_tables'}->{'itan'}
152             # = { 'col_names' => [ "tindex", "itan", "imported", "used", "valid", "memo" ] };
153             #
154             # $dbh->{'csv_tables'}->{'system'}
155             # = { 'col_names' => [ "name", "value" ] };
156              
157 0           return $dbh;
158             }
159              
160             sub _build_cipher {
161 0     0     my ($self) = @_;
162              
163 0           my $password = $self->_get_password();
164            
165 0           my $cipher = Crypt::Twofish->new($password);
166            
167 0           $self->cipher($cipher);
168            
169 0 0         my $stored_password = $self->dbh->selectrow_array("SELECT value FROM system WHERE name = 'password'")
170             or die "ERROR: Cannot query: " . $self->dbh->errstr();
171            
172 0 0         unless ( $self->decrypt_string($stored_password) eq $password) {
173 0           die "ERROR: Invalid password";
174             }
175            
176 0           return $cipher;
177             }
178              
179             sub _parse_date {
180 0     0     my ( $self, $date ) = @_;
181              
182             return
183 0 0 0       unless defined $date && $date =~ m/^
184             (?<year>\d{4})
185             \/
186             (?<month>\d{1,2})
187             \/
188             (?<day>\d{1,2})
189             \s
190             (?<hour>\d{1,2})
191             :
192             (?<minute>\d{1,2})
193             $/x;
194              
195             return DateTime->new(
196 1     1   507 year => $+{year},
  1         375  
  1         90  
197             month => $+{month},
198             day => $+{day},
199             hour => $+{hour},
200             minute => $+{minute},
201 0           );
202             }
203              
204             sub crypt_string {
205 0     0 1   my ( $self, $string ) = @_;
206              
207 1     1   6 use bytes;
  1         1  
  1         8  
208 0           while (1) {
209 0 0         last if length($string) % 16 == 0;
210 0           $string .= ' ';
211             }
212              
213 0           return $self->cipher->encrypt($string);
214             }
215              
216             sub decrypt_string {
217 0     0 1   my ( $self, $data ) = @_;
218              
219 0           my $tan = $self->cipher->decrypt($data);
220 0           $tan =~ s/\s+//g;
221 0           return $tan;
222             }
223              
224             sub _date {
225 0     0     return DateTime->now->format_cldr('yyyy/MM/dd HH:mm');
226             }
227              
228             sub _get_password {
229 0     0     my $password;
230              
231 0           ReadMode 2;
232 0           say 'Please enter your password:';
233 0           while ( not defined( $password = ReadLine(-1) ) ) {
234             # no key pressed yet
235             }
236 0           ReadMode 0;
237 0           chomp($password);
238              
239 0           my $length;
240             {
241 1     1   194 use bytes;
  1         2  
  1         4  
  0            
242 0           $length = length $password;
243             }
244            
245 0 0         if ($length == 16) {
    0          
    0          
246             # ok
247             } elsif ($length < 4) {
248 0           die('ERROR: Password is too short (Min 4 bytes required)');
249             } elsif ($length > 16) {
250 0           die('ERROR: Password is too long (Max 16 bytes allowed)');
251             } else {
252 0           while (1) {
253 0           $password .= '0';
254             last
255 0 0         if length $password == 16;
256             }
257             }
258            
259 0           return $password;
260             }
261              
262             sub get {
263 0     0 1   my ($self,$index) = @_;
264            
265 0 0         my $sth = $self->dbh->prepare('SELECT
266             tindex,
267             itan,
268             imported,
269             used,
270             memo
271             FROM itan
272             WHERE tindex = ?
273             AND valid = 1')
274             or die "ERROR: Cannot prepare: " . $self->dbh->errstr();
275 0 0         $sth->execute($index)
276             or die "ERROR: Cannot execute: " . $sth->errstr();
277            
278 0           my $data = $sth->fetchrow_hashref();
279            
280 0 0         unless (defined $data) {
281 0           die "ERROR: Could not find iTAN ".$index;
282             }
283              
284 0           $data->{imported} = $self->_parse_date($data->{imported});
285 0           $data->{used} = $self->_parse_date($data->{used});
286             #$data->{itan} = $self->decrypt_tan($data->{itan});
287            
288 0           return $data;
289             }
290              
291             sub mark {
292 0     0 1   my ($self,$index,$memo) = @_;
293            
294 0 0         my $sth = $self->dbh->prepare(
295             q[UPDATE itan SET used = ?,memo = ?, valid = 0 WHERE tindex = ?]
296             ) or die "ERROR: Cannot prepare: " . $self->dbh->errstr();
297            
298 0 0         $sth->execute($self->_date,$memo,$index)
299             or die "ERROR: Cannot execute: " . $sth->errstr();
300            
301 0           $sth->finish();
302            
303 0           return 1;
304             }
305              
306             1;