File Coverage

blib/lib/Authen/Bitcard.pm
Criterion Covered Total %
statement 117 172 68.0
branch 23 56 41.0
condition 5 18 27.7
subroutine 28 36 77.7
pod 18 18 100.0
total 191 300 63.6


line stmt bran cond sub pod time code
1             package Authen::Bitcard;
2             BEGIN {
3 2     2   1856 $Authen::Bitcard::VERSION = '0.90';
4             }
5 2     2   17 use strict;
  2         4  
  2         63  
6 2     2   9 use base qw( Class::ErrorHandler );
  2         4  
  2         2140  
7              
8 2     2   17070 use Math::BigInt;
  2         29869  
  2         20  
9 2     2   31500 use MIME::Base64 qw( decode_base64 );
  2         1923  
  2         188  
10 2     2   4954 use Digest::SHA qw( sha1 sha1_hex );
  2         10858  
  2         384  
11 2     2   3289 use LWP::UserAgent;
  2         132780  
  2         2971  
12 2     2   356 use HTTP::Status qw( RC_NOT_MODIFIED );
  2         6  
  2         2959  
13 2     2   15 use URI;
  2         5  
  2         155  
14 2     2   2552 use URI::QueryParam;
  2         2482  
  2         65  
15 2     2   16 use Carp qw(croak);
  2         5  
  2         119  
16 2     2   14 use JSON qw(decode_json);
  2         5  
  2         20  
