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.is_a?(::String) begin parsed = Familia::JsonSerializer.parse(json_string, symbolize_names: true) return false unless parsed.is_a?(Hash) # Check for required fields required_fields = %i[algorithm nonce ciphertext auth_tag key_version] result = required_fields.all? { |field| parsed.key?(field) } Familia.debug "[valid?] result: #{result}, parsed: #{parsed}, required: #{required_fields}" result rescue Familia::SerializerError => e Familia.debug "[valid?] JSON error: #{e.}" false end end def self.validate!(json_string) return nil if json_string.nil? raise EncryptionError, "Expected JSON string, got #{json_string.class}" unless json_string.is_a?(::String) begin parsed = Familia::JsonSerializer.parse(json_string, symbolize_names: true) rescue Familia::SerializerError => e raise EncryptionError, "Invalid JSON structure: #{e.}" end raise EncryptionError, "Expected JSON object, got #{parsed.class}" unless parsed.is_a?(Hash) required_fields = %i[algorithm nonce ciphertext auth_tag key_version] missing_fields = required_fields.reject { |field| parsed.key?(field) } raise EncryptionError, "Missing required fields: #{missing_fields.join(', ')}" unless missing_fields.empty? new(**parsed) end def self.from_json(json_string_or_hash) # Support both JSON strings (legacy) and already-parsed Hashes (v2.0 deserialization) if json_string_or_hash.is_a?(Hash) # Already parsed - use directly parsed = json_string_or_hash # Symbolize keys if they're strings parsed = parsed.transform_keys(&:to_sym) if parsed.keys.first.is_a?(String) new(**parsed) else # JSON string - validate and parse validate!(json_string_or_hash) end 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! raise EncryptionError, 'Missing algorithm field' unless algorithm # Ensure Registry is set up before checking algorithms Registry.setup! if Registry.providers.empty? raise EncryptionError, "Unsupported algorithm: #{algorithm}" unless Registry.providers.key?(algorithm) 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
-
.benchmark(iterations: 1000) ⇒ Object
Benchmark available providers.
-
.clear_request_cache! ⇒ Object
Clear all cached keys and disable caching.
-
.decrypt(encrypted_json, context:, additional_data: nil) ⇒ Object
Quick decryption (auto-detects algorithm from data).
-
.derivation_count ⇒ Object
Derivation counter for monitoring no-caching behavior.
-
.encrypt(plaintext, context:, additional_data: nil) ⇒ Object
Quick encryption with auto-selected best provider.
-
.encrypt_with(algorithm, plaintext, context:, additional_data: nil) ⇒ Object
Encrypt with specific algorithm.
-
.hardware_acceleration? ⇒ Boolean
Check if we're using hardware acceleration.
-
.manager(algorithm: nil) ⇒ Object
Get or create a manager with specific algorithm.
- .reset_derivation_count! ⇒ Object
-
.secure_wipe(key) ⇒ Object
Clear key from memory (no security guarantees in Ruby).
-
.status ⇒ Object
Get info about current encryption setup.
- .validate_configuration! ⇒ Object
-
.with_request_cache ⇒ Object
Enable request-scoped caching (opt-in for performance).
Class Method Details
.benchmark(iterations: 1000) ⇒ Object
Benchmark available providers
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 |
# File 'lib/familia/encryption.rb', line 129 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
31 32 33 34 35 36 37 38 |
# File 'lib/familia/encryption/request_cache.rb', line 31 def clear_request_cache! if (cache = Fiber[:familia_request_cache]) cache.each_value { |key| secure_wipe(key) } cache.clear end Fiber[:familia_request_cache_enabled] = false Fiber[:familia_request_cache] = nil end |
.decrypt(encrypted_json, context:, additional_data: nil) ⇒ Object
Quick decryption (auto-detects algorithm from data)
81 82 83 |
# File 'lib/familia/encryption.rb', line 81 def decrypt(encrypted_json, context:, additional_data: nil) manager.decrypt(encrypted_json, context: context, additional_data: additional_data) end |
.derivation_count ⇒ Object
Derivation counter for monitoring no-caching behavior
95 96 97 |
# File 'lib/familia/encryption.rb', line 95 def derivation_count @derivation_count ||= Concurrent::AtomicFixnum.new(0) end |
.encrypt(plaintext, context:, additional_data: nil) ⇒ Object
Quick encryption with auto-selected best provider
76 77 78 |
# File 'lib/familia/encryption.rb', line 76 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
86 87 88 89 90 91 92 |
# File 'lib/familia/encryption.rb', line 86 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
123 124 125 126 |
# File 'lib/familia/encryption.rb', line 123 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
Thread-safe lazy initialization using Concurrent::Map to ensure only a single Manager instance is created per algorithm even under concurrent encryption/decryption requests.
70 71 72 73 |
# File 'lib/familia/encryption.rb', line 70 def manager(algorithm: nil) @managers ||= Concurrent::Map.new @managers.fetch_or_store(algorithm) { Manager.new(algorithm: algorithm) } end |
.reset_derivation_count! ⇒ Object
99 100 101 |
# File 'lib/familia/encryption.rb', line 99 def reset_derivation_count! derivation_count.value = 0 end |
.secure_wipe(key) ⇒ Object
Clear key from memory (no security guarantees in Ruby)
104 105 106 |
# File 'lib/familia/encryption.rb', line 104 def secure_wipe(key) key&.clear end |
.status ⇒ Object
Get info about current encryption setup
109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'lib/familia/encryption.rb', line 109 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
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
# File 'lib/familia/encryption.rb', line 156 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_cache ⇒ Object
Enable request-scoped caching (opt-in for performance)
22 23 24 25 26 27 28 |
# File 'lib/familia/encryption/request_cache.rb', line 22 def with_request_cache Fiber[:familia_request_cache_enabled] = true Fiber[:familia_request_cache] = {} yield ensure clear_request_cache! end |