Class: Familia::Encryption::Providers::AESGCMProvider

Inherits:
Familia::Encryption::Provider show all
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

Constructor Details

This class inherits a constructor from Familia::Encryption::Provider

Class Method Details

.auth_tag_sizeObject



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

Returns:

  • (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_sizeObject



152
153
154
# File 'lib/familia/encryption/providers/aes_gcm_provider.rb', line 152

def self.nonce_size
  NONCE_SIZE
end

.priorityObject



37
38
39
# File 'lib/familia/encryption/providers/aes_gcm_provider.rb', line 37

def self.priority
  50 # Fallback option
end

Instance Method Details

#algorithmObject



168
169
170
# File 'lib/familia/encryption/providers/aes_gcm_provider.rb', line 168

def algorithm
  ALGORITHM
end

#auth_tag_sizeObject



164
165
166
# File 'lib/familia/encryption/providers/aes_gcm_provider.rb', line 164

def auth_tag_size
  AUTH_TAG_SIZE
end

#current_hkdf_saltObject

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.

Raises:



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_nonceObject



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_saltsObject

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_sizeObject



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