Class: ECIES::Crypt

Inherits:
Object
  • Object
show all
Defined in:
lib/ecies/crypt.rb

Overview

Provides functionality for encrypting and decrypting messages using ECIES. Encapsulates the configuration parameters chosen for ECIES.

Constant Summary collapse

DIGESTS =

The allowed digest algorithms for ECIES.

%w{SHA224 SHA256 SHA384 SHA512}
CIPHERS =

The allowed cipher algorithms for ECIES.

%w{AES-128-CBC AES-192-CBC AES-256-CBC AES-128-CTR AES-192-CTR AES-256-CTR}
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.

("\x00" * 16).force_encoding(Encoding::BINARY)

Instance Method Summary collapse

Constructor Details

#initialize(cipher: 'AES-256-CTR', digest: 'SHA256', mac_length: :half, ec_group: 'secp256k1', kdf_digest: nil, mac_digest: nil, kdf_shared_info: '', mac_shared_info: '') ⇒ Crypt

Creates a new instance of ECIES::Crypt.

Parameters:

  • cipher (String) (defaults to: 'AES-256-CTR')

    The cipher algorithm to use. Must be one of CIPHERS.

  • digest (String, OpenSSL::Digest) (defaults to: 'SHA256')

    The digest algorithm to use for HMAC and KDF. Must be one of DIGESTS.

  • mac_length (:half, :full) (defaults to: :half)

    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.

  • ec_group (OpenSSL::PKey::EC::Group, String) (defaults to: 'secp256k1')

    The elliptical curve group to use when the key is passed in hex form to encrypt or decrypt.

  • kdf_digest (String, OpenSSL::Digest, nil) (defaults to: nil)

    The digest algorithm to use for KDF. If not specified, the digest argument will be used.

  • mac_digest (String, OpenSSL::Digest, nil) (defaults to: nil)

    The digest algorithm to use for HMAC. If not specified, the digest argument will be used.

  • kdf_shared_info (String) (defaults to: '')

    Optional. A string containing the shared info used for KDF, also known as SharedInfo1.

  • mac_shared_info (String) (defaults to: '')

    Optional. A string containing the shared info used for MAC, also known as SharedInfo2.



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/ecies/crypt.rb', line 41

def initialize(cipher: 'AES-256-CTR', digest: 'SHA256', mac_length: :half, ec_group: 'secp256k1', kdf_digest: nil, mac_digest: nil, kdf_shared_info: '', mac_shared_info: '')
  @cipher = OpenSSL::Cipher.new(cipher)
  @ec_group = OpenSSL::PKey::EC::Group.new(ec_group)
  @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

Instance Method Details

#decrypt(key, encrypted_message) ⇒ String

Decrypts a message with a private key using ECIES.

Parameters:

  • key (OpenSSL::EC:PKey)

    The private key.

  • key (OpenSSL::EC:PKey, String)

    The private key. An OpenSSL::EC:PKey containing the private key, or a hex-encoded string representing the private key on this Crypt's ec_group.

  • encrypted_message (String)

    Octet string of the encrypted message.

Returns:

  • (String)

    The plain-text message.



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/ecies/crypt.rb', line 102

def decrypt(key, encrypted_message)
  if key.is_a?(String)
    new_key = OpenSSL::PKey::EC.new(@ec_group)
    new_key.private_key = OpenSSL::BN.new(key, 16)
    key = new_key
  end
  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_text = 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_text, 2))

  shared_secret = key.dh_compute_key(ephemeral_public_key)

  key_pair = kdf(shared_secret, @cipher.key_len + @mac_length)
  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) ⇒ String

Encrypts a message to a public key using ECIES.

Parameters:

  • key (OpenSSL::EC:PKey, String)

    The public key. An OpenSSL::EC:PKey containing the public key, or a hex-encoded string representing the public key on this Crypt's ec_group.

  • message (String)

    The plain-text message.

Returns:

  • (String)

    The octet string of the encrypted message.



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/ecies/crypt.rb', line 65

def encrypt(key, message)
  if key.is_a?(String)
    new_key = OpenSSL::PKey::EC.new(@ec_group)
    new_key.public_key = OpenSSL::PKey::EC::Point.new(@ec_group, OpenSSL::BN.new(key, 16))
    key = new_key
  end
  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

  shared_secret = ephemeral_key.dh_compute_key(key.public_key)

  key_pair = kdf(shared_secret, @cipher.key_len + @mac_length)
  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_key.public_key.to_bn.to_s(2) + ciphertext + mac
end

#kdf(shared_secret, length) ⇒ String

Key-derivation function, compatible with ANSI-X9.63-KDF

Parameters:

  • shared_secret (String)

    The shared secret from which the key will be derived.

  • length (Integer)

    The length of the key to generate.

Returns:

  • (String)

    Octet string of the derived key.



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/ecies/crypt.rb', line 146

def kdf(shared_secret, length)
  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)
    if io.pos >= length
      return io.string.byteslice(0, length)
    end
  end
end