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.}" 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.}" 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
-
.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
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_count ⇒ Object
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
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 |
.status ⇒ Object
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
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_cache ⇒ Object
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 |