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 using ECIES, 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 of ECIES it is acceptable because the definition of ECIES implies the symmetric block cipher key is only to be used once.

Public Class Methods

new(cipher: 'AES-256-CTR', digest: 'SHA256', mac_length: :half, kdf_digest: nil, mac_digest: nil, kdf_shared_info: '', mac_shared_info: '') click to toggle source

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
private_key_from_hex(hex_string, ec_group = 'secp256k1') click to toggle source

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
public_key_from_hex(hex_string, ec_group = 'secp256k1') click to toggle source

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

decrypt(key, encrypted_message) click to toggle source

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
encrypt(key, message) click to toggle source

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
kdf(shared_secret, length, shared_info_suffix) click to toggle source

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
to_s() click to toggle source

@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