Class: Karafka::Pro::Encryption::Ciphers::Envelope
- Defined in:
- lib/karafka/pro/encryption/ciphers/envelope.rb
Overview
Hybrid cipher where each payload is encrypted with a one-time AES-256-GCM key and only that key is RSA-wrapped (OAEP padding). Handles payloads of any size.
The GCM auth tag covers the whole envelope (header included), so corruption and truncation are detected reliably. Note this is corruption detection, not authenticity: the RSA public key is distributed to all producers, so any of its holders can construct a valid envelope.
Instance Method Summary collapse
-
#decrypt(version, content) ⇒ String
Decrypts an envelope produced by #encrypt.
-
#encrypt(content) ⇒ String
Encrypts content with a one-time AES-256-GCM key and RSA-wraps that key using OAEP padding.
Methods inherited from Base
Constructor Details
This class inherits a constructor from Karafka::Pro::Encryption::Ciphers::Base
Instance Method Details
#decrypt(version, content) ⇒ String
All failure paths stay within the OpenSSL::PKey error family: the explicit
guards raise RSAError, while an OAEP unwrap failure (e.g. non-matching private
key) surfaces from the EVP API as its parent PKeyError
Decrypts an envelope produced by #encrypt
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 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 |
# File 'lib/karafka/pro/encryption/ciphers/envelope.rb', line 113 def decrypt(version, content) content = content.b if content.bytesize < VERSION_BYTES raise(OpenSSL::PKey::RSAError, "corrupted or truncated envelope") end # Version goes next so future layouts of different sizes are reported as # unsupported to older consumers instead of as corrupted if content[0, VERSION_BYTES] != VERSION raise(OpenSSL::PKey::RSAError, "unsupported envelope version") end pem = private_pem(version) wrapped_size = pem.n.num_bytes header_size = VERSION_BYTES + wrapped_size + IV_BYTES min_size = header_size + TAG_BYTES if content.bytesize < min_size raise(OpenSSL::PKey::RSAError, "corrupted or truncated envelope") end wrapped_key = content[VERSION_BYTES, wrapped_size] iv = content[VERSION_BYTES + wrapped_size, IV_BYTES] tag = content[header_size, TAG_BYTES] ciphertext = content[min_size..] aes_key = pem.decrypt(wrapped_key, OAEP_OPTIONS) # OAEP unwrapping of a foreign envelope fails reliably, but anyone holding the # public key can wrap a string of arbitrary length. Without this guard such input # would surface as an ArgumentError from the AES key assignment, escaping the # OpenSSL error family this method otherwise normalizes to unless aes_key.bytesize == KEY_BYTES raise(OpenSSL::PKey::RSAError, "invalid envelope key size") end aes = OpenSSL::Cipher.new(AES).decrypt aes.key = aes_key aes.iv = iv aes.auth_tag = tag aes.auth_data = content[0, header_size] # Same empty-input guard as on the encryption side; `#final` still runs and thus # still verifies the auth tag for empty payloads ciphertext.empty? ? aes.final : aes.update(ciphertext) + aes.final end |
#encrypt(content) ⇒ String
Encrypts content with a one-time AES-256-GCM key and RSA-wraps that key using OAEP padding. Unlike PKCS1 v1.5, OAEP unwrapping with a non-matching key fails reliably instead of occasionally yielding garbage. The GCM tag additionally authenticates the whole envelope header, so any bit flip in the version byte, wrapped key or iv is detected, not only ciphertext corruption.
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/karafka/pro/encryption/ciphers/envelope.rb', line 89 def encrypt(content) aes = OpenSSL::Cipher.new(AES).encrypt aes_key = aes.random_key iv = aes.random_iv wrapped_key = public_pem.encrypt(aes_key, OAEP_OPTIONS) header = VERSION + wrapped_key + iv aes.auth_data = header # `Cipher#update` rejects empty input on openssl gem < 3.1 (`data must not be # empty`), so empty payloads go straight to `#final` ciphertext = content.empty? ? aes.final : aes.update(content) + aes.final header + aes.auth_tag(TAG_BYTES) + ciphertext end |