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