File Coverage

blib/lib/Blockchain/Ethereum/Keystore/File.pm
Criterion Covered Total %
statement 114 114 100.0
branch 11 18 61.1
condition 14 26 53.8
subroutine 32 32 100.0
pod 4 12 33.3
total 175 202 86.6


line stmt bran cond sub pod time code
1             package Blockchain::Ethereum::Keystore::File;
2              
3 2     2   343362 use v5.26;
  2         9  
4 2     2   12 use strict;
  2         5  
  2         55  
5 2     2   9 use warnings;
  2         3  
  2         196  
6              
7             # ABSTRACT: Ethereum keystore file abstraction
8             our $AUTHORITY = 'cpan:REFECO'; # AUTHORITY
9             our $VERSION = '0.021'; # VERSION
10              
11 2     2   10 use Carp;
  2         4  
  2         155  
12 2     2   428 use JSON::MaybeXS;
  2         7404  
  2         130  
13 2     2   981 use Crypt::PRNG;
  2         8457  
  2         123  
14 2     2   1011 use Crypt::Mode::CTR;
  2         1965  
  2         94  
15 2     2   949 use Crypt::Digest::Keccak256 qw(keccak256);
  2         4165  
  2         145  
16 2     2   17 use Scalar::Util qw(blessed);
  2         4  
  2         96  
17 2     2   938 use Data::UUID;
  2         1825  
  2         146  
18              
19 2     2   1066 use Blockchain::Ethereum::Key;
  2         10  
  2         112  
20 2     2   3419 use Blockchain::Ethereum::Keystore::KDF;
  2         8  
  2         3382  
