Class: Familia::Encryption::Providers::AESGCMProvider
- Inherits:
-
Familia::Encryption::Provider
- Object
- Familia::Encryption::Provider
- Familia::Encryption::Providers::AESGCMProvider
- Defined in:
- lib/familia/encryption/providers/aes_gcm_provider.rb
Constant Summary collapse
- ALGORITHM =
'aes-256-gcm'.freeze
- NONCE_SIZE =
12- AUTH_TAG_SIZE =
16- LEGACY_HKDF_SALT =
The HKDF salt used before issue #310, when the salt was a static literal. Retained ONLY as a decryption fallback (see #hkdf_salts) so data written before the salt became application-specific stays readable after upgrading. Never used to encrypt new data.
'FamiliaEncryption'.freeze
Class Method Summary collapse
Instance Method Summary collapse
- #algorithm ⇒ Object
- #auth_tag_size ⇒ Object
-
#current_hkdf_salt ⇒ Object
The HKDF salt used to ENCRYPT new data.
- #decrypt(ciphertext, key, nonce, auth_tag, additional_data = nil) ⇒ Object
- #derive_key(master_key, context, personal: nil, salt: nil) ⇒ Object
- #encrypt(plaintext, key, additional_data = nil) ⇒ Object
- #generate_nonce ⇒ Object
-
#hkdf_salts ⇒ Object
Ordered list of HKDF salts to consider when DECRYPTING, current first.
- #nonce_size ⇒ Object
-
#secure_wipe(key) ⇒ Object
Clear key from memory (no security guarantees in Ruby).
Constructor Details
This class inherits a constructor from Familia::Encryption::Provider
Class Method Details
.auth_tag_size ⇒ Object
156 157 158 |
# File 'lib/familia/encryption/providers/aes_gcm_provider.rb', line 156 def self.auth_tag_size AUTH_TAG_SIZE end |
.available? ⇒ Boolean
33 34 35 |
# File 'lib/familia/encryption/providers/aes_gcm_provider.rb', line 33 def self.available? true # OpenSSL is always available end |
.nonce_size ⇒ Object
152 153 154 |
# File 'lib/familia/encryption/providers/aes_gcm_provider.rb', line 152 def self.nonce_size NONCE_SIZE end |
.priority ⇒ Object
37 38 39 |
# File 'lib/familia/encryption/providers/aes_gcm_provider.rb', line 37 def self.priority 50 # Fallback option end |
Instance Method Details
#algorithm ⇒ Object
168 169 170 |
# File 'lib/familia/encryption/providers/aes_gcm_provider.rb', line 168 def algorithm ALGORITHM end |
#auth_tag_size ⇒ Object
164 165 166 |
# File 'lib/familia/encryption/providers/aes_gcm_provider.rb', line 164 def auth_tag_size AUTH_TAG_SIZE end |
#current_hkdf_salt ⇒ Object
The HKDF salt used to ENCRYPT new data.
Unlike #hkdf_salts (the permissive decryption candidate list, which deliberately ends with the pre-#310 global static salt so old ciphertext stays readable), this fails CLOSED. A nil or empty encryption_hkdf_salt is a misconfiguration -- and the raw attr_writer can set one, bypassing the reader's guards -- so silently encrypting new data under the legacy global static salt would quietly withhold the #310 per-deployment domain separation. Refuse instead, so the operator notices (#311). To use the old global salt on purpose, set it explicitly as a non-empty string.
113 114 115 116 117 118 119 120 121 |
# File 'lib/familia/encryption/providers/aes_gcm_provider.rb', line 113 def current_hkdf_salt salt = Familia.config.encryption_hkdf_salt return salt if salt.is_a?(String) && !salt.empty? raise EncryptionError, 'encryption_hkdf_salt must be a non-empty string to encrypt; refusing to ' \ 'fall back to the legacy global HKDF salt. Set Familia.config.encryption_hkdf_salt ' \ 'for per-deployment domain separation.' end |
#decrypt(ciphertext, key, nonce, auth_tag, additional_data = nil) ⇒ Object
58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/familia/encryption/providers/aes_gcm_provider.rb', line 58 def decrypt(ciphertext, key, nonce, auth_tag, additional_data = nil) validate_key_length!(key) cipher = create_cipher(:decrypt) cipher.key = key cipher.iv = nonce cipher.auth_tag = auth_tag cipher.auth_data = additional_data.to_s if additional_data cipher.update(ciphertext) + cipher.final rescue OpenSSL::Cipher::CipherError raise EncryptionError, 'Decryption failed - invalid key or corrupted data' end |
#derive_key(master_key, context, personal: nil, salt: nil) ⇒ Object
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
# File 'lib/familia/encryption/providers/aes_gcm_provider.rb', line 123 def derive_key(master_key, context, personal: nil, salt: nil) validate_key_length!(master_key) info = personal ? "#{context}:#{personal}" : context OpenSSL::KDF.hkdf( master_key, # Use application-specific material for the HKDF salt instead of a # static library literal. A fixed global salt is shared by every # deployment and weakens HKDF's extraction step / domain separation # (RFC 5869). The value comes from encryption_hkdf_salt -- a dedicated # AES-GCM knob, kept separate from the XChaCha20 personalization so # neither cipher family constrains the other (issue #311). See #310 (S2). # # `salt` defaults to the current encryption salt (#current_hkdf_salt), # which fails closed on a nil/blank config rather than silently using # the legacy global static salt. The decrypt path passes explicit # candidate salts from #hkdf_salts so existing ciphertext stays # decryptable after a salt change. salt: salt || current_hkdf_salt, info: info, length: 32, hash: 'SHA256' ) end |
#encrypt(plaintext, key, additional_data = nil) ⇒ Object
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
# File 'lib/familia/encryption/providers/aes_gcm_provider.rb', line 41 def encrypt(plaintext, key, additional_data = nil) validate_key_length!(key) nonce = generate_nonce cipher = create_cipher(:encrypt) cipher.key = key cipher.iv = nonce cipher.auth_data = additional_data.to_s if additional_data ciphertext = cipher.update(plaintext.to_s) + cipher.final { ciphertext: ciphertext, auth_tag: cipher.auth_tag, nonce: nonce, } end |
#generate_nonce ⇒ Object
71 72 73 |
# File 'lib/familia/encryption/providers/aes_gcm_provider.rb', line 71 def generate_nonce OpenSSL::Random.random_bytes(NONCE_SIZE) end |
#hkdf_salts ⇒ Object
Ordered list of HKDF salts to consider when DECRYPTING, current first.
Decryption walks this list until the authenticated decrypt succeeds, so it is intentionally permissive: it ends with the pre-#310 static salt and tolerates a blank current salt (dropped by #compact), so existing ciphertext stays readable even if the current config is broken. Each wrong salt yields a different key and fails GCM authentication cleanly, so trying them in turn never produces a false positive.
ENCRYPTION does NOT use this list's head -- see #current_hkdf_salt, which fails closed rather than silently encrypting under the legacy static salt.
The salt comes from a dedicated config knob (encryption_hkdf_salt), NOT the XChaCha20 personalization. HKDF accepts a salt of any length while BLAKE2b personalization is capped at 16 bytes, so the two cipher families keep separate inputs and never constrain each other (#311).
97 98 99 100 101 |
# File 'lib/familia/encryption/providers/aes_gcm_provider.rb', line 97 def hkdf_salts current = Familia.config.encryption_hkdf_salt history = Familia.config.encryption_hkdf_salt_history [current, *history, LEGACY_HKDF_SALT].compact.uniq end |
#nonce_size ⇒ Object
160 161 162 |
# File 'lib/familia/encryption/providers/aes_gcm_provider.rb', line 160 def nonce_size NONCE_SIZE end |
#secure_wipe(key) ⇒ Object
Clear key from memory (no security guarantees in Ruby)
148 149 150 |
# File 'lib/familia/encryption/providers/aes_gcm_provider.rb', line 148 def secure_wipe(key) key&.clear end |