File Coverage

blib/lib/Blockchain/Ethereum/Keystore/Keyfile.pm
Criterion Covered Total %
statement 111 134 82.8
branch 2 6 33.3
condition n/a
subroutine 27 34 79.4
pod 4 20 20.0
total 144 194 74.2


line stmt bran cond sub pod time code
1 1     1   105219 use v5.26;
  1         13  
2 1     1   604 use Object::Pad ':experimental(init_expr)';
  1         11229  
  1         4  
3              
4             package Blockchain::Ethereum::Keystore::Keyfile;
5             class Blockchain::Ethereum::Keystore::Keyfile;
6              
7             our $AUTHORITY = 'cpan:REFECO'; # AUTHORITY
8             our $VERSION = '0.007'; # VERSION
9              
10 1     1   465 use Carp;
  1         2  
  1         57  
11 1     1   569 use File::Slurp;
  1         29387  
  1         75  
12 1     1   492 use JSON::MaybeXS qw(decode_json encode_json);
  1         6147  
  1         75  
13 1     1   500 use Crypt::PRNG;
  1         3478  
  1         52  
14 1     1   525 use Net::SSH::Perl::Cipher;
  1         4047  
  1         45  
15              
16 1     1   512 use Blockchain::Ethereum::Keystore::Key;
  1         4  
  1         43  
17 1     1   420 use Blockchain::Ethereum::Keystore::Keyfile::KDF;
  1         3  
  1         2851  
18              
19 2     2 0 6 field $cipher :reader :writer;
  2     2 0 9  
  2         5  
  2         7  
20 2     2 0 11 field $ciphertext :reader :writer;
  2     4 0 22  
  4         12  
  4         11  
21 0     0 0 0 field $mac :reader :writer;
  0     2 0 0  
  2         5  
  2         6  
22 2     2 0 7 field $version :reader :writer;
  2     0 0 6  
  0         0  
  0         0  
23 2     2 0 20 field $iv :reader :writer;
  2     4 0 49  
  4         11  
  4         11  
24 14     14 0 43 field $kdf :reader :writer;
  14     2 0 93  
  2         9  
  2         8  
25 0     0 0 0 field $id :reader :writer;
  0     0 0 0  
  0         0  
  0         0  
26 6     6 0 38 field $private_key :reader :writer;
  6     4 0 35  
  4         17  
  4         109  
