Class: ESM::Connection::Encryption

Inherits:
Object
  • Object
show all
Defined in:
lib/esm/connection/encryption.rb

Constant Summary collapse

CIPHER =
"aes-256-gcm"
NONCE_SIZE =

GCM standard

12
TAG_SIZE =

GCM authentication tag size

16
INDEX_LOW_BOUNDS =

First 32 bytes. A standard request will larger than 32 bytes so this shouldn't cause issues with the nonce being stacked at the end of the bytes (because of the data packet being smaller than 32 bytes)

0
INDEX_HIGH_BOUNDS =
31

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(key, nonce_indices: [], session_id: "") ⇒ Encryption

Returns a new instance of Encryption.

Raises:

  • (ArgumentError)


20
21
22
23
24
25
26
27
# File 'lib/esm/connection/encryption.rb', line 20

def initialize(key, nonce_indices: [], session_id: "")
  key = key.bytes[INDEX_LOW_BOUNDS..INDEX_HIGH_BOUNDS]
  raise ArgumentError, "Encryption key must be 32 bytes" if key.size != 32

  @key = key.pack("C*")
  @session_id = session_id
  @nonce_indices = nonce_indices.presence || (0...NONCE_SIZE).to_a
end

Class Method Details

.generate_nonce_indicesObject



15
16
17
18
# File 'lib/esm/connection/encryption.rb', line 15

def self.generate_nonce_indices
  indices = (INDEX_LOW_BOUNDS...INDEX_HIGH_BOUNDS).to_a.shuffle.shuffle
  NONCE_SIZE.times.map { indices.pop }.sort
end

Instance Method Details

#decrypt(input) ⇒ String

Attempts to decrypt the provided byte array

Parameters:

  • input (String)

    A UTF-8 string containing encrypted data

Returns:

  • (String)

    The decoded string



63
64
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
93
94
95
96
97
98
99
100
101
102
# File 'lib/esm/connection/encryption.rb', line 63

def decrypt(input)
  cipher = OpenSSL::Cipher.new(CIPHER).decrypt
  cipher.key = @key

  # Extract nonce and encrypted data
  nonce = []
  packet = []
  input.bytes.each_with_index do |byte, index|
    if @nonce_indices[nonce.size] == index
      nonce << byte
      next
    end

    packet << byte
  end

  # Separate auth tag from encrypted data
  auth_tag = packet.pop(TAG_SIZE).pack("C*")
  encrypted_data = packet.pack("C*")

  cipher.iv = nonce.pack("C*")
  cipher.auth_tag = auth_tag
  cipher.auth_data = @session_id

  decrypted_data = cipher.update(encrypted_data) + cipher.final
  raise ESM::Exception::DecryptionError if decrypted_data.blank?

  decrypted_data
rescue ArgumentError => e
  case e.message
  when "key must be 32 bytes"
    raise ESM::Exception::DecryptionError, "Invalid secret key length"
  when "iv must be #{NONCE_SIZE} bytes"
    raise ESM::Exception::DecryptionError, "Invalid IV length"
  else
    raise e
  end
rescue OpenSSL::Cipher::CipherError
  raise ESM::Exception::DecryptionError, "Authentication failed"
end

#encrypt(data) ⇒ Object



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/esm/connection/encryption.rb', line 29

def encrypt(data)
  cipher = OpenSSL::Cipher.new(CIPHER).encrypt
  nonce = cipher.random_iv

  cipher.key = @key
  cipher.iv = nonce
  cipher.auth_data = @session_id

  encrypted_data = cipher.update(data) + cipher.final
  auth_tag = cipher.auth_tag

  # Combine encrypted data and auth tag
  encrypted_bytes = encrypted_data.bytes + auth_tag.bytes
  nonce_bytes = nonce.bytes

  # Insert nonce bytes at specified positions
  @nonce_indices.each_with_index do |nonce_index, index|
    encrypted_bytes.insert(nonce_index, nonce_bytes[index])
  end

  encrypted_bytes.pack("C*")
end