Module: Familia::Encryption

Defined in:
lib/familia/encryption.rb,
lib/familia/encryption/manager.rb,
lib/familia/encryption/provider.rb,
lib/familia/encryption/registry.rb,
lib/familia/encryption_request_cache.rb,
lib/familia/encryption/encrypted_data.rb,
lib/familia/encryption/providers/aes_gcm_provider.rb,
lib/familia/encryption/providers/xchacha20_poly1305_provider.rb,
lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb

Defined Under Namespace

Modules: Providers Classes: Manager, Provider, Registry

Constant Summary collapse

EncryptedData =
Data.define(:algorithm, :nonce, :ciphertext, :auth_tag, :key_version) do
  # Class methods for parsing and validation
  def self.valid?(json_string)
    return true if json_string.nil?  # Allow nil values
    return false unless json_string.kind_of?(::String)

    begin
      parsed = JSON.parse(json_string, symbolize_names: true)
      return false unless parsed.is_a?(Hash)

      # Check for required fields
      required_fields = [:algorithm, :nonce, :ciphertext, :auth_tag, :key_version]
      result = required_fields.all? { |field| parsed.key?(field) }
      Familia.ld "[valid?] result: #{result}, parsed: #{parsed}, required: #{required_fields}"
      result
    rescue JSON::ParserError => e
      Familia.ld "[valid?] JSON error: #{e.message}"
      false
    end
  end

  def self.validate!(json_string)
    return nil if json_string.nil?

    unless json_string.kind_of?(::String)
      raise EncryptionError, "Expected JSON string, got #{json_string.class}"
    end

    begin
      parsed = JSON.parse(json_string, symbolize_names: true)
    rescue JSON::ParserError => e
      raise EncryptionError, "Invalid JSON structure: #{e.message}"
    end

    unless parsed.is_a?(Hash)
      raise EncryptionError, "Expected JSON object, got #{parsed.class}"
    end

    required_fields = [:algorithm, :nonce, :ciphertext, :auth_tag, :key_version]
    missing_fields = required_fields.reject { |field| parsed.key?(field) }

    unless missing_fields.empty?
      raise EncryptionError, "Missing required fields: #{missing_fields.join(', ')}"
    end

    new(**parsed)
  end

  def self.from_json(json_string)
    validate!(json_string)
  end

  # Instance methods for decryptability validation
  def decryptable?
    return false unless algorithm && nonce && ciphertext && auth_tag && key_version

    # Ensure Registry is set up before checking algorithms
    Registry.setup! if Registry.providers.empty?

    # Check if algorithm is supported
    return false unless Registry.providers.key?(algorithm)

    # Validate Base64 encoding of binary fields
    begin
      Base64.strict_decode64(nonce)
      Base64.strict_decode64(ciphertext)
      Base64.strict_decode64(auth_tag)
    rescue ArgumentError
      return false
    end

    true
  end

  def validate_decryptable!
    unless algorithm
      raise EncryptionError, "Missing algorithm field"
    end

    # Ensure Registry is set up before checking algorithms
    Registry.setup! if Registry.providers.empty?

    unless Registry.providers.key?(algorithm)
      raise EncryptionError, "Unsupported algorithm: #{algorithm}"
    end

    unless nonce && ciphertext && auth_tag && key_version
      missing = []
      missing << 'nonce' unless nonce
      missing << 'ciphertext' unless ciphertext
      missing << 'auth_tag' unless auth_tag
      missing << 'key_version' unless key_version
      raise EncryptionError, "Missing required fields: #{missing.join(', ')}"
    end

    # Get the provider for size validation
    provider = Registry.providers[algorithm]

    # Validate Base64 encoding and sizes
    begin
      decoded_nonce = Base64.strict_decode64(nonce)
      if decoded_nonce.bytesize != provider.nonce_size
        raise EncryptionError, "Invalid nonce size: expected #{provider.nonce_size}, got #{decoded_nonce.bytesize}"
      end
    rescue ArgumentError
      raise EncryptionError, "Invalid Base64 encoding in nonce field"
    end

    begin
      Base64.strict_decode64(ciphertext)  # ciphertext can be variable size
    rescue ArgumentError
      raise EncryptionError, "Invalid Base64 encoding in ciphertext field"
    end

    begin
      decoded_auth_tag = Base64.strict_decode64(auth_tag)
      if decoded_auth_tag.bytesize != provider.auth_tag_size
        raise EncryptionError, "Invalid auth_tag size: expected #{provider.auth_tag_size}, got #{decoded_auth_tag.bytesize}"
      end
    rescue ArgumentError
      raise EncryptionError, "Invalid Base64 encoding in auth_tag field"
    end

    # Validate that the key version exists
    unless Familia.config.encryption_keys&.key?(key_version.to_sym)
      raise EncryptionError, "No key for version: #{key_version}"
    end

    self
  end
end

Class Method Summary collapse

Class Method Details

