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.ld "[valid?] result: #{result}, parsed: #{parsed}, required: #{required_fields}" result rescue Familia::SerializerError => e Familia.ld "[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) 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! 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
122 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 |
# File 'lib/familia/encryption.rb', line 122 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 = 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)
74 75 76 |
# File 'lib/familia/encryption.rb', line 74 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
88 89 90 |
# File 'lib/familia/encryption.rb', line 88 def derivation_count @derivation_count ||= Concurrent::AtomicFixnum.new(0) end |
.encrypt(plaintext, context:, additional_data: nil) ⇒ Object
Quick encryption with auto-selected best provider
69 70 71 |
# File 'lib/familia/encryption.rb', line 69 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
79 80 81 82 83 84 85 |
# File 'lib/familia/encryption.rb', line 79 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
116 117 118 119 |
# File 'lib/familia/encryption.rb', line 116 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
63 64 65 66 |
# File 'lib/familia/encryption.rb', line 63 def manager(algorithm: nil) @managers ||= {} @managers[algorithm] ||= Manager.new(algorithm: algorithm) end |
.reset_derivation_count! ⇒ Object
92 93 94 |
# File 'lib/familia/encryption.rb', line 92 def reset_derivation_count! derivation_count.value = 0 end |
.secure_wipe(key) ⇒ Object
Clear key from memory (no security guarantees in Ruby)
97 98 99 |
# File 'lib/familia/encryption.rb', line 97 def secure_wipe(key) key&.clear end |
.status ⇒ Object
Get info about current encryption setup
102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/familia/encryption.rb', line 102 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
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/familia/encryption.rb', line 149 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 Fiber[:familia_request_cache_enabled] = true Fiber[:familia_request_cache] = {} yield ensure clear_request_cache! end |