21              
22             my $json = JSON::MaybeXS->new(
23             utf8 => 1,
24             pretty => 1,
25             canonical => 1
26             );
27              
28             sub from_key {
29 2     2 1 32 my ($class, $key) = @_;
30              
31 2 50 33     24 croak 'key must be a Blockchain::Ethereum::Key instance'
32             unless blessed $key && $key->isa('Blockchain::Ethereum::Key');
33              
34 2         9 my $self = bless {private_key => $key}, $class;
35 2         7 return $self;
36             }
37              
38             sub from_file {
39 6     6 1 320128 my ($class, $file_path, $password) = @_;
40              
41 6         23 my $self = bless {}, $class;
42              
43 6         12 my $content;
44             {
45 6 100       16 open my $fh, '<:raw', $file_path
  6         742  
46             or croak "Could not read file '$file_path': $!";
47 5         35 local $/; # Enable slurp mode
48 5         276 $content = <$fh>;
49 5         103 close $fh;
50             }
51 5         175 my $decoded = $json->decode(lc $content);
52              
53 5 50 33     68 croak 'Version not supported' unless $decoded->{version} && $decoded->{version} == 3;
54 5 50       38 croak 'Password is required to decrypt the keystore' unless defined $password;
55 5         27 $self->{password} = $password;
56              
57 5         21 return $self->_from_v3($decoded);
58             }
59              
60             sub cipher {
61 7   33 7 0 323 shift->{cipher} //= Crypt::Mode::CTR->new('AES', 1);
62             }
63              
64             sub ciphertext {
65 17     17 0 52 my $self = shift;
66 17   66     259 $self->{ciphertext} //= $self->_generate_ciphertext;
67             }
68              
69             sub mac {
70 10     10 0 30 my $self = shift;
71 10   66     97 $self->{mac} //= $self->_generate_mac;
72              
73             }
74              
75             sub version {
76 2   50 2 0 17 shift->{version} //= 3;
77             }
78              
79             sub iv {
80 11     11 0 33 my $self = shift;
81 11   66     178 $self->{iv} //= $self->_generate_random_iv;
82             }
83              
84             sub kdf {
85 40     40 0 92 my $self = shift;
86 40   66     266 $self->{kdf} //= $self->_generate_kdf;
87             }
88              
89             sub id {
90 4     4 0 99 my $self = shift;
91 4   66     38 $self->{id} //= $self->_generate_id;
92             }
93              
94             sub private_key {
95 12     12 0 1988 shift->{private_key};
96             }
97              
98             sub password {
99 21     21 1 130 shift->{password};
100             }
101              
102             sub _from_v3 {
103 5     5   14 my ($self, $object) = @_;
104              
105 5         13 my $crypto = $object->{crypto};
106              
107 5         16 $self->{ciphertext} = $crypto->{ciphertext};
108 5         14 $self->{mac} = $crypto->{mac};
109 5         15 $self->{iv} = $crypto->{cipherparams}->{iv};
110 5         14 $self->{version} = $object->{version};
111 5         17 $self->{id} = $object->{id};
112              
113 5         41 my $header = $crypto->{kdfparams};
114              
115             $self->{kdf} = Blockchain::Ethereum::Keystore::KDF->new(
116             algorithm => $crypto->{kdf}, #
117             dklen => $header->{dklen},
118             n => $header->{n},
119             p => $header->{p},
120             r => $header->{r},
121             c => $header->{c},
122             prf => $header->{prf},
123 5         84 salt => $header->{salt});
124              
125 5 50       26 $self->{private_key} = $self->_generate_private_key unless $self->private_key;
126 5         29 $self->_verify_mac;
127              
128 4         99 return $self;
129             }
130              
131             sub _verify_mac {
132 5     5   30 my ($self) = @_;
133              
134 5         22 my $computed_mac = $self->_generate_mac;
135 5         143 my $expected_mac = $self->mac;
136              
137 5 100       399 croak "Invalid password or corrupted keystore"
138             unless lc $computed_mac eq lc $expected_mac;
139             }
140              
141             sub _generate_mac {
142 8     8   27 my ($self) = @_;
143              
144 8         30 my $derived_key = $self->kdf->decode($self->password);
145 8         44 my $mac_key = substr($derived_key, 16, 16);
146              
147 8         108 return unpack "H*", keccak256($mac_key . pack("H*", $self->ciphertext));
148             }
149              
150             sub _generate_private_key {
151 5     5   15 my ($self) = @_;
152              
153 5         18 my $derived_key = $self->kdf->decode($self->password);
154 5         31 my $cipher_key = substr($derived_key, 0, 16);
155              
156 5         40 my $key = $self->cipher->decrypt(pack("H*", $self->ciphertext), $cipher_key, pack("H*", $self->iv));
157              
158 5         302 return Blockchain::Ethereum::Key->new(private_key => $key);
159             }
160              
161             sub _generate_random_iv {
162 2     2   13 my $iv = Crypt::PRNG::random_bytes(16);
163 2         198 return unpack "H*", $iv;
164             }
165              
166             sub _generate_kdf {
167 2     2   5 my ($self) = @_;
168              
169 2         9 my ($derived_key, $salt, $N, $r, $p) = Crypt::ScryptKDF::_scrypt_extra($self->password);
170 2         232669 return Blockchain::Ethereum::Keystore::KDF->new(
171             algorithm => 'scrypt',
172             dklen => length $derived_key,
173             n => $N,
174             p => $p,
175             r => $r,
176             salt => unpack 'H*',
177             $salt
178             );
179             }
180              
181             sub _generate_ciphertext {
182 2     2   7 my ($self) = @_;
183              
184 2         8 my $derived_key = $self->kdf->decode($self->password);
185 2         17 my $cipher_key = substr($derived_key, 0, 16);
186              
187 2         19 my $encrypted = $self->cipher->encrypt($self->private_key->export, $cipher_key, pack("H*", $self->iv));
188 2         92 return unpack "H*", $encrypted;
189             }
190              
191             sub _generate_id {
192 2     2   1232 my $uuid = Data::UUID->new->create_str();
193 2         27 $uuid =~ s/-//g; # Remove hyphens for Ethereum format
194 2         26 return lc($uuid);
195             }
196              
197             sub write_to_file {
198 2     2 1 1741 my ($self, $file_path, $password) = @_;
199              
200 2 50       9 if ($password) {
201 2         8 $self->{password} = $password;
202              
203             # regenerate required fields for password change
204 2         12 delete $self->{$_} for qw(kdf iv ciphertext mac);
205             }
206              
207 2 50       11 croak 'Password is required to encrypt the keystore'
208             unless defined $self->password;
209              
210 2         11 my $file = {
211             "crypto" => {
212             "cipher" => 'aes-128-ctr',
213             "cipherparams" => {"iv" => $self->iv},
214             "ciphertext" => $self->ciphertext,
215             "kdf" => $self->kdf->algorithm,
216             "kdfparams" => {
217             "dklen" => $self->kdf->dklen,
218             "n" => $self->kdf->n,
219             "p" => $self->kdf->p,
220             "r" => $self->kdf->r,
221             "salt" => $self->kdf->salt
222             },
223             "mac" => $self->mac
224             },
225             "id" => $self->id,
226             "version" => 3
227             };
228              
229 2 50       298 open my $fh, '>:raw', $file_path
230             or croak "Could not write to file '$file_path': $!";
231 2         94 print $fh $json->encode($file);
232 2         543 close $fh;
233              
234 2         41 return 1;
235             }
236              
237             1;
238              
239             __END__