asherah 0.10.0-aarch64-linux-musl
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.
- checksums.yaml +7 -0
- data/README.md +386 -0
- data/lib/asherah/config.rb +106 -0
- data/lib/asherah/error.rb +16 -0
- data/lib/asherah/hooks.rb +239 -0
- data/lib/asherah/native/libasherah_ffi.so +0 -0
- data/lib/asherah/native.rb +146 -0
- data/lib/asherah/session.rb +176 -0
- data/lib/asherah/session_factory.rb +35 -0
- data/lib/asherah/version.rb +5 -0
- data/lib/asherah.rb +321 -0
- metadata +90 -0
data/lib/asherah.rb
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
require_relative "asherah/version"
|
|
6
|
+
require_relative "asherah/error"
|
|
7
|
+
require_relative "asherah/config"
|
|
8
|
+
require_relative "asherah/native"
|
|
9
|
+
require_relative "asherah/session_factory"
|
|
10
|
+
require_relative "asherah/session"
|
|
11
|
+
require_relative "asherah/hooks"
|
|
12
|
+
|
|
13
|
+
module Asherah
|
|
14
|
+
DEFAULT_SESSION_CACHE_MAX_SIZE = 1000
|
|
15
|
+
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
@factory = nil
|
|
18
|
+
# @sessions is an LRU cache. Ruby Hash preserves insertion order, so we
|
|
19
|
+
# implement LRU by delete-and-reinsert on hit (moves the entry to the
|
|
20
|
+
# end), and `shift` on overflow (removes the oldest, i.e. least-recently
|
|
21
|
+
# used). Bounded by @session_cache_max_size, which honors the
|
|
22
|
+
# SessionCacheMaxSize config field — same default (1000) as the Rust
|
|
23
|
+
# core and the other bindings.
|
|
24
|
+
@sessions = {}
|
|
25
|
+
@initialized = false
|
|
26
|
+
@session_cache_enabled = true
|
|
27
|
+
@session_cache_max_size = DEFAULT_SESSION_CACHE_MAX_SIZE
|
|
28
|
+
@verbose = false
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
# Configure Asherah using a block with snake_case accessors.
|
|
32
|
+
# Compatible with the canonical godaddy/asherah-ruby gem API.
|
|
33
|
+
#
|
|
34
|
+
# Asherah.configure do |config|
|
|
35
|
+
# config.service_name = "MyService"
|
|
36
|
+
# config.product_id = "MyProduct"
|
|
37
|
+
# config.kms = "static"
|
|
38
|
+
# config.metastore = "memory"
|
|
39
|
+
# end
|
|
40
|
+
def configure
|
|
41
|
+
@mutex.synchronize do
|
|
42
|
+
raise Error::AlreadyInitialized if @initialized
|
|
43
|
+
|
|
44
|
+
config = Config.new
|
|
45
|
+
yield config
|
|
46
|
+
config.validate!
|
|
47
|
+
|
|
48
|
+
json = config.to_json
|
|
49
|
+
pointer = Native.asherah_factory_new_with_config(json)
|
|
50
|
+
@factory = SessionFactory.new(pointer)
|
|
51
|
+
@sessions = {}
|
|
52
|
+
@initialized = true
|
|
53
|
+
@session_cache_enabled = config.enable_session_caching != false
|
|
54
|
+
@session_cache_max_size = positive_int(config.session_cache_max_size) || DEFAULT_SESSION_CACHE_MAX_SIZE
|
|
55
|
+
@verbose = config.verbose == true
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Initialize Asherah with a PascalCase config hash.
|
|
60
|
+
# Also accepts snake_case string/symbol keys (auto-normalized).
|
|
61
|
+
def setup(config)
|
|
62
|
+
normalized = normalize_config(config)
|
|
63
|
+
json = JSON.generate(normalized)
|
|
64
|
+
|
|
65
|
+
pointer = Native.asherah_factory_new_with_config(json)
|
|
66
|
+
factory = SessionFactory.new(pointer)
|
|
67
|
+
|
|
68
|
+
@mutex.synchronize do
|
|
69
|
+
raise Error::AlreadyInitialized if @initialized
|
|
70
|
+
|
|
71
|
+
@factory = factory
|
|
72
|
+
@sessions = {}
|
|
73
|
+
@initialized = true
|
|
74
|
+
@session_cache_enabled = truthy(normalized["EnableSessionCaching"], default: true)
|
|
75
|
+
@session_cache_max_size = positive_int(normalized["SessionCacheMaxSize"]) || DEFAULT_SESSION_CACHE_MAX_SIZE
|
|
76
|
+
@verbose = truthy(normalized["Verbose"], default: false)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
nil
|
|
80
|
+
rescue StandardError
|
|
81
|
+
factory&.close if defined?(factory) && factory
|
|
82
|
+
raise
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Run setup in a background Thread. Yields the result to +block+ on
|
|
86
|
+
# success. Exceptions raised by setup propagate via the Thread's
|
|
87
|
+
# joined error — the previous implementation called the block with
|
|
88
|
+
# the result on success but silently dropped the Thread's error
|
|
89
|
+
# state on failure, leaving callers unable to distinguish completion
|
|
90
|
+
# from an unsetup factory. The Thread aborts on exception
|
|
91
|
+
# (`Thread.report_on_exception = true`), so a stack trace lands on
|
|
92
|
+
# stderr; callers that need programmatic access should
|
|
93
|
+
# `thread.join` to re-raise. T-finding "setup_async/shutdown_async/
|
|
94
|
+
# encrypt_async/decrypt_async swallow Thread exceptions" in
|
|
95
|
+
# `docs/review-2026-05-05-findings.md`.
|
|
96
|
+
def setup_async(config, &block)
|
|
97
|
+
Thread.new do
|
|
98
|
+
Thread.current.report_on_exception = true
|
|
99
|
+
result = setup(config)
|
|
100
|
+
block&.call(result)
|
|
101
|
+
result
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def shutdown
|
|
106
|
+
factory = nil
|
|
107
|
+
sessions = nil
|
|
108
|
+
@mutex.synchronize do
|
|
109
|
+
raise Error::NotInitialized unless @initialized
|
|
110
|
+
|
|
111
|
+
factory = @factory
|
|
112
|
+
sessions = @sessions.values
|
|
113
|
+
@factory = nil
|
|
114
|
+
@sessions = {}
|
|
115
|
+
@initialized = false
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
Array(sessions).each do |session|
|
|
119
|
+
begin
|
|
120
|
+
session.close unless session.closed?
|
|
121
|
+
rescue StandardError => e
|
|
122
|
+
warn "asherah: error closing session during shutdown: #{e.message}"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
factory&.close unless factory&.closed?
|
|
126
|
+
nil
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Run shutdown in a background Thread; see `setup_async` for the
|
|
130
|
+
# exception-propagation contract.
|
|
131
|
+
def shutdown_async(&block)
|
|
132
|
+
Thread.new do
|
|
133
|
+
Thread.current.report_on_exception = true
|
|
134
|
+
result = shutdown
|
|
135
|
+
block&.call(result)
|
|
136
|
+
result
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def get_setup_status
|
|
141
|
+
@mutex.synchronize { @initialized }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def setenv(env = {})
|
|
145
|
+
data = case env
|
|
146
|
+
when String
|
|
147
|
+
JSON.parse(env)
|
|
148
|
+
else
|
|
149
|
+
env
|
|
150
|
+
end
|
|
151
|
+
unless data.respond_to?(:each_pair)
|
|
152
|
+
raise ArgumentError, "environment payload must be a Hash or JSON object"
|
|
153
|
+
end
|
|
154
|
+
data.each_pair do |k, v|
|
|
155
|
+
if v.nil?
|
|
156
|
+
ENV.delete(String(k))
|
|
157
|
+
else
|
|
158
|
+
ENV[String(k)] = v.to_s
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
nil
|
|
162
|
+
end
|
|
163
|
+
alias_method :set_env, :setenv
|
|
164
|
+
|
|
165
|
+
def encrypt(partition_id, payload)
|
|
166
|
+
raise ArgumentError, "payload cannot be nil" if payload.nil?
|
|
167
|
+
session = resolve_session(partition_id)
|
|
168
|
+
session.encrypt_bytes(payload)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def encrypt_string(partition_id, text)
|
|
172
|
+
raise ArgumentError, "text cannot be nil" if text.nil?
|
|
173
|
+
encrypt(partition_id, text)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def decrypt(partition_id, data_row_record)
|
|
177
|
+
raise ArgumentError, "data_row_record cannot be nil" if data_row_record.nil?
|
|
178
|
+
session = resolve_session(partition_id)
|
|
179
|
+
session.decrypt_bytes(data_row_record).force_encoding(Encoding::UTF_8)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def decrypt_string(partition_id, data_row_record)
|
|
183
|
+
raise ArgumentError, "data_row_record cannot be nil" if data_row_record.nil?
|
|
184
|
+
decrypt(partition_id, data_row_record).force_encoding(Encoding::UTF_8)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Run encrypt in a background Thread; see `setup_async` for the
|
|
188
|
+
# exception-propagation contract.
|
|
189
|
+
def encrypt_async(partition_id, payload, &block)
|
|
190
|
+
Thread.new do
|
|
191
|
+
Thread.current.report_on_exception = true
|
|
192
|
+
result = encrypt(partition_id, payload)
|
|
193
|
+
block&.call(result)
|
|
194
|
+
result
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Run decrypt in a background Thread; see `setup_async` for the
|
|
199
|
+
# exception-propagation contract.
|
|
200
|
+
def decrypt_async(partition_id, data_row_record, &block)
|
|
201
|
+
Thread.new do
|
|
202
|
+
Thread.current.report_on_exception = true
|
|
203
|
+
result = decrypt(partition_id, data_row_record)
|
|
204
|
+
block&.call(result)
|
|
205
|
+
result
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Install a log hook. Yields a +Hash+ +{level:, target:, message:}+ for
|
|
210
|
+
# every log record emitted by the underlying Rust crates. The block may
|
|
211
|
+
# fire from any thread; implementations must be thread-safe and
|
|
212
|
+
# non-blocking. Pass +nil+ to clear (equivalent to {clear_log_hook}).
|
|
213
|
+
#
|
|
214
|
+
# Replaces any previously installed log hook. Exceptions raised from the
|
|
215
|
+
# callback are caught and silently swallowed.
|
|
216
|
+
def set_log_hook(callback = nil, &block)
|
|
217
|
+
Hooks.set_log_hook(callback, &block)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Remove the active log hook, if any. Idempotent.
|
|
221
|
+
def clear_log_hook
|
|
222
|
+
Hooks.clear_log_hook
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Install a metrics hook. Yields a +Hash+ +{type:, duration_ns:, name:}+
|
|
226
|
+
# for every metrics event. Timing events ({:encrypt, :decrypt, :store,
|
|
227
|
+
# :load}) carry a positive +duration_ns+ and a +nil+ +name+; cache events
|
|
228
|
+
# ({:cache_hit, :cache_miss, :cache_stale}) carry +duration_ns+ == 0 and
|
|
229
|
+
# the cache identifier in +name+.
|
|
230
|
+
#
|
|
231
|
+
# Installing a hook implicitly enables the global metrics gate; clearing
|
|
232
|
+
# it disables the gate. Replaces any previously installed metrics hook.
|
|
233
|
+
# Pass +nil+ to clear (equivalent to {clear_metrics_hook}).
|
|
234
|
+
def set_metrics_hook(callback = nil, &block)
|
|
235
|
+
Hooks.set_metrics_hook(callback, &block)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Remove the active metrics hook and disable metrics. Idempotent.
|
|
239
|
+
def clear_metrics_hook
|
|
240
|
+
Hooks.clear_metrics_hook
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
private
|
|
244
|
+
|
|
245
|
+
REQUIRED_KEYS = %w[ServiceName ProductID Metastore].freeze
|
|
246
|
+
|
|
247
|
+
def normalize_config(config)
|
|
248
|
+
unless config.respond_to?(:each_pair)
|
|
249
|
+
raise ArgumentError, "config must be a Hash-like object"
|
|
250
|
+
end
|
|
251
|
+
normalized = {}
|
|
252
|
+
config.each_pair do |key, value|
|
|
253
|
+
normalized[String(key)] = value
|
|
254
|
+
end
|
|
255
|
+
REQUIRED_KEYS.each do |key|
|
|
256
|
+
raise ArgumentError, "#{key} is required" if normalized[key].nil? || normalized[key].to_s.strip.empty?
|
|
257
|
+
end
|
|
258
|
+
normalized
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def truthy(value, default: false)
|
|
262
|
+
return default if value.nil?
|
|
263
|
+
|
|
264
|
+
case value
|
|
265
|
+
when true, "1", "true", "TRUE", "yes", "on" then true
|
|
266
|
+
when false, "0", "false", "FALSE", "no", "off" then false
|
|
267
|
+
else
|
|
268
|
+
default
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def resolve_session(partition_id)
|
|
273
|
+
raise ArgumentError, "partition_id cannot be empty" if String(partition_id).empty?
|
|
274
|
+
|
|
275
|
+
evicted = nil
|
|
276
|
+
session = @mutex.synchronize do
|
|
277
|
+
raise Error::NotInitialized unless @initialized
|
|
278
|
+
|
|
279
|
+
unless @session_cache_enabled
|
|
280
|
+
break @factory.get_session(partition_id)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# LRU: on hit, delete + reinsert to move the entry to the
|
|
284
|
+
# most-recently-used end of the Hash (Ruby Hash is insertion-
|
|
285
|
+
# ordered). On miss + overflow, `shift` removes the oldest entry,
|
|
286
|
+
# which is the least-recently-used.
|
|
287
|
+
if (existing = @sessions.delete(partition_id))
|
|
288
|
+
@sessions[partition_id] = existing
|
|
289
|
+
break existing
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
fresh = @factory.get_session(partition_id)
|
|
293
|
+
@sessions[partition_id] = fresh
|
|
294
|
+
if @sessions.size > @session_cache_max_size
|
|
295
|
+
_, evicted = @sessions.shift
|
|
296
|
+
end
|
|
297
|
+
fresh
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Close evicted session outside the lock — close hits the FFI and
|
|
301
|
+
# we don't want to serialize all encrypts behind one eviction.
|
|
302
|
+
if evicted
|
|
303
|
+
begin
|
|
304
|
+
evicted.close unless evicted.closed?
|
|
305
|
+
rescue StandardError => e
|
|
306
|
+
warn "asherah: error closing evicted session: #{e.message}"
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
session
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def positive_int(value)
|
|
314
|
+
return nil if value.nil?
|
|
315
|
+
n = Integer(value)
|
|
316
|
+
n.positive? ? n : nil
|
|
317
|
+
rescue ArgumentError, TypeError
|
|
318
|
+
nil
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: asherah
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.10.0
|
|
5
|
+
platform: aarch64-linux-musl
|
|
6
|
+
authors:
|
|
7
|
+
- Jay Gowdy
|
|
8
|
+
- Bo Thompson
|
|
9
|
+
- Michael Micco
|
|
10
|
+
- Dalibor Nasevic
|
|
11
|
+
autorequire:
|
|
12
|
+
bindir: bin
|
|
13
|
+
cert_chain: []
|
|
14
|
+
date: 2026-06-05 00:00:00.000000000 Z
|
|
15
|
+
dependencies:
|
|
16
|
+
- !ruby/object:Gem::Dependency
|
|
17
|
+
name: ffi
|
|
18
|
+
requirement: !ruby/object:Gem::Requirement
|
|
19
|
+
requirements:
|
|
20
|
+
- - "~>"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '1.15'
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - "~>"
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '1.15'
|
|
30
|
+
- !ruby/object:Gem::Dependency
|
|
31
|
+
name: logger
|
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
|
33
|
+
requirements:
|
|
34
|
+
- - "~>"
|
|
35
|
+
- !ruby/object:Gem::Version
|
|
36
|
+
version: '1.6'
|
|
37
|
+
type: :runtime
|
|
38
|
+
prerelease: false
|
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - "~>"
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: '1.6'
|
|
44
|
+
description: Asherah application-layer encryption for Ruby with automatic key rotation,
|
|
45
|
+
powered by the native Rust implementation.
|
|
46
|
+
email: oss@godaddy.com
|
|
47
|
+
executables: []
|
|
48
|
+
extensions: []
|
|
49
|
+
extra_rdoc_files: []
|
|
50
|
+
files:
|
|
51
|
+
- README.md
|
|
52
|
+
- lib/asherah.rb
|
|
53
|
+
- lib/asherah/config.rb
|
|
54
|
+
- lib/asherah/error.rb
|
|
55
|
+
- lib/asherah/hooks.rb
|
|
56
|
+
- lib/asherah/native.rb
|
|
57
|
+
- lib/asherah/native/libasherah_ffi.so
|
|
58
|
+
- lib/asherah/session.rb
|
|
59
|
+
- lib/asherah/session_factory.rb
|
|
60
|
+
- lib/asherah/version.rb
|
|
61
|
+
homepage: https://github.com/godaddy/asherah-ffi
|
|
62
|
+
licenses:
|
|
63
|
+
- Apache-2.0
|
|
64
|
+
metadata:
|
|
65
|
+
homepage_uri: https://github.com/godaddy/asherah-ffi
|
|
66
|
+
source_code_uri: https://github.com/godaddy/asherah-ffi
|
|
67
|
+
github_repo: https://github.com/godaddy/asherah-ffi
|
|
68
|
+
changelog_uri: https://github.com/godaddy/asherah-ffi/releases
|
|
69
|
+
bug_tracker_uri: https://github.com/godaddy/asherah-ffi/issues
|
|
70
|
+
rubygems_mfa_required: 'true'
|
|
71
|
+
post_install_message:
|
|
72
|
+
rdoc_options: []
|
|
73
|
+
require_paths:
|
|
74
|
+
- lib
|
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
76
|
+
requirements:
|
|
77
|
+
- - ">="
|
|
78
|
+
- !ruby/object:Gem::Version
|
|
79
|
+
version: '3.0'
|
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
81
|
+
requirements:
|
|
82
|
+
- - ">="
|
|
83
|
+
- !ruby/object:Gem::Version
|
|
84
|
+
version: '0'
|
|
85
|
+
requirements: []
|
|
86
|
+
rubygems_version: 3.5.22
|
|
87
|
+
signing_key:
|
|
88
|
+
specification_version: 4
|
|
89
|
+
summary: Asherah application-layer encryption for Ruby with automatic key rotation.
|
|
90
|
+
test_files: []
|