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   1425 use utf8;
  1         3  
  1         18  
5 1     1   30 use Moose::Role;
  1         2  
  1         11  
6 1     1   5151 use MooseX::App::Role;
  1         1728  
  1         4  
7 1     1   4484 use 5.0100;
  1         3  
8              
9 1     1   5 use Path::Class;
  1         2  
  1         57  
10 1     1   422 use Params::Coerce;
  1         1052  
  1         5  
11 1     1   385 use MooseX::Types::Path::Class;
  1         39238  
  1         7  
12 1     1   1339 use File::HomeDir;
  1         5575  
  1         55  
13              
14 1     1   374 use Term::ReadKey;
  1         1402  
  1         61  
15 1     1   1215 use DBI;
  1         11885  
  1         51  
16 1     1   402 use Crypt::Twofish;
  1         683  
  1         25  
17 1     1   686 use DateTime;
  1         372718  
  1         508  
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   455 year => $+{year},
  1         310  
  1         74  
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   9 use bytes;
  1         2  
  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   164 use bytes;
  1         3  
  1         5  
  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;