17              
18             sub new {
19 2     2 1 1106 my $class = shift;
20 2         9 my $bc = bless { }, $class;
21 2         9 $bc->skip_expiry_check(0);
22 2         8 $bc->expires(600);
23 2         8 $bc->bitcard_url('https://www.bitcard.org/');
24 2         14 $bc->version(4);
25 2         8 $bc->token('');
26 2         6 my %args = @_;
27 2         8 for my $k (keys %args) {
28 1 50       12 next unless $bc->can($k);
29 1         5 $bc->$k($args{$k});
30             }
31 2         16 $bc;
32             }
33              
34             sub _var {
35 54     54   74 my $bc = shift;
36 54         136 my $var = shift;
37 54 100       154 $bc->{$var} = shift if @_;
38 54         183 $bc->{$var};
39             }
40              
41 3     3 1 27 sub key_cache { shift->_var('key_cache', @_) }
42 5     5 1 22 sub skip_expiry_check { shift->_var('skip_expiry_check', @_) }
43 2     2 1 10 sub expires { shift->_var('expires', @_) }
44 10     10 1 29 sub token { shift->_var('token', @_) }
45 0     0 1 0 sub api_secret { shift->_var('api_secret', @_) }
46 9     9 1 105 sub version { shift->_var('version', @_) }
47 0     0 1 0 sub ua { shift->_var('ua', @_) }
48 8     8 1 23 sub bitcard_url { shift->_var('bitcard_url', @_) }
49 8     8 1 15 sub info_optional { shift->_var('io', @_) }
50 9     9 1 1511 sub info_required { shift->_var('ir', @_) }
51              
52             sub _url {
53 4     4   11 my ($bc, $url) = (shift, shift);
54 4 50 66     54 my $args = ($_[0] && ref $_[0]) ? $_[0] : { @_ };
55 4         30 $args->{"bc_$_"} = delete $args->{$_} for keys %$args;
56 4         17 $args->{bc_t} = $bc->token;
57 4         14 $args->{bc_v} = $bc->version;
58 4 50       16 $args->{bc_io} = ref $bc->info_optional ? join ",", @{$bc->info_optional} : $bc->info_optional;
  0         0  
59 4 50       14 $args->{bc_ir} = ref $bc->info_required ? join ",", @{$bc->info_required} : $bc->info_required;
  0         0  
60 4 50       20 delete $args->{bc_io} unless $args->{bc_io};
61 4 100       16 delete $args->{bc_ir} unless $args->{bc_ir};
62 4         13 my $base = $bc->bitcard_url;
63 4 50       25 $base = "$base/" unless $base =~ m!/$!;
64 4         30 my $uri = URI->new($base . $url);
65 4 100       13857 unless ($url =~ m/regkey.txt/) {
66 1 50       7 if ($url =~ m!^api/!) {
67 0 0       0 croak "Bitcard API Secret required for API calls" unless $bc->api_secret;
68 0         0 $args->{bc_ts} = time;
69 0         0 my @fields = sort keys %$args;
70 0         0 $args->{bc_fields} = join ",", @fields, 'bc_fields';
71 0         0 my $string = join "::", (map { "$args->{$_}" } @fields, 'bc_fields'), $bc->api_secret;
  0         0  
72 0         0 warn "ST: $string";
73 0         0 $args->{bc_sig} = sha1_hex($string);
74             }
75 1         15 $uri->query_form_hash($args);
76             }
77 4         349 $uri->as_string;
78             }
79              
80             sub key_url{
81 3     3 1 11 shift->_url("regkey.txt");
82             }
83              
84             sub login_url {
85 1     1 1 6 shift->_url('login', @_)
86             }
87              
88             sub logout_url {
89 0     0 1 0 shift->_url('logout', @_)
90             }
91              
92             sub account_url {
93 0     0 1 0 shift->_url('account', @_)
94             }
95              
96             sub register_url {
97 0     0 1 0 shift->_url('register', @_)
98             }
99              
100             sub _api_url {
101 0     0   0 my ($self, $method) = (shift, shift);
102 0         0 $self->_url("api/$method", @_);
103             }
104              
105              
106             sub verify {
107 2     2 1 20495 my $bc = shift;
108 2         5 my %data;
109             my $fields;
110 2 50       11 if (@_ == 1) {
111 2         3 my $q = $_[0];
112 2 50       10 if (ref $q eq 'HASH') {
113 2   50     16 $fields = $_[0]->{bc_fields} || '';
114 2         9 %data = map { $_ => $_[0]->{$_} } grep { defined $_[0]->{$_} } split(/,/, $fields), 'bc_sig';
  11         30  
  11         25  
115             }
116             else {
117 0   0     0 $fields = $q->param('bc_fields') || '';
118 0         0 %data = map { $_ => $q->param($_) } grep { defined $q->param($_) } split(/,/, $fields), 'bc_sig';
  0         0  
  0         0  
119             }
120             }
121             else {
122             ## Later we could process arguments passed in a hash.
123 0         0 return $bc->error("usage: verify(\$query)");
124             }
125              
126             #warn Data::Dumper->Dump([\%data], [qw(data)]);
127              
128 2         11 for ($data{bc_email}, $data{bc_sig}) {
129 4 100       16 defined $_ and tr/ /+/;
130             }
131 2 50 0     10 return $bc->error("Bitcard data has expired")
      33        
132             unless $bc->skip_expiry_check or ($data{bc_ts}||0) + $bc->expires >= time;
133              
134 2 50       13 my $key = $bc->_fetch_key($bc->key_url) or return;
135 2         507 my($r, $s) = split /:/, $data{bc_sig};
136 2         4 my $sig = {};
137 2         29 $sig->{r} = Math::BigInt->new("0b" . unpack("B*", decode_base64($r)));
138 2         1082 $sig->{s} = Math::BigInt->new("0b" . unpack("B*", decode_base64($s)));
139 2 50       741 my $msg = join '::', (map { $data{$_} || '' } split /,/, $data{bc_fields} ), $bc->token;
  9         31  
140 2 50       10 unless ($bc->_verify($msg, $key, $sig)) {
141 0         0 return $bc->error("Bitcard signature verification failed");
142             }
143              
144 2         126 for my $k (keys %data) {
145 12         19 my $nk = $k;
146 12         38 $nk =~ s/^bc_//;
147 12         40 $data{$nk} = delete $data{$k};
148             }
149              
150 2 50       14 if ($bc->version >= 4) {
151 0 0       0 unless ($data{version} == $bc->version) {
152 0         0 $data{version} =~ s/\D//g;
153 0         0 return $bc->error(sprintf "Expected Bitcard protocol version [%i], got version [%i].", $bc->version, $data{version});
154             }
155              
156 0 0       0 unless ($data{confirmed}) {
157 0         0 return $bc->error('Account not confirmed');
158             }
159             }
160              
161 2         39 \%data;
162             }
163              
164             sub _verify {
165 2     2   3 my $bc = shift;
166 2         3 my($msg, $key, $sig) = @_;
167 2         59 my $u1 = Math::BigInt->new("0b" . unpack("B*", sha1($msg)));
168 2         759 $sig->{s}->bmodinv($key->{q});
169 2         20315 $u1 = ($u1 * $sig->{s}) % $key->{q};
170 2         1121 $sig->{s} = ($sig->{r} * $sig->{s}) % $key->{q};
171 2         1051 $key->{g}->bmodpow($u1, $key->{p});
172 2         1140223 $key->{pub_key}->bmodpow($sig->{s}, $key->{p});
173 2         920586 $u1 = ($key->{g} * $key->{pub_key}) % $key->{p};
174 2         3731 $u1 %= $key->{q};
175 2         1108 $u1 == $sig->{r};
176             }
177              
178             sub _get_ua {
179 0 0   0   0 shift->ua || LWP::UserAgent->new;
180             }
181              
182             sub _fetch_key {
183 2     2   29 my $bc = shift;
184 2         4 my($uri) = @_;
185 2         7 my $cache = $bc->key_cache;
186             ## If it's a callback, call it and return the return value.
187 2 50 33     23 return $cache->($bc, $uri) if $cache && ref($cache) eq 'CODE';
188             ## Otherwise, load the key.
189 0           my $data;
190 0           my $ua = $bc->_get_ua;
191 0 0         if ($cache) {
192 0           my $res = $ua->mirror($uri, $cache);
193 0 0 0       return $bc->error("Failed to fetch key: " . $res->status_line)
194             unless $res->is_success || $res->code == RC_NOT_MODIFIED;
195 0 0         open my $fh, $cache
196             or return $bc->error("Can't open $cache: $!");
197 0           $data = do { local $/; <$fh> };
  0            
  0            
198 0           close $fh;
199             } else {
200 0           my $res = $ua->get($uri);
201 0 0         return $bc->error("Failed to fetch key: " . $res->status_line)
202             unless $res->is_success;
203 0           $data = $res->content;
204             }
205 0           chomp $data;
206 0           my $key = {};
207 0           for my $f (split /\s+/, $data) {
208 0           my($k, $v) = split /=/, $f, 2;
209 0           $key->{$k} = Math::BigInt->new($v);
210             }
211 0           $key;
212             }
213              
214             sub add_invite {
215 0     0 1   my $self = shift;
216 0           my $url = $self->_api_url('invite/add_invite', @_);
217 0           warn "URL: $url\n";
218 0           my $res = $self->_get_ua->get($url);
219 0 0         return $self->error("Failed to retrive invitation code: " . $res->status_line)
220             unless $res->is_success;
221 0           my $data = decode_json($res->content);
222 0           $data;
223             }
224              
225             1;
226             __END__