familia 2.0.0.pre3 → 2.0.0.pre5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rubocop_todo.yml +17 -17
  4. data/CLAUDE.md +3 -3
  5. data/Gemfile +5 -1
  6. data/Gemfile.lock +18 -3
  7. data/README.md +36 -157
  8. data/TEST_COVERAGE.md +40 -0
  9. data/docs/overview.md +359 -0
  10. data/docs/wiki/API-Reference.md +270 -0
  11. data/docs/wiki/Encrypted-Fields-Overview.md +64 -0
  12. data/docs/wiki/Home.md +49 -0
  13. data/docs/wiki/Implementation-Guide.md +183 -0
  14. data/docs/wiki/Security-Model.md +143 -0
  15. data/lib/familia/base.rb +18 -27
  16. data/lib/familia/connection.rb +6 -5
  17. data/lib/familia/{datatype → data_type}/commands.rb +2 -5
  18. data/lib/familia/{datatype → data_type}/serialization.rb +8 -10
  19. data/lib/familia/{datatype → data_type}/types/hashkey.rb +2 -2
  20. data/lib/familia/{datatype → data_type}/types/list.rb +17 -18
  21. data/lib/familia/{datatype → data_type}/types/sorted_set.rb +17 -17
  22. data/lib/familia/{datatype → data_type}/types/string.rb +2 -1
  23. data/lib/familia/{datatype → data_type}/types/unsorted_set.rb +17 -18
  24. data/lib/familia/{datatype.rb → data_type.rb} +10 -12
  25. data/lib/familia/encryption/manager.rb +102 -0
  26. data/lib/familia/encryption/provider.rb +49 -0
  27. data/lib/familia/encryption/providers/aes_gcm_provider.rb +103 -0
  28. data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +184 -0
  29. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +118 -0
  30. data/lib/familia/encryption/registry.rb +50 -0
  31. data/lib/familia/encryption.rb +178 -0
  32. data/lib/familia/encryption_request_cache.rb +68 -0
  33. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +153 -0
  34. data/lib/familia/features/encrypted_fields.rb +28 -0
  35. data/lib/familia/features/expiration.rb +107 -77
  36. data/lib/familia/features/quantization.rb +5 -9
  37. data/lib/familia/features/relatable_objects.rb +2 -4
  38. data/lib/familia/features/safe_dump.rb +14 -17
  39. data/lib/familia/features/transient_fields/redacted_string.rb +159 -0
  40. data/lib/familia/features/transient_fields/single_use_redacted_string.rb +62 -0
  41. data/lib/familia/features/transient_fields/transient_field_type.rb +139 -0
  42. data/lib/familia/features/transient_fields.rb +47 -0
  43. data/lib/familia/features.rb +40 -24
  44. data/lib/familia/field_type.rb +270 -0
  45. data/lib/familia/horreum/connection.rb +8 -11
  46. data/lib/familia/horreum/{commands.rb → database_commands.rb} +7 -19
  47. data/lib/familia/horreum/definition_methods.rb +453 -0
  48. data/lib/familia/horreum/{class_methods.rb → management_methods.rb} +19 -229
  49. data/lib/familia/horreum/serialization.rb +46 -18
  50. data/lib/familia/horreum/settings.rb +10 -2
  51. data/lib/familia/horreum/utils.rb +9 -10
  52. data/lib/familia/horreum.rb +18 -10
  53. data/lib/familia/logging.rb +14 -14
  54. data/lib/familia/settings.rb +39 -3
  55. data/lib/familia/utils.rb +45 -0
  56. data/lib/familia/version.rb +1 -1
  57. data/lib/familia.rb +2 -1
  58. data/try/core/base_enhancements_try.rb +115 -0
  59. data/try/core/connection_try.rb +0 -1
  60. data/try/core/errors_try.rb +0 -1
  61. data/try/core/familia_extended_try.rb +3 -4
  62. data/try/core/familia_try.rb +0 -1
  63. data/try/core/pools_try.rb +2 -2
  64. data/try/core/secure_identifier_try.rb +0 -1
  65. data/try/core/settings_try.rb +0 -1
  66. data/try/core/utils_try.rb +0 -1
  67. data/try/{datatypes → data_types}/boolean_try.rb +1 -2
  68. data/try/{datatypes → data_types}/datatype_base_try.rb +2 -3
  69. data/try/{datatypes → data_types}/hash_try.rb +1 -2
  70. data/try/{datatypes → data_types}/list_try.rb +1 -2
  71. data/try/{datatypes → data_types}/set_try.rb +1 -2
  72. data/try/{datatypes → data_types}/sorted_set_try.rb +1 -2
  73. data/try/{datatypes → data_types}/string_try.rb +1 -2
  74. data/try/debugging/README.md +32 -0
  75. data/try/debugging/cache_behavior_tracer.rb +91 -0
  76. data/try/debugging/encryption_method_tracer.rb +138 -0
  77. data/try/debugging/provider_diagnostics.rb +110 -0
  78. data/try/edge_cases/hash_symbolization_try.rb +0 -1
  79. data/try/edge_cases/json_serialization_try.rb +0 -1
  80. data/try/edge_cases/reserved_keywords_try.rb +42 -11
  81. data/try/encryption/config_persistence_try.rb +192 -0
  82. data/try/encryption/encryption_core_try.rb +328 -0
  83. data/try/encryption/instance_variable_scope_try.rb +31 -0
  84. data/try/encryption/module_loading_try.rb +28 -0
  85. data/try/encryption/providers/aes_gcm_provider_try.rb +178 -0
  86. data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +169 -0
  87. data/try/encryption/roundtrip_validation_try.rb +28 -0
  88. data/try/encryption/secure_memory_handling_try.rb +125 -0
  89. data/try/features/encrypted_fields_core_try.rb +117 -0
  90. data/try/features/encrypted_fields_integration_try.rb +220 -0
  91. data/try/features/encrypted_fields_no_cache_security_try.rb +205 -0
  92. data/try/features/encrypted_fields_security_try.rb +370 -0
  93. data/try/features/encryption_fields/aad_protection_try.rb +53 -0
  94. data/try/features/encryption_fields/context_isolation_try.rb +120 -0
  95. data/try/features/encryption_fields/error_conditions_try.rb +116 -0
  96. data/try/features/encryption_fields/fresh_key_derivation_try.rb +122 -0
  97. data/try/features/encryption_fields/fresh_key_try.rb +163 -0
  98. data/try/features/encryption_fields/key_rotation_try.rb +117 -0
  99. data/try/features/encryption_fields/memory_security_try.rb +37 -0
  100. data/try/features/encryption_fields/missing_current_key_version_try.rb +23 -0
  101. data/try/features/encryption_fields/nonce_uniqueness_try.rb +54 -0
  102. data/try/features/encryption_fields/thread_safety_try.rb +199 -0
  103. data/try/features/expiration_try.rb +0 -1
  104. data/try/features/feature_dependencies_try.rb +159 -0
  105. data/try/features/quantization_try.rb +0 -1
  106. data/try/features/real_feature_integration_try.rb +148 -0
  107. data/try/features/relatable_objects_try.rb +0 -1
  108. data/try/features/safe_dump_advanced_try.rb +0 -1
  109. data/try/features/safe_dump_try.rb +0 -1
  110. data/try/features/transient_fields/redacted_string_try.rb +248 -0
  111. data/try/features/transient_fields/refresh_reset_try.rb +164 -0
  112. data/try/features/transient_fields/simple_refresh_test.rb +50 -0
  113. data/try/features/transient_fields/single_use_redacted_string_try.rb +310 -0
  114. data/try/features/transient_fields_core_try.rb +181 -0
  115. data/try/features/transient_fields_integration_try.rb +260 -0
  116. data/try/helpers/test_helpers.rb +42 -0
  117. data/try/horreum/base_try.rb +157 -3
  118. data/try/horreum/class_methods_try.rb +27 -36
  119. data/try/horreum/enhanced_conflict_handling_try.rb +176 -0
  120. data/try/horreum/field_categories_try.rb +118 -0
  121. data/try/horreum/field_definition_try.rb +96 -0
  122. data/try/horreum/initialization_try.rb +0 -1
  123. data/try/horreum/relations_try.rb +0 -1
  124. data/try/horreum/serialization_persistent_fields_try.rb +165 -0
  125. data/try/horreum/serialization_try.rb +2 -3
  126. data/try/memory/memory_basic_test.rb +73 -0
  127. data/try/memory/memory_detailed_test.rb +121 -0
  128. data/try/memory/memory_docker_ruby_dump.sh +80 -0
  129. data/try/memory/memory_search_for_string.rb +83 -0
  130. data/try/memory/test_actual_redactedstring_protection.rb +38 -0
  131. data/try/models/customer_safe_dump_try.rb +0 -1
  132. data/try/models/customer_try.rb +0 -1
  133. data/try/models/datatype_base_try.rb +1 -2
  134. data/try/models/familia_object_try.rb +0 -1
  135. metadata +85 -18
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env ruby
2
+ # Debug script for testing encryption providers and diagnosing issues
3
+
4
+ require 'base64'
5
+ require_relative '../helpers/test_helpers'
6
+
7
+ puts "=== Familia Encryption Provider Diagnostics ==="
8
+ puts
9
+
10
+ # Check encryption system status
11
+ puts "1. Encryption System Status:"
12
+ puts " Status: #{Familia::Encryption.status.inspect}"
13
+ puts
14
+
15
+ # Check registry setup
16
+ puts "2. Registry Providers:"
17
+ require_relative '../../lib/familia/encryption/registry'
18
+ Familia::Encryption::Registry.setup!
19
+ Familia::Encryption::Registry.providers.each do |algo, provider_class|
20
+ puts " #{algo}: #{provider_class.name}"
21
+ puts " Available: #{provider_class.available?}"
22
+ puts " Priority: #{provider_class.priority}"
23
+ end
24
+ puts
25
+
26
+ # Setup test keys
27
+ test_keys = { v1: Base64.strict_encode64('a' * 32) }
28
+ Familia.config.encryption_keys = test_keys
29
+ Familia.config.current_key_version = :v1
30
+
31
+ # Test each provider individually
32
+ ['xchacha20poly1305', 'aes-256-gcm'].each do |algorithm|
33
+ puts "3. Testing #{algorithm.upcase} Provider:"
34
+
35
+ begin
36
+ manager = Familia::Encryption::Manager.new(algorithm: algorithm)
37
+ provider = manager.provider
38
+
39
+ puts " Provider class: #{provider.class.name}"
40
+ puts " Algorithm: #{provider.algorithm}"
41
+ puts " Nonce size: #{provider.nonce_size} bytes"
42
+ puts " Auth tag size: #{provider.auth_tag_size} bytes"
43
+
44
+ # Test encryption/decryption
45
+ test_data = "diagnostic test data for #{algorithm}"
46
+ encrypted = manager.encrypt(test_data, context: 'diagnostics')
47
+ puts " Encryption: SUCCESS (#{encrypted.length} chars)"
48
+
49
+ decrypted = manager.decrypt(encrypted, context: 'diagnostics')
50
+ success = decrypted == test_data
51
+ puts " Decryption: #{success ? 'SUCCESS' : 'FAILED'}"
52
+
53
+ if !success
54
+ puts " Expected: #{test_data.inspect}"
55
+ puts " Got: #{decrypted.inspect}"
56
+ end
57
+
58
+ rescue => e
59
+ puts " ERROR: #{e.class}: #{e.message}"
60
+ puts " Backtrace: #{e.backtrace.first(3).join(', ')}"
61
+ end
62
+
63
+ puts
64
+ end
65
+
66
+ # Test cross-algorithm compatibility
67
+ puts "4. Cross-Algorithm Compatibility Test:"
68
+ begin
69
+ xchacha_manager = Familia::Encryption::Manager.new(algorithm: 'xchacha20poly1305')
70
+ aes_manager = Familia::Encryption::Manager.new(algorithm: 'aes-256-gcm')
71
+ default_manager = Familia::Encryption::Manager.new
72
+
73
+ test_data = "cross-algorithm test"
74
+
75
+ # Encrypt with XChaCha20Poly1305
76
+ xchacha_encrypted = xchacha_manager.encrypt(test_data, context: 'cross-test')
77
+ xchacha_decrypted = default_manager.decrypt(xchacha_encrypted, context: 'cross-test')
78
+ puts " XChaCha20Poly1305 -> Default: #{xchacha_decrypted == test_data ? 'SUCCESS' : 'FAILED'}"
79
+
80
+ # Encrypt with AES-GCM
81
+ aes_encrypted = aes_manager.encrypt(test_data, context: 'cross-test')
82
+ aes_decrypted = default_manager.decrypt(aes_encrypted, context: 'cross-test')
83
+ puts " AES-GCM -> Default: #{aes_decrypted == test_data ? 'SUCCESS' : 'FAILED'}"
84
+
85
+ rescue => e
86
+ puts " ERROR: #{e.class}: #{e.message}"
87
+ end
88
+ puts
89
+
90
+ # Test high-level API
91
+ puts "5. High-Level API Test:"
92
+ begin
93
+ encrypted_high = Familia::Encryption.encrypt_with('aes-256-gcm', 'high-level test', context: 'api-test')
94
+ puts " encrypt_with: SUCCESS"
95
+
96
+ # Parse encrypted data to verify structure
97
+ require 'json'
98
+ parsed = JSON.parse(encrypted_high, symbolize_names: true)
99
+ puts " Algorithm stored: #{parsed[:algorithm]}"
100
+ puts " Key version: #{parsed[:key_version]}"
101
+
102
+ decrypted_high = Familia::Encryption.decrypt(encrypted_high, context: 'api-test')
103
+ puts " decrypt: #{decrypted_high == 'high-level test' ? 'SUCCESS' : 'FAILED'}"
104
+
105
+ rescue => e
106
+ puts " ERROR: #{e.class}: #{e.message}"
107
+ end
108
+
109
+ puts
110
+ puts "=== Diagnostics Complete ==="
@@ -4,7 +4,6 @@
4
4
  # bug in Tryouts 3.1 that prevents the setup instance vars from
