class Sygna::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_digest: nil) 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_digest [String,OpenSSL::Digest,nil] The digest algorithm to

use for HMAC. If not specified, the `digest` argument will be used.
# File lib/sygna/crypt.rb, line 30
def initialize(cipher: 'AES-256-CTR', digest: 'SHA256', mac_digest: nil)
  @cipher = OpenSSL::Cipher.new(cipher)
  @mac_digest = OpenSSL::Digest.new(mac_digest || digest)

  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}"
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/sygna/crypt.rb, line 134
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/sygna/crypt.rb, line 117
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/sygna/crypt.rb, line 77
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)

  ephemeral_public_key_octet = encrypted_message.slice(0, 65)

  mac = encrypted_message.slice(65, 20)

  ciphertext = encrypted_message.slice(85, encrypted_message.size)

  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)

  hashed_secret = Digest::SHA512.digest(shared_secret)

  cipher_key = hashed_secret.slice(0, 32)
  hmac_key = hashed_secret.slice(32, hashed_secret.length)

  data_to_mac = IV + ephemeral_public_key_octet + ciphertext

  computed_mac = OpenSSL::HMAC.digest("SHA1", hmac_key, data_to_mac)
  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/sygna/crypt.rb, line 43
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)

  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)

  hashed_secret = Digest::SHA512.digest(shared_secret)

  cipher_key = hashed_secret.slice(0, 32)
  hmac_key = hashed_secret.slice(32, hashed_secret.length - 32)

  @cipher.encrypt
  @cipher.iv = IV
  @cipher.key = cipher_key
  ciphertext = @cipher.update(message) + @cipher.final

  data_to_mac = IV + ephemeral_public_key_octet + ciphertext

  mac = OpenSSL::HMAC.digest(@mac_digest, hmac_key, data_to_mac)

  # 65 + 20 + 16
  ephemeral_public_key_octet + mac + ciphertext
end