.benchmark(iterations: 1000) ⇒ Object

Benchmark available providers



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
# File 'lib/familia/encryption.rb', line 123

def benchmark(iterations: 1000)
  require 'benchmark'
  test_data = 'x' * 1024 # 1KB test
  context = 'benchmark:test'

  results = {}
  Registry.providers.each do |algo, provider_class|
    next unless provider_class.available?

    mgr = Manager.new(algorithm: algo)
    time = Benchmark.realtime do
      iterations.times do
        encrypted = mgr.encrypt(test_data, context: context)
        mgr.decrypt(encrypted, context: context)
      end
    end

    results[algo] = {
      time: time,
      ops_per_sec: (iterations * 2 / time).round,
      priority: provider_class.priority
    }
  end

  results
end

.clear_request_cache!Object

Clear all cached keys and disable caching



29
30
31
32
33
34
35
36
# File 'lib/familia/encryption_request_cache.rb', line 29

def clear_request_cache!
  if (cache = Thread.current[:familia_request_cache])
    cache.each_value { |key| secure_wipe(key) }
    cache.clear
  end
  Thread.current[:familia_request_cache_enabled] = false
  Thread.current[:familia_request_cache] = nil
end

.decrypt(encrypted_json, context:, additional_data: nil) ⇒ Object

Quick decryption (auto-detects algorithm from data)



75
76
77
# File 'lib/familia/encryption.rb', line 75

def decrypt(encrypted_json, context:, additional_data: nil)
  manager.decrypt(encrypted_json, context: context, additional_data: additional_data)
end

.derivation_countObject

Derivation counter for monitoring no-caching behavior



89
90
91
# File 'lib/familia/encryption.rb', line 89

def derivation_count
  @derivation_count ||= Concurrent::AtomicFixnum.new(0)
end

.encrypt(plaintext, context:, additional_data: nil) ⇒ Object

Quick encryption with auto-selected best provider



70
71
72
# File 'lib/familia/encryption.rb', line 70

def encrypt(plaintext, context:, additional_data: nil)
  manager.encrypt(plaintext, context: context, additional_data: additional_data)
end

.encrypt_with(algorithm, plaintext, context:, additional_data: nil) ⇒ Object

Encrypt with specific algorithm



80
81
82
83
84
85
86
# File 'lib/familia/encryption.rb', line 80

def encrypt_with(algorithm, plaintext, context:, additional_data: nil)
  manager(algorithm: algorithm).encrypt(
    plaintext,
    context: context,
    additional_data: additional_data
  )
end

.hardware_acceleration?Boolean

Check if we’re using hardware acceleration

Returns:

  • (Boolean)


117
118
119
120
# File 'lib/familia/encryption.rb', line 117

def hardware_acceleration?
  provider = Registry.default_provider
  provider && provider.class.name.include?('Hardware')
end

.manager(algorithm: nil) ⇒ Object

Get or create a manager with specific algorithm



64
65
66
67
# File 'lib/familia/encryption.rb', line 64

def manager(algorithm: nil)
  @managers ||= {}
  @managers[algorithm] ||= Manager.new(algorithm: algorithm)
end

.reset_derivation_count!Object



93
94
95
# File 'lib/familia/encryption.rb', line 93

def reset_derivation_count!
  derivation_count.value = 0
end

.secure_wipe(key) ⇒ Object

Clear key from memory (no security guarantees in Ruby)



98
99
100
# File 'lib/familia/encryption.rb', line 98

def secure_wipe(key)
  key&.clear
end

.statusObject

Get info about current encryption setup



103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/familia/encryption.rb', line 103

def status
  Registry.setup! if Registry.providers.empty?

  {
    default_algorithm: Registry.default_provider&.algorithm,
    available_algorithms: Registry.available_algorithms,
    preferred_available: Registry.default_provider&.class&.name,
    using_hardware: hardware_acceleration?,
    key_versions: encryption_keys.keys,
    current_version: current_key_version
  }
end

.validate_configuration!Object

Raises:



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/familia/encryption.rb', line 150

def validate_configuration!
  raise EncryptionError, 'No encryption keys configured' if encryption_keys.empty?
  raise EncryptionError, 'No current key version set' unless current_key_version

  current_key = encryption_keys[current_key_version]
  raise EncryptionError, "Current key version not found: #{current_key_version}" unless current_key

  begin
    Base64.strict_decode64(current_key)
  rescue ArgumentError
    raise EncryptionError, 'Current encryption key is not valid Base64'
  end

  Registry.setup!
  raise EncryptionError, 'No encryption providers available' unless Registry.default_provider
end

.with_request_cacheObject

Enable request-scoped caching (opt-in for performance)



20
21
22
23
24
25
26
# File 'lib/familia/encryption_request_cache.rb', line 20

def with_request_cache
  Thread.current[:familia_request_cache_enabled] = true
  Thread.current[:familia_request_cache] = {}
  yield
ensure
  clear_request_cache!
end