5
5
  # being available to the testcases.
6
6
 
7
- require_relative '../../lib/familia'
8
7
  require_relative '../helpers/test_helpers'
9
8
 
10
9
  Familia.debug = false
@@ -1,6 +1,5 @@
1
1
  # try/edge_cases/json_serialization_try.rb
2
2
 
3
- require_relative '../../lib/familia'
4
3
  require_relative '../helpers/test_helpers'
5
4
 
6
5
  Familia.debug = false
@@ -17,7 +17,7 @@ result
17
17
  #=!> StandardError
18
18
 
19
19
  ## prefixed field names work as expected
20
- TestClass2 = Class.new(Familia::Horreum) do
20
+ ExampleTestClass = Class.new(Familia::Horreum) do
21
21
  identifier_field :email
22
22
  field :email
23
23
  field :secret_ttl
@@ -25,7 +25,7 @@ TestClass2 = Class.new(Familia::Horreum) do
25
25
  field :dbclient_config
26
26
  end
27
27
 
28
- user = TestClass2.new(email: 'test@example.com')
28
+ user = ExampleTestClass.new(email: 'test@example.com')
29
29
  user.secret_ttl = 3600
30
30
  user.user_db = 5
31
31
  user.dbclient_config = { host: 'localhost' }
@@ -39,7 +39,7 @@ user.delete!
39
39
  result