27              
28 2     2   15 field $_json :reader(_json) = JSON::MaybeXS->new(utf8 => 1);
29              
30 2     2 1 53 method import_file ($file_path, $password) {
  2         91  
  2         9  
  2         10  
  2         4  
  2         4  
31              
32 2         12 my $content = read_file($file_path);
33 2         2076 my $decoded = $self->_json->decode(lc $content);
34              
35 2         16 return $self->_from_object($decoded, $password);
36             }
37              
38 2     2   6 method _from_object ($object, $password) {
  2         5  
  2         4  
  2         7  
  2         3  
39              
40 2         10 my $version = $object->{version};
41              
42 2 50       28 croak 'Could not determine the version' unless $version >= 3;
43              
44 2         12 return $self->_from_v3($object, $password);
45             }
46              
47 2     2   5 method _from_v3 ($object, $password) {
  2         3  
  2         7  
  2         9  
  2         5  
48              
49 2         6 my $crypto = $object->{crypto};
50              
51 2         9 $self->set_cipher('AES128_CTR');
52 2         10 $self->set_ciphertext($crypto->{ciphertext});
53 2         15 $self->set_mac($crypto->{mac});
54 2         12 $self->set_version(3);
55 2         9 $self->set_iv($crypto->{cipherparams}->{iv});
56              
57 2         4 my $header = $crypto->{kdfparams};
58              
59             $self->set_kdf(
60             Blockchain::Ethereum::Keystore::Keyfile::KDF->new(
61             algorithm => $crypto->{kdf}, #
62             dklen => $header->{dklen},
63             n => $header->{n},
64             p => $header->{p},
65             r => $header->{r},
66             c => $header->{c},
67             prf => $header->{prf},
68 2         63 salt => $header->{salt}));
69              
70 2         16 $self->set_private_key($self->_private_key($password));
71              
72 2         32 return $self;
73             }
74              
75 0     0 1 0 method change_password ($old_password, $new_password) {
  0         0  
  0         0  
  0         0  
  0         0  
76              
77 0         0 return $self->import_key($self->_private_key($old_password), $new_password);
78             }
79              
80 2     2   5 method _private_key ($password) {
  2         6  
  2         4  
  2         2  
81              
82 2 50       6 return $self->private_key if $self->private_key;
83              
84 2         11 my $cipher = Net::SSH::Perl::Cipher->new(
85             $self->cipher, #
86             $self->kdf->decode($password),
87             pack("H*", $self->iv));
88              
89 2         5580 my $key = $cipher->decrypt(pack("H*", $self->ciphertext));
90              
91 2         202 return Blockchain::Ethereum::Keystore::Key->new(private_key => $key);
92             }
93              
94 2     2 1 9 method import_key ($key, $password) {
  2         5  
  2         9  
  2         4  
  2         7  
95              
96             # use the internal method here otherwise would not be availble to get the kdf params
97             # salt if give will be the same as the response, if not will be auto generated by the library
98 2         8 my ($derived_key, $salt, $N, $r, $p);
99 2         15 ($derived_key, $salt, $N, $r, $p) = Crypt::ScryptKDF::_scrypt_extra($password);
100 2         223646 $self->kdf->set_algorithm("scrypt");
101 2         8 $self->kdf->set_dklen(length $derived_key);
102 2         8 $self->kdf->set_n($N);
103 2         6 $self->kdf->set_p($p);
104 2         7 $self->kdf->set_r($r);
105 2         22 $self->kdf->set_salt(unpack "H*", $salt);
106              
107 2         14 my $iv = Crypt::PRNG::random_bytes(16);
108 2         51 $self->set_iv(unpack "H*", $iv);
109              
110 2         46 my $cipher = Net::SSH::Perl::Cipher->new(
111             "AES128_CTR", #
112             $derived_key,
113             $iv
114             );
115              
116 2         229 my $encrypted = $cipher->encrypt($key->export);
117 2         149 $self->set_ciphertext(unpack "H*", $encrypted);
118              
119 2         11 $self->set_private_key($key);
120              
121 2         18 return $self;
122             }
123              
124 0     0     method _write_to_object {
125              
126 0 0         croak "KDF algorithm and parameters are not set" unless $self->kdf;
127              
128 0           my $file = {
129             "crypto" => {
130             "cipher" => 'aes-128-ctr',
131             "cipherparams" => {"iv" => $self->iv},
132             "ciphertext" => $self->ciphertext,
133             "kdf" => $self->kdf->algorithm,
134             "kdfparams" => {
135             "dklen" => $self->kdf->dklen,
136             "n" => $self->kdf->n,
137             "p" => $self->kdf->p,
138             "r" => $self->kdf->r,
139             "salt" => $self->kdf->salt
140             },
141             "mac" => $self->mac
142             },
143             "id" => $self->id,
144             "version" => 3
145             };
146              
147 0           return $file;
148             }
149              
150 0     0 1   method write_to_file ($file_path) {
  0            
  0            
  0            
151              
152 0           return write_file($file_path, $self->_json->canonical(1)->pretty->encode($self->_write_to_object));
153             }
154              
155             1;
156              
157             __END__
158              
159             =pod
160              
161             =encoding UTF-8
162              
163             =head1 NAME
164              
165             Blockchain::Ethereum::Keystore::Keyfile
166              
167             =head1 VERSION
168              
169             version 0.007
170              
171             =head1 SYNOPSIS
172              
173             ...
174              
175             =head1 OVERVIEW
176              
177             This is an Ethereum keyfile abstraction that provides a way of change/read the
178             keyfile information.
179              
180             Currently only supports version 3 keyfiles.
181              
182             =head1 METHODS
183              
184             =head2 import_file
185              
186             Import a v3 keyfile
187              
188             =over 4
189              
190             =item * C<file_path> - string path for the keyfile
191              
192             =back
193              
194             self
195              
196             =head2 change_password
197              
198             Change the imported keyfile password
199              
200             =over 4
201              
202             =item * C<old_password> - Current password for the keyfile
203              
204             =item * C<new_password> - New password to be set
205              
206             =back
207              
208             self
209              
210             =head2 import_key
211              
212             Import a L<Blockchain::Ethereum::keystore::Key>
213              
214             =over 4
215              
216             =item * C<keyfile> - L<Blockchain::Ethereum::Keystore::Key>
217              
218             =back
219              
220             self
221              
222             =head2 write_to_file
223              
224             Write the imported keyfile/private_key to a keyfile in the file system
225              
226             =over 4
227              
228             =item * C<file_path> - file path to save the data
229              
230             =back
231              
232             returns 1 upon successfully writing the file or undef if it encountered an error
233              
234             =head1 AUTHOR
235              
236             Reginaldo Costa <refeco@cpan.org>
237              
238             =head1 COPYRIGHT AND LICENSE
239              
240             This software is Copyright (c) 2023 by REFECO.
241              
242             This is free software, licensed under:
243              
244             The MIT (X11) License
245              
246             =cut