File Coverage

blib/lib/App/iTan/Utils.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             # ================================================================
2             package App::iTan::Utils;
3             # ================================================================
4 1     1   1325 use utf8;
  1         2  
  1         13  
5 1     1   463 use Moose::Role;
  0            
  0            
6             use MooseX::App::Role;
7             use 5.0100;
8              
9             use Path::Class;
10             use Params::Coerce;
11             use MooseX::Types::Path::Class;
12             use File::HomeDir;
13              
14             use Term::ReadKey;
15             use DBI;
16             use Crypt::Twofish;
17             use DateTime;
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             my ($self) = @_;
99              
100             unless ( -e -f $self->database->stringify ) {
101             $self->database->touch
102             }
103              
104             my $dbh = DBI->connect("dbi:SQLite:dbname=" .$self->database->stringify,"","",{
105             RaiseError => 1,
106             }) or die "ERROR: Cannot connect: " . $DBI::errstr;
107              
108              
109             my @list;
110             my $sth = $dbh->prepare('SELECT name
111             FROM sqlite_master
112             WHERE type=?
113             ORDER BY name');
114             $sth->execute('table');
115             while (my $name = $sth->fetchrow_array) {
116             push @list,$name;
117             }
118             $sth->finish();
119            
120             unless ( grep { $_ eq 'itan' } @list ) {
121             say "Initializing iTAN database ...";
122              
123             my $password = $self->_get_password();
124             $self->cipher(Crypt::Twofish->new($password));
125             my $crypted = $self->crypt_string($password);
126            
127             $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             $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             my $sth = $dbh->prepare(q[INSERT INTO system (name,value) VALUES (?,?)]);
146             $sth->execute('password',$crypted);
147             $sth->execute('version',$App::iTan::VERSION);
148             $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             return $dbh;
158             }
159              
160             sub _build_cipher {
161             my ($self) = @_;
162              
163             my $password = $self->_get_password();
164            
165             my $cipher = Crypt::Twofish->new($password);
166            
167             $self->cipher($cipher);
168            
169             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             unless ( $self->decrypt_string($stored_password) eq $password) {
173             die "ERROR: Invalid password";
174             }
175            
176             return $cipher;
177             }
178              
179             sub _parse_date {
180             my ( $self, $date ) = @_;
181              
182             return
183             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             year => $+{year},
197             month => $+{month},
198             day => $+{day},
199             hour => $+{hour},
200             minute => $+{minute},
201             );
202             }
203              
204             sub crypt_string {
205             my ( $self, $string ) = @_;
206              
207             use bytes;
208             while (1) {
209             last if length($string) % 16 == 0;
210             $string .= ' ';
211             }
212              
213             return $self->cipher->encrypt($string);
214             }
215              
216             sub decrypt_string {
217             my ( $self, $data ) = @_;
218              
219             my $tan = $self->cipher->decrypt($data);
220             $tan =~ s/\s+//g;
221             return $tan;
222             }
223              
224             sub _date {
225             return DateTime->now->format_cldr('yyyy/MM/dd HH:mm');
226             }
227              
228             sub _get_password {
229             my $password;
230              
231             ReadMode 2;
232             say 'Please enter your password:';
233             while ( not defined( $password = ReadLine(-1) ) ) {
234             # no key pressed yet
235             }
236             ReadMode 0;
237             chomp($password);
238              
239             my $length;
240             {
241             use bytes;
242             $length = length $password;
243             }
244            
245             if ($length == 16) {
246             # ok
247             } elsif ($length < 4) {
248             die('ERROR: Password is too short (Min 4 bytes required)');
249             } elsif ($length > 16) {
250             die('ERROR: Password is too long (Max 16 bytes allowed)');
251             } else {
252             while (1) {
253             $password .= '0';
254             last
255             if length $password == 16;
256             }
257             }
258            
259             return $password;
260             }
261              
262             sub get {
263             my ($self,$index) = @_;
264            
265             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             $sth->execute($index)
276             or die "ERROR: Cannot execute: " . $sth->errstr();
277            
278             my $data = $sth->fetchrow_hashref();
279            
280             unless (defined $data) {
281             die "ERROR: Could not find iTAN ".$index;
282             }
283              
284             $data->{imported} = $self->_parse_date($data->{imported});
285             $data->{used} = $self->_parse_date($data->{used});
286             #$data->{itan} = $self->decrypt_tan($data->{itan});
287            
288             return $data;
289             }
290              
291             sub mark {
292             my ($self,$index,$memo) = @_;
293            
294             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             $sth->execute($self->_date,$memo,$index)
299             or die "ERROR: Cannot execute: " . $sth->errstr();
300            
301             $sth->finish();
302            
303             return 1;
304             }
305              
306             1;