#!/usr/bin/env perl ## ## Author......: See docs/credits.txt ## License.....: MIT ## use strict; use warnings; use Crypt::PBKDF2; use Crypt::OpenSSL::EC; use Crypt::OpenSSL::Bignum::CTX; use Digest::SHA qw (sha256 sha512); use Digest::HMAC qw (hmac_hex); use Crypt::CBC; use Compress::Zlib; sub module_constraints { [[0, 256], [-1, -1], [-1, -1], [-1, -1], [-1, -1]] } my $MAX_DATA_LEN = 16384; my $TRUNCATE_DATA_LEN = 1024; # helper function: key derivation from password and one point on the curve (public key) sub generate_key { my $word = shift; my $ephemeral_pubkey = shift; my $pbkdf2 = Crypt::PBKDF2->new ( hasher => Crypt::PBKDF2->hasher_from_algorithm ('HMACSHA2', 512), iterations => 1024, output_len => 64 ); my $private_key = $pbkdf2->PBKDF2 ("", $word); my $method = Crypt::OpenSSL::EC::EC_GFp_simple_method (); # or Crypt::OpenSSL::EC::EC_GFp_mont_method () my $group = Crypt::OpenSSL::EC::EC_GROUP::new ($method); # secp256k1 elliptic curve parameters my $p = Crypt::OpenSSL::Bignum->new_from_hex ("fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f"); my $a = Crypt::OpenSSL::Bignum->new_from_hex ("0000000000000000000000000000000000000000000000000000000000000000"); my $b = Crypt::OpenSSL::Bignum->new_from_hex ("0000000000000000000000000000000000000000000000000000000000000007"); my $ctx = Crypt::OpenSSL::Bignum::CTX->new (); Crypt::OpenSSL::EC::EC_GROUP::set_curve_GFp ($group, $p, $a, $b, $ctx); my $Gx = Crypt::OpenSSL::Bignum->new_from_hex ("79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"); my $Gy = Crypt::OpenSSL::Bignum->new_from_hex ("483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8"); my $G = Crypt::OpenSSL::EC::EC_POINT::new ($group); Crypt::OpenSSL::EC::EC_POINT::set_affine_coordinates_GFp ($group, $G, $Gx, $Gy, $ctx); my $order = Crypt::OpenSSL::Bignum->new_from_hex ("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"); my $cofactor = Crypt::OpenSSL::Bignum->new_from_hex ("0000000000000000000000000000000000000000000000000000000000000001"); Crypt::OpenSSL::EC::EC_GROUP::set_generator ($group, $G, $order, $cofactor); # or cofactor = Crypt::OpenSSL::Bignum->one () # scalar # hash mod GROUP_ORDER my $m = Crypt::OpenSSL::Bignum->new_from_hex (unpack ("H*", $private_key)); # point (public key, ephemeral_pubkey) my $Q = Crypt::OpenSSL::EC::EC_POINT::new ($group); my $ret = Crypt::OpenSSL::EC::EC_POINT::oct2point ($group, $Q, $ephemeral_pubkey, $ctx); if ($ret == 0) { return; } # multiply my $result = Crypt::OpenSSL::EC::EC_POINT::new ($group); my $n = Crypt::OpenSSL::Bignum->zero (); Crypt::OpenSSL::EC::EC_POINT::mul ($group, $result, $n, $Q, $m, $ctx); # get compressed public/shared key format my $public_key = Crypt::OpenSSL::EC::EC_POINT::point2oct ($group, $result, &Crypt::OpenSSL::EC::POINT_CONVERSION_COMPRESSED, $ctx); # hash the compressed public key with sha512 () return sha512 ($public_key); } sub module_generate_hash { my $word = shift; my $ephemeral_pubkey = ""; my $key = ""; my $valid_point = 0; while ($valid_point == 0) { my $sign_of_curve_point = int (rand (2)); # 2 possibilities: 02... or 03... ephemeral public keys $ephemeral_pubkey = pack ("H*", "0" . ($sign_of_curve_point + 2) . random_hex_string (64)); $key = generate_key ($word, $ephemeral_pubkey); if (defined ($key)) { $valid_point = 1; } } my $valid_compression_rate = 0; my $compressed_data = ""; while ($valid_compression_rate == 0) { my $data_buf = "{\r\n \""; if (int (rand (2)) == 1) # alternative with different line break { $data_buf = "{\n \""; } # we assume a compression rate of 30% (smaller if compressed) my $data_length = $MAX_DATA_LEN + int (rand (int ($MAX_DATA_LEN * 1.30 + 1))); my $random_length = $data_length - length ($data_buf); if ($random_length > 0) { $data_buf .= random_string ($random_length); # or random_bytes ($random_length); } # compress/deflate the data: my $deflator = deflateInit (-WindowBits => MAX_WBITS); my $header = $deflator->deflate ($data_buf); $compressed_data = $deflator->flush (); $compressed_data = $header . $compressed_data; # check if data is valid: if ((length ($compressed_data) + 15) <= $MAX_DATA_LEN) { next; } my $zlib_rate = ord (substr ($compressed_data, 2, 1)) & 0x07; if ($zlib_rate != 0x05) { next; } $valid_compression_rate = 1; } # encrypt the data with AES128: my $iv = substr ($key, 0, 16); my $aes_key = substr ($key, 16, 16); my $aes = Crypt::CBC->new ({ cipher => "Crypt::Rijndael", keysize => 16, literal_key => 1, header => "none", iv => $iv, key => $aes_key }); my $encrypted_data = $aes->encrypt ($compressed_data); # MAC: my $hmac_key = substr ($key, 32, 32); my $mac = hmac_hex ($encrypted_data, $hmac_key, \&sha256); # truncate for version 5: $encrypted_data = substr ($encrypted_data, 0, $TRUNCATE_DATA_LEN); # format the hash: my $hash = sprintf ("\$electrum\$5*%s*%s*%s", unpack ("H*", $ephemeral_pubkey), unpack ("H*", $encrypted_data), $mac ); return $hash; } sub module_verify_hash { my $line = shift; my $index1 = index ($line, ":"); return if $index1 < 1; my $hash_in = substr ($line, 0, $index1); my $word = substr ($line, $index1 + 1); return if (substr ($hash_in, 0, 10) ne "\$electrum\$"); # version: my $index2 = index ($hash_in, "*"); return if $index2 < 1; my $version = substr ($hash_in, 10, $index2 - 10); return if ($version ne "5"); # public key: $index1 = index ($line, "*", $index2 + 1); return if $index1 < 1; my $ephemeral_pubkey = substr ($hash_in, $index2 + 1, $index1 - $index2 - 1); $ephemeral_pubkey = pack ("H*", $ephemeral_pubkey); # data: $index2 = index ($hash_in, "*", $index1 + 1); return if $index2 < 1; my $data_buf = substr ($hash_in, $index1 + 1, $index2 - $index1 - 1); $data_buf = pack ("H*", $data_buf); # MAC: my $mac = substr ($hash_in, $index2 + 1); # Start: my $new_hash = ""; my $key = generate_key ($word, $ephemeral_pubkey); # decrypt the data with AES128 my $iv = substr ($key, 0, 16); my $aes_key = substr ($key, 16, 16); my $aes = Crypt::CBC->new ({ cipher => "Crypt::Rijndael", keysize => 16, literal_key => 1, header => "none", iv => $iv, key => $aes_key }); my $decrypted_data = $aes->decrypt ($data_buf); # some early reject/validation steps: # first test: if (substr ($decrypted_data, 0, 2) ne "\x78\x9c") { return ($new_hash, $word); } # second test: if ((ord (substr ($decrypted_data, 2, 1)) & 0x07) != 0x05) { return ($new_hash, $word); } # decompress/inflate: my $inflator = inflateInit (-WindowBits => MAX_WBITS); my ($decompressed_data, $status) = $inflator->inflate ($decrypted_data); # final validation of data: if (length ($status) > 0) { return ($new_hash, $word); } if ((substr ($decompressed_data, 0, 7) ne "{\n \"") && (substr ($decompressed_data, 0, 8) ne "{\r\n \"")) { return ($new_hash, $word); } $new_hash = $hash_in; return ($new_hash, $word); } 1;