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)

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(cipher: 'AES-256-CTR', digest: 'SHA256', mac_length: :half, 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.

  • 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.



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 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

Class Method Details

.public_key_from_hex(hex_string, ec_group = 'secp256k1') ⇒ OpenSSL::PKey::EC

Converts a hex-encoded public key to an OpenSSL::PKey::EC.

Parameters:

  • hex_string (String)

    The hex-encoded public key.

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

    The elliptical curve group for this public key.

Returns:

  • (OpenSSL::PKey::EC)

    The public key.

Raises:

  • (ArgumentError)

    If the public key is invalid.



161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/ecies/crypt.rb', line 161

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)

  sequence = OpenSSL::ASN1.Sequence([
    OpenSSL::ASN1.Sequence([
      OpenSSL::ASN1.ObjectId("id-ecPublicKey"),
      OpenSSL::ASN1::ObjectId(ec_group.curve_name),
    ]),
    OpenSSL::ASN1.BitString([hex_string].pack('H*')),
  ])

  OpenSSL::PKey::EC.new(sequence.to_der)
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.

  • encrypted_message (String)

    Octet string of the encrypted message.

Returns:

  • (String)

    The plain-text message.



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/ecies/crypt.rb', line 87

def decrypt(key, encrypted_message)
  key.private_key? or raise "Must have private key to decrypt"
  @cipher.reset

  ephemeral_public_key_length = key.group.generator.to_octet_string(:compressed).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(key.group, 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) ⇒ String

Encrypts a message to a public key using ECIES.

Parameters:

  • key (OpenSSL::EC:PKey)

    The public key.

  • message (String)

    The plain-text message.

Returns:

  • (String)

    The octet string of the encrypted message.



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/ecies/crypt.rb', line 59

def encrypt(key, message)
  key.public_key? or raise "Must have public key to encrypt"
  @cipher.reset

  ephemeral_key = OpenSSL::PKey::EC.generate(key.group)
  ephemeral_public_key_octet = ephemeral_key.public_key.to_octet_string(:compressed)

  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) ⇒ 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.

  • shared_info_suffix (String)

    The suffix to append to the shared_info.

Returns:

  • (String)

    Octet string of the derived key.



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/ecies/crypt.rb', line 125

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_sString

Returns A string representing this Crypt's parameters.

Returns:

  • (String)

    A string representing this Crypt's parameters.



148
149
150
151
152
# File 'lib/ecies/crypt.rb', line 148

def to_s
  "KDF-#{@kdf_digest.name}_" +
  "HMAC-SHA-#{@mac_digest.digest_length * 8}-#{@mac_length * 8}_" +
  @cipher.name
end