class ECIES::Crypt
Provides functionality for encrypting and decrypting messages using ECIES
. Encapsulates the configuration parameters chosen for ECIES
.
Constants
- CIPHERS
The allowed cipher algorithms for
ECIES
.- DIGESTS
The allowed digest algorithms for
ECIES
.- IV
The initialization vector used in
ECIES
. Quoting from sec1-v2: “When usingECIES
, some exception are made. For the CBC and CTR modes, the initial value or initial counter are set to be zero and are omitted from the ciphertext. In general this practice is not advisable, but in the case ofECIES
it is acceptable because the definition ofECIES
implies the symmetric block cipher key is only to be used once.
Public Class Methods
Creates a new instance of {Crypt}.
@param cipher [String] The cipher algorithm to use. Must be one of
{CIPHERS}.
@param digest [String,OpenSSL::Digest] The digest algorithm to use for
HMAC and KDF. Must be one of {DIGESTS}.
@param mac_length [:half,:full] The length of the mac. If :half, the mac
length will be equal to half the mac_digest's digest_legnth. If :full, the mac length will be equal to the mac_digest's digest_length.
@param kdf_digest [String,OpenSSL::Digest,nil] The digest algorithm to
use for KDF. If not specified, the `digest` argument will be used.
@param mac_digest [String,OpenSSL::Digest,nil] The digest algorithm to
use for HMAC. If not specified, the `digest` argument will be used.
@param kdf_shared_info [String] Optional. A string containing the shared
info used for KDF, also known as SharedInfo1.
@param mac_shared_info [String] Optional. A string containing the shared
info used for MAC, also known as SharedInfo2.
# File lib/ecies/crypt.rb, line 38 def initialize(cipher: 'AES-256-CTR', digest: 'SHA256', mac_length: :half, kdf_digest: nil, mac_digest: nil, kdf_shared_info: '', mac_shared_info: '') @cipher = OpenSSL::Cipher.new(cipher) @mac_digest = OpenSSL::Digest.new(mac_digest || digest) @kdf_digest = OpenSSL::Digest.new(kdf_digest || digest) @kdf_shared_info = kdf_shared_info @mac_shared_info = mac_shared_info CIPHERS.include?(@cipher.name) or raise "Cipher must be one of #{CIPHERS}" DIGESTS.include?(@mac_digest.name) or raise "Digest must be one of #{DIGESTS}" DIGESTS.include?(@kdf_digest.name) or raise "Digest must be one of #{DIGESTS}" [:half, :full].include?(mac_length) or raise "mac_length must be :half or :full" @mac_length = @mac_digest.digest_length @mac_length /= 2 if mac_length == :half end
Converts a hex-encoded private key to an `OpenSSL::PKey::EC`.
@param hex_string [String] The hex-encoded private key. @param ec_group [OpenSSL::PKey::EC::Group,String] The elliptical curve
group for this private key.
@return [OpenSSL::PKey::EC] The private key. @note The returned key only contains the private component. In order to
populate the public component of the key, you must compute it as follows: `key.public_key = key.group.generator.mul(key.private_key)`.
@raise [OpenSSL::PKey::ECError] If the private key is invalid.
# File lib/ecies/crypt.rb, line 183 def self.private_key_from_hex(hex_string, ec_group = 'secp256k1') ec_group = OpenSSL::PKey::EC::Group.new(ec_group) if ec_group.is_a?(String) key = OpenSSL::PKey::EC.new(ec_group) key.private_key = OpenSSL::BN.new(hex_string, 16) key.private_key < ec_group.order or raise OpenSSL::PKey::ECError, "Private key greater than group's order" key.private_key > 1 or raise OpenSSL::PKey::ECError, "Private key too small" key end
Converts a hex-encoded public key to an `OpenSSL::PKey::EC`.
@param hex_string [String] The hex-encoded public key. @param ec_group [OpenSSL::PKey::EC::Group,String] The elliptical curve
group for this public key.
@return [OpenSSL::PKey::EC] The public key. @raise [OpenSSL::PKey::EC::Point::Error] If the public key is invalid.
# File lib/ecies/crypt.rb, line 166 def self.public_key_from_hex(hex_string, ec_group = 'secp256k1') ec_group = OpenSSL::PKey::EC::Group.new(ec_group) if ec_group.is_a?(String) key = OpenSSL::PKey::EC.new(ec_group) key.public_key = OpenSSL::PKey::EC::Point.new(ec_group, OpenSSL::BN.new(hex_string, 16)) key end
Public Instance Methods
Decrypts a message with a private key using ECIES
.
@param key [OpenSSL::EC:PKey] The private key. @param encrypted_message [String] Octet string of the encrypted message. @return [String] The plain-text message.
# File lib/ecies/crypt.rb, line 89 def decrypt(key, encrypted_message) key.private_key? or raise "Must have private key to decrypt" @cipher.reset group_copy = OpenSSL::PKey::EC::Group.new(key.group) group_copy.point_conversion_form = :compressed ephemeral_public_key_length = group_copy.generator.to_bn.to_s(2).bytesize ciphertext_length = encrypted_message.bytesize - ephemeral_public_key_length - @mac_length ciphertext_length > 0 or raise OpenSSL::PKey::ECError, "Encrypted message too short" ephemeral_public_key_octet = encrypted_message.byteslice(0, ephemeral_public_key_length) ciphertext = encrypted_message.byteslice(ephemeral_public_key_length, ciphertext_length) mac = encrypted_message.byteslice(-@mac_length, @mac_length) ephemeral_public_key = OpenSSL::PKey::EC::Point.new(group_copy, OpenSSL::BN.new(ephemeral_public_key_octet, 2)) shared_secret = key.dh_compute_key(ephemeral_public_key) key_pair = kdf(shared_secret, @cipher.key_len + @mac_length, ephemeral_public_key_octet) cipher_key = key_pair.byteslice(0, @cipher.key_len) hmac_key = key_pair.byteslice(-@mac_length, @mac_length) computed_mac = OpenSSL::HMAC.digest(@mac_digest, hmac_key, ciphertext + @mac_shared_info).byteslice(0, @mac_length) computed_mac == mac or raise OpenSSL::PKey::ECError, "Invalid Message Authenticaton Code" @cipher.decrypt @cipher.iv = IV @cipher.key = cipher_key @cipher.update(ciphertext) + @cipher.final end
Encrypts a message to a public key using ECIES
.
@param key [OpenSSL::EC:PKey] The public key. @param message [String] The plain-text message. @return [String] The octet string of the encrypted message.
# File lib/ecies/crypt.rb, line 59 def encrypt(key, message) key.public_key? or raise "Must have public key to encrypt" @cipher.reset group_copy = OpenSSL::PKey::EC::Group.new(key.group) group_copy.point_conversion_form = :compressed ephemeral_key = OpenSSL::PKey::EC.new(group_copy).generate_key ephemeral_public_key_octet = ephemeral_key.public_key.to_bn.to_s(2) shared_secret = ephemeral_key.dh_compute_key(key.public_key) key_pair = kdf(shared_secret, @cipher.key_len + @mac_length, ephemeral_public_key_octet) cipher_key = key_pair.byteslice(0, @cipher.key_len) hmac_key = key_pair.byteslice(-@mac_length, @mac_length) @cipher.encrypt @cipher.iv = IV @cipher.key = cipher_key ciphertext = @cipher.update(message) + @cipher.final mac = OpenSSL::HMAC.digest(@mac_digest, hmac_key, ciphertext + @mac_shared_info).byteslice(0, @mac_length) ephemeral_public_key_octet + ciphertext + mac end
Key-derivation function, compatible with ANSI-X9.63-KDF
@param shared_secret [String] The shared secret from which the key will
be derived.
@param length [Integer] The length of the key to generate. @param shared_info_suffix [String] The suffix to append to the
shared_info.
@return [String] Octet string of the derived key.
# File lib/ecies/crypt.rb, line 130 def kdf(shared_secret, length, shared_info_suffix) length >=0 or raise "length cannot be negative" return "" if length == 0 if length / @kdf_digest.digest_length >= 0xFF_FF_FF_FF raise "length too large" end io = StringIO.new(String.new) counter = 0 loop do counter += 1 counter_bytes = [counter].pack('N') io << @kdf_digest.digest(shared_secret + counter_bytes + @kdf_shared_info + shared_info_suffix) if io.pos >= length return io.string.byteslice(0, length) end end end
@return [String] A string representing this Crypt's parameters.
# File lib/ecies/crypt.rb, line 153 def to_s "KDF-#{@kdf_digest.name}_" + "HMAC-SHA-#{@mac_digest.digest_length * 8}-#{@mac_length * 8}_" + @cipher.name end