40
40
  #=> true
41
41
 
42
- ## reserved methods still work normally
42
+ ## Reserved methods still work normally
43
43
  TestClass3 = Class.new(Familia::Horreum) do
44
44
  # Note: Does not enable expiration feature
45
45
  identifier_field :email
@@ -55,20 +55,51 @@ user
55
55
  #==> _.respond_to?(:logical_database)
56
56
  #==> _.respond_to?(:dbclient)
57
57
 
58
+ ## Attempting to pass default_expiration as a field value when instantiating,
59
+ ## when expiration feature is enabled. It doesn't actually change the default
60
+ ## expiration for the instance b/c "default_expiration" is not a regular field.
61
+ TestClassWithExpirationEnabled1 = Class.new(Familia::Horreum) do
62
+ feature :expiration
63
+ identifier_field :email
64
+ field :email
65
+ end
66
+
67
+ user = TestClassWithExpirationEnabled1.new(email: 'test@example.com', default_expiration: 3600)
68
+ user.default_expiration
69
+ #=> 0
58
70
 
59
- ## Attempting to set default_expiration on an instance with expiration feature enabled
60
- TestClass4 = Class.new(Familia::Horreum) do
71
+ ## Attempting to set default_expiration for an instance when
72
+ ## the feature is enabled should work
73
+ TestClassWithExpirationEnabled2 = Class.new(Familia::Horreum) do
61
74
  feature :expiration
