1package Crypt::PK::X25519;
2
3use strict;
4use warnings;
5our $VERSION = '0.075';
6
7require Exporter; our @ISA = qw(Exporter); ### use Exporter 5.57 'import';
8our %EXPORT_TAGS = ( all => [qw( )] );
9our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
10our @EXPORT = qw();
11
12use Carp;
13$Carp::Internal{(__PACKAGE__)}++;
14use CryptX;
15use Crypt::PK;
16use Crypt::Misc qw(read_rawfile encode_b64u decode_b64u encode_b64 decode_b64 pem_to_der der_to_pem);
17
18sub new {
19  my $self = shift->_new();
20  return @_ > 0 ? $self->import_key(@_) : $self;
21}
22
23sub import_key_raw {
24  my ($self, $key, $type) = @_;
25  croak "FATAL: undefined key" unless $key;
26  croak "FATAL: invalid key" unless length($key) == 32;
27  croak "FATAL: undefined type" unless $type;
28  return $self->_import_raw($key, 1) if $type eq 'private';
29  return $self->_import_raw($key, 0) if $type eq 'public';
30  croak "FATAL: invalid key type '$type'";
31}
32
33sub import_key {
34  my ($self, $key, $password) = @_;
35  local $SIG{__DIE__} = \&CryptX::_croak;
36  croak "FATAL: undefined key" unless $key;
37
38  # special case
39  if (ref($key) eq 'HASH') {
40    if ($key->{kty} && $key->{kty} eq "OKP" && $key->{crv} && $key->{crv} eq 'X25519') {
41      # JWK-like structure e.g.
42      # {"kty":"OKP","crv":"X25519","d":"...","x":"..."}
43      return $self->_import_raw(decode_b64u($key->{d}), 1) if $key->{d}; # private
44      return $self->_import_raw(decode_b64u($key->{x}), 0) if $key->{x}; # public
45    }
46    if ($key->{curve} && $key->{curve} eq "x25519" && ($key->{priv} || $key->{pub})) {
47      # hash exported via key2hash
48      return $self->_import_raw(pack("H*", $key->{priv}), 1) if $key->{priv};
49      return $self->_import_raw(pack("H*", $key->{pub}),  0) if $key->{pub};
50    }
51    croak "FATAL: unexpected X25519 key hash";
52  }
53
54  my $data;
55  if (ref($key) eq 'SCALAR') {
56    $data = $$key;
57  }
58  elsif (-f $key) {
59    $data = read_rawfile($key);
60  }
61  else {
62    croak "FATAL: non-existing file '$key'";
63  }
64  croak "FATAL: invalid key data" unless $data;
65
66  if ($data =~ /-----BEGIN PUBLIC KEY-----(.*?)-----END/sg) {
67    $data = pem_to_der($data, $password) or croak "FATAL: PEM/key decode failed";
68    return $self->_import($data);
69  }
70  elsif ($data =~ /-----BEGIN PRIVATE KEY-----(.*?)-----END/sg) {
71    $data = pem_to_der($data, $password) or croak "FATAL: PEM/key decode failed";
72    return $self->_import_pkcs8($data, $password);
73  }
74  elsif ($data =~ /-----BEGIN ENCRYPTED PRIVATE KEY-----(.*?)-----END/sg) {
75    $data = pem_to_der($data, $password) or croak "FATAL: PEM/key decode failed";
76    return $self->_import_pkcs8($data, $password);
77  }
78  elsif ($data =~ /-----BEGIN X25519 PRIVATE KEY-----(.*?)-----END/sg) {
79    $data = pem_to_der($data, $password) or croak "FATAL: PEM/key decode failed";
80    return $self->_import_pkcs8($data, $password);
81  }
82  elsif ($data =~ /^\s*(\{.*?\})\s*$/s) { # JSON
83    my $h = CryptX::_decode_json("$1");
84    if ($h->{kty} && $h->{kty} eq "OKP" && $h->{crv} && $h->{crv} eq 'X25519') {
85      return $self->_import_raw(decode_b64u($h->{d}), 1) if $h->{d}; # private
86      return $self->_import_raw(decode_b64u($h->{x}), 0) if $h->{x}; # public
87    }
88  }
89  elsif (length($data) == 32) {
90    croak "FATAL: use import_key_raw() to load raw (32 bytes) X25519 key";
91  }
92  else {
93    my $rv = eval { $self->_import($data) }                  ||
94             eval { $self->_import_pkcs8($data, $password) } ||
95             eval { $self->_import_x509($data) };
96    return $rv if $rv;
97  }
98  croak "FATAL: invalid or unsupported X25519 key format";
99}
100
101sub export_key_pem {
102  my ($self, $type, $password, $cipher) = @_;
103  local $SIG{__DIE__} = \&CryptX::_croak;
104  my $key = $self->export_key_der($type||'');
105  return unless $key;
106  return der_to_pem($key, "X25519 PRIVATE KEY", $password, $cipher) if substr($type, 0, 7) eq 'private';
107  return der_to_pem($key, "PUBLIC KEY") if substr($type,0, 6) eq 'public';
108}
109
110sub export_key_jwk {
111  my ($self, $type, $wanthash) = @_;
112  local $SIG{__DIE__} = \&CryptX::_croak;
113  my $kh = $self->key2hash;
114  return unless $kh;
115  my $hash = { kty => "OKP", crv => "X25519" };
116  $hash->{x} = encode_b64u(pack("H*", $kh->{pub}));
117  $hash->{d} = encode_b64u(pack("H*", $kh->{priv})) if $type && $type eq 'private' && $kh->{priv};
118  return $wanthash ? $hash : CryptX::_encode_json($hash);
119}
120
121sub CLONE_SKIP { 1 } # prevent cloning
122
1231;
124
125=pod
126
127=head1 NAME
128
129Crypt::PK::X25519 - Asymmetric cryptography based on X25519
130
131=head1 SYNOPSIS
132
133 use Crypt::PK::X25519;
134
135 #Shared secret
136 my $priv = Crypt::PK::X25519->new('Alice_priv_x25519.der');
137 my $pub = Crypt::PK::X25519->new('Bob_pub_x25519.der');
138 my $shared_secret = $priv->shared_secret($pub);
139
140 #Load key
141 my $pk = Crypt::PK::X25519->new;
142 my $pk_hex = "EA7806F721A8570512C8F6EFB4E8D620C49A529E4DF5EAA77DEC646FB1E87E41";
143 $pk->import_key_raw(pack("H*", $pk_hex), "public");
144 my $sk = Crypt::PK::X25519->new;
145 my $sk_hex = "002F93D10BA5728D8DD8E9527721DABA3261C0BB1BEFDE7B4BBDAC631D454651";
146 $sk->import_key_raw(pack("H*", $sk_hex), "private");
147
148 #Key generation
149 my $pk = Crypt::PK::X25519->new->generate_key;
150 my $private_der = $pk->export_key_der('private');
151 my $public_der  = $pk->export_key_der('public');
152 my $private_pem = $pk->export_key_pem('private');
153 my $public_pem  = $pk->export_key_pem('public');
154 my $private_raw = $pk->export_key_raw('private');
155 my $public_raw  = $pk->export_key_raw('public');
156 my $private_jwk = $pk->export_key_jwk('private');
157 my $public_jwk  = $pk->export_key_jwk('public');
158
159=head1 DESCRIPTION
160
161I<Since: CryptX-0.067>
162
163=head1 METHODS
164
165=head2 new
166
167 my $pk = Crypt::PK::X25519->new();
168 #or
169 my $pk = Crypt::PK::X25519->new($priv_or_pub_key_filename);
170 #or
171 my $pk = Crypt::PK::X25519->new(\$buffer_containing_priv_or_pub_key);
172
173Support for password protected PEM keys
174
175 my $pk = Crypt::PK::X25519->new($priv_pem_key_filename, $password);
176 #or
177 my $pk = Crypt::PK::X25519->new(\$buffer_containing_priv_pem_key, $password);
178
179=head2 generate_key
180
181Uses Yarrow-based cryptographically strong random number generator seeded with
182random data taken from C</dev/random> (UNIX) or C<CryptGenRandom> (Win32).
183
184 $pk->generate_key;
185
186=head2 import_key
187
188Loads private or public key in DER or PEM format.
189
190 $pk->import_key($filename);
191 #or
192 $pk->import_key(\$buffer_containing_key);
193
194Support for password protected PEM keys:
195
196 $pk->import_key($filename, $password);
197 #or
198 $pk->import_key(\$buffer_containing_key, $password);
199
200Loading private or public keys form perl hash:
201
202 $pk->import_key($hashref);
203
204 # the $hashref is either a key exported via key2hash
205 $pk->import_key({
206      curve => "x25519",
207      pub   => "EA7806F721A8570512C8F6EFB4E8D620C49A529E4DF5EAA77DEC646FB1E87E41",
208      priv  => "002F93D10BA5728D8DD8E9527721DABA3261C0BB1BEFDE7B4BBDAC631D454651",
209 });
210
211 # or a hash with items corresponding to JWK (JSON Web Key)
212 $pk->import_key({
213       kty => "OKP",
214       crv => "X25519",
215       d   => "AC-T0Qulco2N2OlSdyHaujJhwLsb7957S72sYx1FRlE",
216       x   => "6ngG9yGoVwUSyPbvtOjWIMSaUp5N9eqnfexkb7HofkE",
217 });
218
219Supported key formats:
220
221 # all formats can be loaded from a file
222 my $pk = Crypt::PK::X25519->new($filename);
223
224 # or from a buffer containing the key
225 my $pk = Crypt::PK::X25519->new(\$buffer_with_key);
226
227=over
228
229=item * X25519 private keys in PEM format
230
231 -----BEGIN X25519 PRIVATE KEY-----
232 MC4CAQAwBQYDK2VuBCIEIAAvk9ELpXKNjdjpUnch2royYcC7G+/ee0u9rGMdRUZR
233 -----END X25519 PRIVATE KEY-----
234
235=item * X25519 private keys in password protected PEM format
236
237 -----BEGIN X25519 PRIVATE KEY-----
238 Proc-Type: 4,ENCRYPTED
239 DEK-Info: DES-CBC,DEEFD3D6B714E75A
240
241 dfFWP5bKn49aZ993NVAhQQPdFWgsTb4j8CWhRjGBVTPl6ITstAL17deBIRBwZb7h
242 pAyIka81Kfs=
243 -----END X25519 PRIVATE KEY-----
244
245=item * X25519 public keys in PEM format
246
247 -----BEGIN PUBLIC KEY-----
248 MCowBQYDK2VuAyEA6ngG9yGoVwUSyPbvtOjWIMSaUp5N9eqnfexkb7HofkE=
249 -----END PUBLIC KEY-----
250
251=item * PKCS#8 private keys
252
253 -----BEGIN PRIVATE KEY-----
254 MC4CAQAwBQYDK2VuBCIEIAAvk9ELpXKNjdjpUnch2royYcC7G+/ee0u9rGMdRUZR
255 -----END PRIVATE KEY-----
256
257=item * PKCS#8 encrypted private keys
258
259 -----BEGIN ENCRYPTED PRIVATE KEY-----
260 MIGHMEsGCSqGSIb3DQEFDTA+MCkGCSqGSIb3DQEFDDAcBAiS0NOFZmjJswICCAAw
261 DAYIKoZIhvcNAgkFADARBgUrDgMCBwQIGd40Hdso8Y4EONSRCTrqvftl9hl3zbH9
262 2QmHF1KJ4HDMdLDRxD7EynonCw2SV7BO+XNRHzw2yONqiTybfte7nk9t
263 -----END ENCRYPTED PRIVATE KEY-----
264
265=item * X25519 private keys in JSON Web Key (JWK) format
266
267See L<https://tools.ietf.org/html/rfc8037>
268
269 {
270  "kty":"OKP",
271  "crv":"X25519",
272  "x":"6ngG9yGoVwUSyPbvtOjWIMSaUp5N9eqnfexkb7HofkE",
273  "d":"AC-T0Qulco2N2OlSdyHaujJhwLsb7957S72sYx1FRlE",
274 }
275
276B<BEWARE:> For JWK support you need to have L<JSON::PP>, L<JSON::XS> or L<Cpanel::JSON::XS> module.
277
278=item * X25519 public keys in JSON Web Key (JWK) format
279
280 {
281  "kty":"OKP",
282  "crv":"X25519",
283  "x":"6ngG9yGoVwUSyPbvtOjWIMSaUp5N9eqnfexkb7HofkE",
284 }
285
286B<BEWARE:> For JWK support you need to have L<JSON::PP>, L<JSON::XS> or L<Cpanel::JSON::XS> module.
287
288=back
289
290=head2 import_key_raw
291
292Import raw public/private key - can load raw key data exported by L</export_key_raw>.
293
294 $pk->import_key_raw($key, 'public');
295 $pk->import_key_raw($key, 'private');
296
297=head2 export_key_der
298
299 my $private_der = $pk->export_key_der('private');
300 #or
301 my $public_der = $pk->export_key_der('public');
302
303=head2 export_key_pem
304
305 my $private_pem = $pk->export_key_pem('private');
306 #or
307 my $public_pem = $pk->export_key_pem('public');
308
309Support for password protected PEM keys
310
311 my $private_pem = $pk->export_key_pem('private', $password);
312 #or
313 my $private_pem = $pk->export_key_pem('private', $password, $cipher);
314
315 # supported ciphers: 'DES-CBC'
316 #                    'DES-EDE3-CBC'
317 #                    'SEED-CBC'
318 #                    'CAMELLIA-128-CBC'
319 #                    'CAMELLIA-192-CBC'
320 #                    'CAMELLIA-256-CBC'
321 #                    'AES-128-CBC'
322 #                    'AES-192-CBC'
323 #                    'AES-256-CBC' (DEFAULT)
324
325=head2 export_key_jwk
326
327Exports public/private keys as a JSON Web Key (JWK).
328
329 my $private_json_text = $pk->export_key_jwk('private');
330 #or
331 my $public_json_text = $pk->export_key_jwk('public');
332
333Also exports public/private keys as a perl HASH with JWK structure.
334
335 my $jwk_hash = $pk->export_key_jwk('private', 1);
336 #or
337 my $jwk_hash = $pk->export_key_jwk('public', 1);
338
339B<BEWARE:> For JWK support you need to have L<JSON::PP>, L<JSON::XS> or L<Cpanel::JSON::XS> module.
340
341=head2 export_key_raw
342
343Export raw public/private key
344
345 my $private_bytes = $pk->export_key_raw('private');
346 #or
347 my $public_bytes = $pk->export_key_raw('public');
348
349=head2 shared_secret
350
351  # Alice having her priv key $pk and Bob's public key $pkb
352  my $pk  = Crypt::PK::X25519->new($priv_key_filename);
353  my $pkb = Crypt::PK::X25519->new($pub_key_filename);
354  my $shared_secret = $pk->shared_secret($pkb);
355
356  # Bob having his priv key $pk and Alice's public key $pka
357  my $pk = Crypt::PK::X25519->new($priv_key_filename);
358  my $pka = Crypt::PK::X25519->new($pub_key_filename);
359  my $shared_secret = $pk->shared_secret($pka);  # same value as computed by Alice
360
361=head2 is_private
362
363 my $rv = $pk->is_private;
364 # 1 .. private key loaded
365 # 0 .. public key loaded
366 # undef .. no key loaded
367
368=head2 key2hash
369
370 my $hash = $pk->key2hash;
371
372 # returns hash like this (or undef if no key loaded):
373 {
374   curve => "x25519",
375   # raw public key as a hexadecimal string
376   pub   => "EA7806F721A8570512C8F6EFB4E8D620C49A529E4DF5EAA77DEC646FB1E87E41",
377   # raw private key as a hexadecimal string. undef if key is public only
378   priv  => "002F93D10BA5728D8DD8E9527721DABA3261C0BB1BEFDE7B4BBDAC631D454651",
379 }
380
381=head1 SEE ALSO
382
383=over
384
385=item * L<https://en.wikipedia.org/wiki/Curve25519>
386
387=item * L<https://tools.ietf.org/html/rfc7748>
388
389=back
390
391=cut
392