62
75
  identifier_field :email
63
76
  field :email
64
- field :default_expiration
65
77
  end
66
78
 
67
- user = TestClass4.new(email: 'test@example.com', default_expiration: 3600)
68
- user.save
69
- user.delete!
70
- user
71
- #==> _.default_expiration == 3600
79
+ user = TestClassWithExpirationEnabled2.new(email: 'test@example.com')
80
+ user.default_expiration = 3601
81
+ user.default_expiration
82
+ #=> 3601
83
+
84
+ ## Attempting to pass default_expiration as a field value when instantiating,
85
+ ## when expiration feature is disabled and then trying to access that value
86
+ ## simply raises a NoMethodError error.
87
+ TestClassWithExpirationDisabled = Class.new(Familia::Horreum) do
88
+ identifier_field :email
89
+ field :email
90
+ end
91
+
92
+ user = TestClassWithExpirationDisabled.new(email: 'test@example.com', default_expiration: 3600)
93
+ user.default_expiration
94
+ #=!> NoMethodError
95
+
96
+ ## Attempting to add a field with a reserved name should raise an error
97
+ TestClassWithExpirationDisabled = Class.new(Familia::Horreum) do
98
+ identifier_field :email
99
+ field :email
100
+ field :default_expiration
101
+ end
102
+ ##=!> NoMethodError
72
103
 
73
104
  ## prefixed field names work as expected
74
105
  TestClass5 = Class.new(Familia::Horreum) do
@@ -0,0 +1,192 @@
1
+ # try/encryption/debug2_try.rb
2
+
3
+ # - Tests configuration persistence between test sections
4
+ # - Validates that config can be set and accessed in tryouts
5
+
6
+ require 'base64'
7
+
8
+ require_relative '../helpers/test_helpers'
9
+ require 'familia/encryption/providers/xchacha20_poly1305_provider'
10
+
11
+ # SETUP
12
+ Familia.config.encryption_keys = {
13
+ v1: Base64.strict_encode64('a' * 32)
14
+ }
15
+ Familia.config.current_key_version = :v1
16
+
17
+ ## Check config in test
18
+ keys = Familia.config.encryption_keys
19
+ version = Familia.config.current_key_version
20
+ [keys.nil?, version.nil?]
21
+ #=> [false, false]
22
+
23
+ ## Try basic encryption in test
24
+ Familia.config.encryption_keys = {v1: Base64.strict_encode64('a' * 32)}
25
+ Familia.config.current_key_version = :v1
26
+ result = Familia::Encryption.encrypt('test', context: 'test')
27
+ result.nil?
28
+ #=> false
29
+
30
+ ## XChaCha20Poly1305Provider is available when RbNaCl is loaded
31
+ @provider_class = Familia::Encryption::Providers::XChaCha20Poly1305Provider
32
+ @provider_class.available?
33
+ #=> true
34
+
35
+ ## Provider has highest priority
36
+ @provider_class.priority
37
+ #=> 100
38
+
39
+ ## derive_key generates 32-byte key from master key and context
40
+ provider = @provider_class.new
41
+ master_key = 'a' * 32
42
+ context = 'TestModel:field:user123'
43
+ derived_key = provider.derive_key(master_key, context)
44
+ derived_key.bytesize
45
+ #=> 32
46
+
47
+ ## derive_key with same inputs produces same output
48
+ provider = @provider_class.new
49
+ master_key = 'a' * 32
50
+ context = 'TestModel:field:user123'
51
+ key1 = provider.derive_key(master_key, context)
52
+ key2 = provider.derive_key(master_key, context)
53
+ key1 == key2
54
+ #=> true
55
+
56
+ ## derive_key with different contexts produces different keys
57
+ provider = @provider_class.new
58
+ master_key = 'a' * 32
59
+ context1 = 'TestModel:field:user123'
60
+ context2 = 'TestModel:field:user456'
61
+ key1 = provider.derive_key(master_key, context1)
62
+ key2 =
63
+ provider.derive_key(master_key, context2)
64
+ key1 != key2
65
+ #=> true
66
+
67
+ ## derive_key with custom personalization works
68
+ provider = @provider_class.new
69
+ master_key = 'a' * 32
70
+ context = 'TestModel:field:user123'
71
+ personal = 'custom_app_v2'
72
+ derived_key = provider.derive_key(master_key, context, personal: personal)
73
+ derived_key.bytesize
74
+ #=> 32
75
+
76
+ ## derive_key rejects personalization string with null bytes
77
+ provider = @provider_class.new
78
+ master_key = 'a' * 32
79
+ context = 'TestModel:field:user123'
80
+ personal_with_null = "app\0version"
81
+ begin
82
+ provider.derive_key(master_key, context, personal: personal_with_null)
83
+ "should_not_reach_here"
84
+ rescue Familia::EncryptionError => e
85
+ e.message
86
+ end
87
+ #=> "Personalization string must not contain null bytes"
88
+
89
+ ## derive_key rejects config personalization with null bytes
90
+ provider = @provider_class.new
91
+ master_key = 'a' * 32
92
+ context = 'TestModel:field:user123'
93
+ # Set config with null byte
94
+ original_personal = Familia.config.encryption_personalization
95
+ Familia.config.encryption_personalization = "bad\0config"
96
+ begin
97
+ provider.derive_key(master_key, context)
98
+ "should_not_reach_here"
99
+ rescue Familia::EncryptionError => e
100
+ e.message
101
+ ensure
102
+ Familia.config.encryption_personalization = original_personal
103
+ end
104
+ #=> "Personalization string must not contain null bytes"
105
+
106
+ ## derive_key validates master key length
107
+ provider = @provider_class.new
108
+ short_key = 'a' * 16 # Too short
109
+ context = 'TestModel:field:user123'
110
+ begin
111
+ provider.derive_key(short_key, context)
112
+ "should_not_reach_here"
113
+ rescue Familia::EncryptionError => e
114
+ e.message
115
+ end
116
+ #=> "Key must be at least 32 bytes"
117
+
118
+ ## derive_key rejects nil master key
119
+ provider = @provider_class.new
120
+ context = 'TestModel:field:user123'
121
+ begin
122
+ provider.derive_key(nil, context)
123
+ "should_not_reach_here"
124
+ rescue Familia::EncryptionError => e
125
+ e.message
126
+ end
127
+ #=> "Key cannot be nil"
128
+
129
+ ## encrypt/decrypt round trip with derived key works
130
+ provider = @provider_class.new
131
+ master_key = 'a' * 32
132
+ context = 'TestModel:field:user123'
133
+ derived_key = provider.derive_key(master_key, context)
134
+ plaintext = 'sensitive data'
135
+ encrypted_data = provider.encrypt(plaintext, derived_key)
136
+ decrypted = provider.decrypt(
137
+ encrypted_data[:ciphertext],
138
+ derived_key,
139
+ encrypted_data[:nonce],
140
+ encrypted_data[:auth_tag]
141
+ )
142
+ decrypted
143
+ #=> "sensitive data"
144
+
145
+ ## encrypt with additional data and derived key
146
+ provider = @provider_class.new
147
+ master_key = 'a' * 32
148
+ context = 'TestModel:field:user123'
149
+ derived_key = provider.derive_key(master_key, context)
150
+ plaintext = 'sensitive data'
151
+ additional_data = 'user_id:123'
152
+ encrypted_data = provider.encrypt(plaintext, derived_key, additional_data)
153
+ decrypted = provider.decrypt(
154
+ encrypted_data[:ciphertext],
155
+ derived_key,
156
+ encrypted_data[:nonce],
157
+ encrypted_data[:auth_tag],
158
+ additional_data
159
+ )
160
+ decrypted
161
+ #=> "sensitive data"
162
+
163
+ ## generate_nonce produces correct size
164
+ provider = @provider_class.new
165
+ nonce = provider.generate_nonce
166
+ nonce.bytesize
167
+ #=> 24
168
+
169
+ ## generate_nonce produces unique values
170
+ provider = @provider_class.new
171
+ nonce1 = provider.generate_nonce
172
+ nonce2 = provider.generate_nonce
173
+ nonce1 != nonce2
174
+ #=> true
175
+
176
+ ## secure_wipe works with valid key
177
+ provider = @provider_class.new
178
+ key = 'a' * 32
179
+ provider.secure_wipe(key)
180
+ # Should not raise error
181
+ true
182
+ #=> true
183
+
184
+ ## secure_wipe handles nil key gracefully
185
+ provider = @provider_class.new
186
+ provider.secure_wipe(nil)
187
+ # Should not raise error
188
+ true
189
+ #=> true
190
+
191
+ # TEARDOWN
192
+ Thread.current[:familia_key_cache]&.clear if Thread.current[:familia_key_cache]