capybara-simulated 0.7.0 → 0.9.0
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 +4 -4
- data/README.md +12 -10
- data/lib/capybara/simulated/browser.rb +3077 -245
- data/lib/capybara/simulated/driver.rb +85 -15
- data/lib/capybara/simulated/js/bridge.bundle.js +33716 -19156
- data/lib/capybara/simulated/node.rb +33 -7
- data/lib/capybara/simulated/quickjs_runtime.rb +74 -17
- data/lib/capybara/simulated/runtime_shared.rb +453 -34
- data/lib/capybara/simulated/v8_runtime.rb +134 -10
- data/lib/capybara/simulated/version.rb +1 -1
- metadata +2 -2
|
@@ -41,7 +41,7 @@ module Capybara
|
|
|
41
41
|
# JS exception that crashes the whole script chain. Bodies take
|
|
42
42
|
# `(browser, *js_args)` and return whatever the JS caller expects.
|
|
43
43
|
BROWSER_HOST_FNS = {
|
|
44
|
-
'__rackFetch' => ->(b, *a) { b.rack_fetch(a[0], a[1], a[2], a[3], a[4], a[5], credentials: a[6] || 'same-origin', referrer_policy: a[7], referrer: a[8], cache_mode: a[9] || 'default') },
|
|
44
|
+
'__rackFetch' => ->(b, *a) { b.rack_fetch(a[0], a[1], a[2], a[3], a[4], a[5], credentials: a[6] || 'same-origin', referrer_policy: a[7], referrer: a[8], cache_mode: a[9] || 'default', initiator: a[10], site_seed: a[11], origin_null: a[12]) },
|
|
45
45
|
'__csimExternalAsset' => ->(b, *a) { b.external_asset_source(a[0]) },
|
|
46
46
|
'__locationAssign' => ->(b, *a) { b.location_assign(a[0]); nil },
|
|
47
47
|
'__locationReload' => ->(b, *_) { b.location_reload; nil },
|
|
@@ -62,9 +62,15 @@ module Capybara
|
|
|
62
62
|
# like __csimFrameNavigate — see Browser#frame_submit_self.
|
|
63
63
|
'__csimFrameSubmit' => ->(b, *a) { b.frame_submit_self(a[0].to_i); nil },
|
|
64
64
|
'__csimFrameHistoryGo' => ->(b, *a) { b.frame_history_go(a[0].to_i, a[1].to_i); nil },
|
|
65
|
+
# A frame `src=` re-navigation records a session-history entry (snapshotting the OUTGOING
|
|
66
|
+
# document) so history.go(-1) can traverse back — and a controlling SW sees
|
|
67
|
+
# isHistoryNavigation. Must run while the outgoing realm (a[0]) is still alive.
|
|
68
|
+
'__csim_recordFrameNav' => ->(b, *a) { b.record_frame_nav(a[0].to_i, a[1]); nil },
|
|
65
69
|
'__setTimersActive' => ->(b, *a) { b.timers_active = !!a[0]; nil },
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
# a[2] is the realm that navigated: a nested browsing context keeps its OWN session
|
|
71
|
+
# history, so a frame's pushState must not be mirrored onto the top document's.
|
|
72
|
+
'__setCurrentUrl' => ->(b, *a) { b.history_state(a[0], a[1], a[2]); nil },
|
|
73
|
+
'__pushHistoryEntry' => ->(b, *a) { b.history_push(a[0], a[1], a[2]); nil },
|
|
68
74
|
'__historyGo' => ->(b, *a) { b.history_go(a[0]); nil },
|
|
69
75
|
'__historyLength' => ->(b, *_) { b.history_length },
|
|
70
76
|
'__csimReadFilePick' => ->(b, *a) { b.read_file_pick(a[0], a[1], a[2], a[3]) },
|
|
@@ -72,11 +78,24 @@ module Capybara
|
|
|
72
78
|
'__setDocumentCookie' => ->(b, *a) { b.write_document_cookie(a[0].to_s); nil },
|
|
73
79
|
'__getDocumentReferrer' => ->(b, *_) { b.current_referer },
|
|
74
80
|
'__csim_storageGet' => ->(b, *a) { b.storage_get(a[0], a[1]) },
|
|
75
|
-
'__csim_storageSet' => ->(b, *a) { b.storage_set(a[0], a[1], a[2])
|
|
81
|
+
'__csim_storageSet' => ->(b, *a) { b.storage_set(a[0], a[1], a[2]) },
|
|
76
82
|
'__csim_storageRemove' => ->(b, *a) { b.storage_remove(a[0], a[1]); nil },
|
|
77
83
|
'__csim_storageClear' => ->(b, *a) { b.storage_clear(a[0]); nil },
|
|
78
84
|
'__csim_storageKey' => ->(b, *a) { b.storage_key(a[0], a[1]) },
|
|
79
85
|
'__csim_storageLength' => ->(b, *a) { b.storage_length(a[0]) },
|
|
86
|
+
'__csimStorageChanged' => ->(b, *a) { b.storage_changed(a[0], a[1], a[2], a[3], a[4], a[5]); nil },
|
|
87
|
+
# Cache Storage — origin-partitioned (a[0] = origin key), Ruby-backed so it
|
|
88
|
+
# survives the per-visit VM rebuild and is shared between a service worker and
|
|
89
|
+
# the client it controls. The JS side (cache-storage.js) owns the spec matching;
|
|
90
|
+
# Ruby is a dumb ordered store keyed by (origin, cache name).
|
|
91
|
+
'__csim_cacheStorageOpen' => ->(b, *a) { b.cache_storage_open(a[0], a[1]) },
|
|
92
|
+
'__csim_cacheStorageHas' => ->(b, *a) { b.cache_storage_has(a[0], a[1]) },
|
|
93
|
+
'__csim_cacheStorageDelete' => ->(b, *a) { b.cache_storage_delete(a[0], a[1]) },
|
|
94
|
+
'__csim_cacheStorageKeys' => ->(b, *a) { b.cache_storage_keys(a[0]) },
|
|
95
|
+
'__csim_cacheEntries' => ->(b, *a) { b.cache_entries(a[0], a[1]) },
|
|
96
|
+
'__csim_cacheEntryResponse' => ->(b, *a) { b.cache_entry_response(a[0], a[1], a[2]) },
|
|
97
|
+
'__csim_cachePut' => ->(b, *a) { b.cache_put(a[0], a[1], a[2], a[3], a[4]); nil },
|
|
98
|
+
'__csim_cacheDeleteEntries' => ->(b, *a) { b.cache_delete_entries(a[0], a[1], a[2]) },
|
|
80
99
|
'__csimGeolocationState' => ->(b, *_) { b.geolocation_state_json },
|
|
81
100
|
'__modalDialog' => ->(b, *a) { b.handle_modal(a[0], a[1], a[2]) },
|
|
82
101
|
'__csim_pushImportmap' => ->(b, *a) { b.set_importmap(a[0]); nil },
|
|
@@ -90,15 +109,24 @@ module Capybara
|
|
|
90
109
|
'__csim_rackFetchAsyncAbort' => ->(b, *a) { b.rack_fetch_async_abort(a[0]); nil },
|
|
91
110
|
# Cross-window references (window.open / opener / postMessage). A separate-VM
|
|
92
111
|
# aux window forwards to the Driver; a same-origin window realm lives in this
|
|
93
|
-
# isolate. a[2] is the opener's realm id (for wiring window.opener)
|
|
94
|
-
'
|
|
112
|
+
# isolate. a[2] is the opener's realm id (for wiring window.opener); a[3]/a[4] the opener
|
|
113
|
+
# document's base URL and origin, which an about:blank popup inherits.
|
|
114
|
+
'__csimWindowOpen' => ->(b, *a) { b.open_child_window(a[0], a[1], a[2], a[3], a[4]) },
|
|
95
115
|
# A `target=_blank`/named link/area activation from a frame or window realm:
|
|
96
116
|
# open a new auxiliary window (the realm's VM isn't rebuilt — a fresh window
|
|
97
117
|
# is). `opener` = rel=opener (target=_blank defaults to noopener); the Driver
|
|
98
118
|
# forces noopener for a cross-partition blob: target.
|
|
99
119
|
'__csimOpenAuxFromRealm' => ->(b, *a) { b.open_aux_from_realm(a[0], a[1], a[2]); nil },
|
|
100
120
|
'__csimWindowPostMessage' => ->(b, *a) { b.post_message_to_window(a[0], a[1], a[2]); nil },
|
|
101
|
-
'__csimBroadcast' => ->(b, *a) { b.broadcast_to_windows(a[0], a[1], a[2].to_i); nil },
|
|
121
|
+
'__csimBroadcast' => ->(b, *a) { b.broadcast_to_windows(a[0], a[1], a[2].to_i, a[3]); nil },
|
|
122
|
+
# BroadcastChannel isolate-wide, creation-ordered registry (the multi-realm delivery path). A
|
|
123
|
+
# channel registers on construction / unregisters on close; `bc_post` snapshots the eligible
|
|
124
|
+
# targets at post time and queues one ordered delivery per target. Only consulted when a sibling
|
|
125
|
+
# same-isolate realm exists (`bc_siblings_exist?`); single-window pages never touch it.
|
|
126
|
+
'__csimBcRegister' => ->(b, *a) { b.bc_register(a[0], a[1], a[2], a[3]); nil },
|
|
127
|
+
'__csimBcUnregister' => ->(b, *a) { b.bc_unregister(a[0], a[1]); nil },
|
|
128
|
+
'__csimBcPost' => ->(b, *a) { b.bc_post(a[0], a[1], a[2], a[3], a[4], a[5]); nil },
|
|
129
|
+
'__csimBcSiblingsExist' => ->(b, *_a) { b.bc_siblings_exist? },
|
|
102
130
|
'__csimWindowGet' => ->(b, *a) { b.window_get(a[0], a[1]) },
|
|
103
131
|
'__csimWindowDocGet' => ->(b, *a) { b.window_doc_get(a[0], a[1]) },
|
|
104
132
|
# Cross-window remote-ref RPC: route an opener's node/object proxy op to
|
|
@@ -115,13 +143,57 @@ module Capybara
|
|
|
115
143
|
# Fire an aux window's OWN `load` event (in its VM) — deferred by the
|
|
116
144
|
# opener so a child `window.onload` runs after the opener's current task.
|
|
117
145
|
'__csimFireAuxWindowLoad' => ->(b, *a) { b.fire_aux_window_load(a[0]); nil },
|
|
118
|
-
|
|
146
|
+
# a[3] is the CREATING realm (the worker dies when it is discarded); a[4] that context's
|
|
147
|
+
# CURRENT controller handle, which a DEDICATED worker inherits rather than scope-matching
|
|
148
|
+
# its own — often opaque (blob:/data:) — script URL. 0 when the creator is uncontrolled.
|
|
149
|
+
'__csim_workerSpawn' => ->(b, *a) { b.worker_spawn(a[0], shared: !!a[1], creator_key: a[2], realm_id: a[3].to_i, controller_handle: a[4].to_i) },
|
|
119
150
|
# navigator.serviceWorker.register (universal-server only) — spawn a worker
|
|
120
151
|
# running the SW script as an executor context. Returns its handle.
|
|
121
|
-
'__csim_serviceWorkerRegister' => ->(b, *a) { b.worker_spawn(a[0], service: true) },
|
|
152
|
+
'__csim_serviceWorkerRegister' => ->(b, *a) { b.worker_spawn(a[0], service: true, creator_key: a[1]) },
|
|
122
153
|
'__csim_workerPostToWorker' => ->(b, *a) { b.worker_post_to_worker(a[0], a[1]); nil },
|
|
154
|
+
# ServiceWorker.postMessage from a client window → the SW's `message` event (source = client).
|
|
155
|
+
'__csim_serviceWorkerPostMessage' => ->(b, *a) { b.service_worker_post_message(a[0], a[1], a[2], a[3]); nil },
|
|
156
|
+
# Cross-isolate MessagePort channel (client-realm side): register this realm's endpoint, and
|
|
157
|
+
# relay a client-realm port's postMessage to its remote (worker/SW) peer.
|
|
158
|
+
'__csimClientPortEndpoint' => ->(b, *a) { b.port_channel_endpoint_realm(a[0], a[1]); nil },
|
|
159
|
+
'__csimClientPortPost' => ->(b, *a) { b.client_port_post(a[0], a[1]); nil },
|
|
160
|
+
# The focus chain moved into this realm's browsing context (a focus() commit, or an
|
|
161
|
+
# <iframe> focused in its container, which hands focus to the nested context).
|
|
162
|
+
'__csimNoteFocusedRealm' => ->(b, *a) { b.note_focused_realm(a[0]); nil },
|
|
163
|
+
# This realm is a service-worker client: reported at document load and again whenever
|
|
164
|
+
# control is installed, by the realm, because only it knows its own URL, frame type and
|
|
165
|
+
# controller. `a[3]` is the controlling worker's handle, 0 when uncontrolled.
|
|
166
|
+
'__csimNoteClient' => ->(b, *a) { b.sw_note_client(a[0], a[1], a[2], a[3]); nil },
|
|
167
|
+
# A controlled client's fetch → the controlling SW's `fetch` event. Returns false if the SW
|
|
168
|
+
# is gone (client falls back to the network).
|
|
169
|
+
'__csim_serviceWorkerControllerFetch' => ->(b, *a) { b.service_worker_controller_fetch(a[0], a[1], a[2], a[3]) },
|
|
170
|
+
# A controlled client cancelled a streaming respondWith body → cancel the SW's source stream
|
|
171
|
+
# (routed to the worker that owns this [realm, fetch] stream). See sw_stream_cancel.
|
|
172
|
+
'__csim_swStreamCancel' => ->(b, *a) { b.sw_stream_cancel(a[0], a[1]); nil },
|
|
173
|
+
# Client lifecycle mirrors scope→active-worker into Ruby so a navigation (fetched
|
|
174
|
+
# Ruby-side before the destination realm's JS exists) can find its controlling SW.
|
|
175
|
+
'__csim_swRegisterScope' => ->(b, *a) { b.sw_register_scope(a[0], a[1]); nil },
|
|
176
|
+
'__csim_swUnregisterScope' => ->(b, *a) { b.sw_unregister_scope(a[0]); nil },
|
|
177
|
+
# The active worker handle at an EXACT scope (0 if none), so a register() from a realm with no
|
|
178
|
+
# local registration (a different iframe registering an already-active scope) can synthesize a
|
|
179
|
+
# registration reflecting the shared active worker instead of installing a duplicate.
|
|
180
|
+
'__csim_swActiveHandleForScope' => ->(b, *a) { b.sw_active_handle_for_scope(a[0]) },
|
|
181
|
+
# Does this worker still control any client? An incoming worker may only activate once the
|
|
182
|
+
# outgoing one controls nothing (or skipWaiting is called) — see _scheduleLifecycle.
|
|
183
|
+
'__csim_swControlsClients' => ->(b, *a) { b.sw_worker_controls_clients?(a[0]) },
|
|
184
|
+
'__csim_swNoteActivationParked' => ->(b, *_) { b.sw_note_activation_parked; nil },
|
|
185
|
+
# Navigation Preload state (NavigationPreloadManager), keyed by the registration's active
|
|
186
|
+
# worker handle — reached identically from the client (registration.active._handle) and the
|
|
187
|
+
# worker (__csimWorkerHandle). Get returns {enabled, headerValue}; set leaves a nil field as-is.
|
|
188
|
+
'__csim_swNavPreloadState' => ->(b, *a) { b.nav_preload_state(a[0]) },
|
|
189
|
+
'__csim_swNavPreloadSet' => ->(b, *a) { b.nav_preload_set(a[0], a[1], a[2]); nil },
|
|
190
|
+
# A navigation (iframe/document load) → its controlling SW's `fetch` event, awaited
|
|
191
|
+
# synchronously. Returns the response wire hash, or nil to load from the network.
|
|
192
|
+
'__csim_swNavigationFetch' => ->(b, *a) { b.service_worker_navigation_fetch(a[0], is_reload: !!a[1], is_history: !!a[2], referrer_source: a[3], method: a[4] || 'GET', body_b64: a[5] || '', content_type: a[6]) },
|
|
123
193
|
'__csim_workerTerminate' => ->(b, *a) { b.worker_terminate(a[0]); nil },
|
|
124
194
|
'__csim_decodeImage' => ->(b, *a) { b.decode_image(a[0], a[1], a[2]) },
|
|
195
|
+
'__csim_renderText' => ->(b, *a) { b.render_text(a[0], a[1], a[2], a[3], a[4]) },
|
|
196
|
+
'__csim_loadImage' => ->(b, *a) { b.load_image(a[0], !!a[1], a[2] || 'same-origin') },
|
|
125
197
|
'__csim_blobRegister' => ->(b, *a) { b.blob_register(a[0], a[1], a[2]); nil },
|
|
126
198
|
# WHATWG/UTS46 IDNA for the URL parser's host processing (the JS tr46 stub
|
|
127
199
|
# delegates non-ASCII / xn-- hosts here; ASCII stays in-VM).
|
|
@@ -144,6 +216,7 @@ module Capybara
|
|
|
144
216
|
# eager-@app.calls a foreign URL (side effects: extra visit / log row).
|
|
145
217
|
'__csim_allHostsLocal' => ->(b, *a) { b.send(:all_hosts_local?) },
|
|
146
218
|
'__csim_decodeVideoFrame' => ->(b, *a) { b.decode_video_frame(a[0]) },
|
|
219
|
+
'__csim_videoBytesB64' => ->(b, *a) { b.video_bytes_b64(a[0]) },
|
|
147
220
|
'__csim_encodeImage' => ->(b, *a) { b.encode_image(a[0], a[1], a[2], a[3], a[4]) },
|
|
148
221
|
# WebAuthn create / get raise `WebauthnState::Error` carrying
|
|
149
222
|
# the DOMException name (`InvalidStateError`, …); rescue here
|
|
@@ -171,34 +244,18 @@ module Capybara
|
|
|
171
244
|
# Host fns that route to pure stdlib — no Browser surface,
|
|
172
245
|
# nothing to safe_call, no allocation needed for the wrap. Skip
|
|
173
246
|
# the rescue overhead on every per-find / per-event invocation.
|
|
174
|
-
# Process-wide
|
|
175
|
-
#
|
|
176
|
-
#
|
|
177
|
-
#
|
|
178
|
-
#
|
|
179
|
-
#
|
|
180
|
-
#
|
|
181
|
-
CASCADE_RULE_CACHE = {}
|
|
182
|
-
CASCADE_RULE_CACHE_MUTEX = Mutex.new
|
|
183
|
-
|
|
184
|
-
# Process-wide PER-SHEET parse cache (companion to CASCADE_RULE_CACHE). The
|
|
185
|
-
# built whole-cascade is cached above, but it misses whenever a page's inline
|
|
186
|
-
# `<style>` changes (Avo injects per-page styles), forcing a rebuild that
|
|
187
|
-
# re-parses every sheet — including unchanged linked bundles (avo.base.css).
|
|
188
|
-
# `parseSheet` is pure, so the JS side caches its serialized `{hide,layout}`
|
|
189
|
-
# keyed by (cssText hash, viewport) here, surviving the per-visit VM rebuild
|
|
190
|
-
# that wipes the in-VM `__sheetCache` — the CSS analogue of the JS bytecode
|
|
191
|
-
# cache. Keyed by content, so a content change yields a new key. Capped.
|
|
247
|
+
# Process-wide PER-SHEET parse cache (the CSS analogue of the JS bytecode
|
|
248
|
+
# cache). `parseSheet` is pure, so the JS side caches its serialized
|
|
249
|
+
# `{hide,layout}` here keyed by (cssText hash, viewport), surviving the
|
|
250
|
+
# per-visit VM rebuild that wipes the in-VM `__sheetCache`. A cascade
|
|
251
|
+
# rebuild then re-parses only sheets it has never seen (content change =
|
|
252
|
+
# new key). Content-keyed ONLY — never url-keyed — so freshness stays the
|
|
253
|
+
# asset cache's call. Capped.
|
|
192
254
|
SHEET_PARSE_CACHE = {}
|
|
193
255
|
SHEET_PARSE_CACHE_MUTEX = Mutex.new
|
|
194
256
|
SHEET_PARSE_CACHE_MAX = 2048
|
|
195
257
|
|
|
196
258
|
STDLIB_HOST_FNS = {
|
|
197
|
-
'__csimCascadeCacheGet' => ->(*a) { CASCADE_RULE_CACHE_MUTEX.synchronize { CASCADE_RULE_CACHE[a[0].to_s] } },
|
|
198
|
-
'__csimCascadeCachePut' => lambda {|*a|
|
|
199
|
-
CASCADE_RULE_CACHE_MUTEX.synchronize { CASCADE_RULE_CACHE[a[0].to_s] = a[1].to_s }
|
|
200
|
-
nil
|
|
201
|
-
},
|
|
202
259
|
'__csimSheetCacheGet' => ->(*a) { SHEET_PARSE_CACHE_MUTEX.synchronize { SHEET_PARSE_CACHE[a[0].to_s] } },
|
|
203
260
|
'__csimSheetCachePut' => lambda {|*a|
|
|
204
261
|
SHEET_PARSE_CACHE_MUTEX.synchronize {
|
|
@@ -215,15 +272,377 @@ module Capybara
|
|
|
215
272
|
'__csim_utf8Decode' => ->(*a) { a[0].pack('C*').force_encoding('UTF-8') },
|
|
216
273
|
# `__csim_parseUrl` is defined in JS now (js/src/url-parse.js, backed by
|
|
217
274
|
# the vendored whatwg-url) — spec-correct + no V8↔Ruby boundary per parse.
|
|
218
|
-
# Web Crypto
|
|
219
|
-
#
|
|
275
|
+
# Web Crypto raw primitives, backed by OpenSSL. The JS side (js/src/webcrypto.js)
|
|
276
|
+
# owns the whole SubtleCrypto contract — algorithm normalization, CryptoKey
|
|
277
|
+
# objects, usage validation, the DOMException grammar — and calls these only for
|
|
278
|
+
# the actual number-crunching. Byte arguments cross as plain Arrays (JS packs a
|
|
279
|
+
# BufferSource into one); results return as byte Arrays.
|
|
280
|
+
#
|
|
281
|
+
# digest — algo is "SHA1"/"SHA256"/… (JS strips the dash and upcases).
|
|
220
282
|
'__csim_subtleDigest' => lambda {|*a|
|
|
221
283
|
algo = a[0].to_s.upcase.tr('-', '')
|
|
222
284
|
bytes = a[1].is_a?(Array) ? a[1].pack('C*') : a[1].to_s
|
|
223
285
|
OpenSSL::Digest.new(algo).digest(bytes).bytes
|
|
286
|
+
},
|
|
287
|
+
# HMAC sign — args: (hash "SHA256", keyBytes, dataBytes) → mac bytes. verify is
|
|
288
|
+
# done JS-side by re-signing and comparing, so no separate host fn is needed.
|
|
289
|
+
'__csim_hmacSign' => lambda {|*a|
|
|
290
|
+
hash = a[0].to_s.upcase.tr('-', '')
|
|
291
|
+
key = a[1].is_a?(Array) ? a[1].pack('C*') : a[1].to_s
|
|
292
|
+
data = a[2].is_a?(Array) ? a[2].pack('C*') : a[2].to_s
|
|
293
|
+
OpenSSL::HMAC.digest(OpenSSL::Digest.new(hash), key, data).bytes
|
|
294
|
+
},
|
|
295
|
+
# AES encrypt/decrypt — args: (cipher "aes-256-gcm", key, iv, data, aad, tagBytes).
|
|
296
|
+
# For GCM the JS side follows the WebCrypto layout (ciphertext ‖ truncated tag), so
|
|
297
|
+
# encrypt appends the tagBytes-long tag and decrypt splits it back off before
|
|
298
|
+
# verifying; a tag mismatch / bad padding raises CipherError, which the JS layer
|
|
299
|
+
# maps to OperationError. CBC/CTR pass tagBytes 0 and an empty aad.
|
|
300
|
+
'__csim_aesEncrypt' => lambda {|*a|
|
|
301
|
+
name = a[0].to_s
|
|
302
|
+
key = a[1].is_a?(Array) ? a[1].pack('C*') : a[1].to_s
|
|
303
|
+
iv = a[2].is_a?(Array) ? a[2].pack('C*') : a[2].to_s
|
|
304
|
+
data = a[3].is_a?(Array) ? a[3].pack('C*') : a[3].to_s
|
|
305
|
+
aad = a[4].is_a?(Array) ? a[4].pack('C*') : a[4].to_s
|
|
306
|
+
tagbytes = a[5].to_i
|
|
307
|
+
c = OpenSSL::Cipher.new(name)
|
|
308
|
+
c.encrypt
|
|
309
|
+
c.key = key
|
|
310
|
+
c.iv_len = iv.bytesize if name.end_with?('-gcm') # GCM defaults to 12; allow any length
|
|
311
|
+
c.iv = iv
|
|
312
|
+
if name.end_with?('-gcm')
|
|
313
|
+
c.auth_data = aad
|
|
314
|
+
ct = c.update(data) + c.final
|
|
315
|
+
(ct + c.auth_tag(tagbytes)).bytes
|
|
316
|
+
else
|
|
317
|
+
(c.update(data) + c.final).bytes
|
|
318
|
+
end
|
|
319
|
+
},
|
|
320
|
+
'__csim_aesDecrypt' => lambda {|*a|
|
|
321
|
+
name = a[0].to_s
|
|
322
|
+
key = a[1].is_a?(Array) ? a[1].pack('C*') : a[1].to_s
|
|
323
|
+
iv = a[2].is_a?(Array) ? a[2].pack('C*') : a[2].to_s
|
|
324
|
+
data = a[3].is_a?(Array) ? a[3].pack('C*') : a[3].to_s
|
|
325
|
+
aad = a[4].is_a?(Array) ? a[4].pack('C*') : a[4].to_s
|
|
326
|
+
tagbytes = a[5].to_i
|
|
327
|
+
c = OpenSSL::Cipher.new(name)
|
|
328
|
+
c.decrypt
|
|
329
|
+
c.key = key
|
|
330
|
+
c.iv_len = iv.bytesize if name.end_with?('-gcm')
|
|
331
|
+
c.iv = iv
|
|
332
|
+
if name.end_with?('-gcm')
|
|
333
|
+
ct = data[0, data.bytesize - tagbytes]
|
|
334
|
+
tag = data[data.bytesize - tagbytes, tagbytes]
|
|
335
|
+
c.auth_data = aad
|
|
336
|
+
c.auth_tag = tag
|
|
337
|
+
(c.update(ct) + c.final).bytes
|
|
338
|
+
else
|
|
339
|
+
(c.update(data) + c.final).bytes
|
|
340
|
+
end
|
|
341
|
+
},
|
|
342
|
+
# RSA — keys cross as their DER encoding (SPKI for public, PKCS#8 for private)
|
|
343
|
+
# in a byte Array; the host re-parses per call (OpenSSL::PKey.read auto-detects).
|
|
344
|
+
# generateKey returns the private PKCS#8 DER, from which the JS side derives the
|
|
345
|
+
# public key and parameters via the export / key-info fns below.
|
|
346
|
+
'__csim_rsaGenerate' => lambda {|*a|
|
|
347
|
+
bits = a[0].to_i
|
|
348
|
+
exp = OpenSSL::BN.new(crypto_bytes(a[1]), 2).to_i
|
|
349
|
+
OpenSSL::PKey::RSA.generate(bits, exp).private_to_der.bytes
|
|
350
|
+
},
|
|
351
|
+
# Re-encode a key to 'spki' (public) or 'pkcs8' (private). Asking a private key
|
|
352
|
+
# for 'spki' yields its public half — how generateKey obtains the public key.
|
|
353
|
+
'__csim_rsaExport' => lambda {|*a|
|
|
354
|
+
key = RuntimeShared.pkey_read(a[0])
|
|
355
|
+
a[1].to_s == 'spki' ? key.public_to_der.bytes : key.private_to_der.bytes
|
|
356
|
+
},
|
|
357
|
+
# Modulus length (bits) + public exponent (big-endian bytes) for key.algorithm.
|
|
358
|
+
'__csim_rsaKeyInfo' => lambda {|*a|
|
|
359
|
+
key = RuntimeShared.pkey_read(a[0])
|
|
360
|
+
{ 'modulusLength' => key.n.num_bits, 'publicExponent' => key.e.to_s(2).bytes }
|
|
361
|
+
},
|
|
362
|
+
# Build a key DER from JWK components (big-endian byte arrays). Public keys pass
|
|
363
|
+
# only n/e; private keys pass the full CRT set. Returns SPKI / PKCS#8 DER bytes.
|
|
364
|
+
'__csim_rsaImportJwk' => lambda {|*a|
|
|
365
|
+
RuntimeShared.rsa_jwk_to_der(*a).bytes
|
|
366
|
+
},
|
|
367
|
+
# Explode a key DER into JWK components (big-endian byte arrays). Private keys
|
|
368
|
+
# include the CRT parameters; public keys carry only n/e.
|
|
369
|
+
'__csim_rsaExportJwk' => lambda {|*a|
|
|
370
|
+
RuntimeShared.rsa_der_to_jwk(a[0])
|
|
371
|
+
},
|
|
372
|
+
# sign/verify — scheme 'pkcs1' (RSASSA-PKCS1-v1_5) or 'pss' (RSA-PSS, MGF1 with
|
|
373
|
+
# the same hash, saltLength in bytes). verify swallows OpenSSL errors to a false.
|
|
374
|
+
'__csim_rsaSign' => lambda {|*a|
|
|
375
|
+
key = RuntimeShared.pkey_read(a[0])
|
|
376
|
+
hash = a[1].to_s
|
|
377
|
+
data = crypto_bytes(a[2])
|
|
378
|
+
if a[3].to_s == 'pss'
|
|
379
|
+
key.sign_pss(hash, data, salt_length: a[4].to_i, mgf1_hash: hash).bytes
|
|
380
|
+
else
|
|
381
|
+
key.sign(hash, data).bytes
|
|
382
|
+
end
|
|
383
|
+
},
|
|
384
|
+
'__csim_rsaVerify' => lambda {|*a|
|
|
385
|
+
key = RuntimeShared.pkey_read(a[0])
|
|
386
|
+
hash = a[1].to_s
|
|
387
|
+
data = crypto_bytes(a[2])
|
|
388
|
+
sig = crypto_bytes(a[3])
|
|
389
|
+
begin
|
|
390
|
+
if a[4].to_s == 'pss'
|
|
391
|
+
key.verify_pss(hash, sig, data, salt_length: a[5].to_i, mgf1_hash: hash)
|
|
392
|
+
else
|
|
393
|
+
key.verify(hash, sig, data)
|
|
394
|
+
end
|
|
395
|
+
rescue OpenSSL::PKey::PKeyError
|
|
396
|
+
false
|
|
397
|
+
end
|
|
398
|
+
},
|
|
399
|
+
# RSA-OAEP encrypt/decrypt — MGF1 + OAEP with the key's hash and an optional label.
|
|
400
|
+
# `rsa_oaep_label` takes a HEX string, not raw bytes (OpenSSL parses it via
|
|
401
|
+
# prepare_from_text), so the label crosses hex-encoded.
|
|
402
|
+
'__csim_rsaEncrypt' => lambda {|*a|
|
|
403
|
+
key = RuntimeShared.pkey_read(a[0])
|
|
404
|
+
opts = { rsa_padding_mode: 'oaep', rsa_oaep_md: a[1].to_s }
|
|
405
|
+
label = crypto_bytes(a[3])
|
|
406
|
+
opts[:rsa_oaep_label] = label.unpack1('H*') unless label.empty?
|
|
407
|
+
key.encrypt(crypto_bytes(a[2]), opts).bytes
|
|
408
|
+
},
|
|
409
|
+
'__csim_rsaDecrypt' => lambda {|*a|
|
|
410
|
+
key = RuntimeShared.pkey_read(a[0])
|
|
411
|
+
opts = { rsa_padding_mode: 'oaep', rsa_oaep_md: a[1].to_s }
|
|
412
|
+
label = crypto_bytes(a[3])
|
|
413
|
+
opts[:rsa_oaep_label] = label.unpack1('H*') unless label.empty?
|
|
414
|
+
key.decrypt(crypto_bytes(a[2]), opts).bytes
|
|
415
|
+
},
|
|
416
|
+
# Elliptic curve (ECDSA / ECDH). The JS side passes the OpenSSL curve name
|
|
417
|
+
# ('prime256v1' / 'secp384r1' / 'secp521r1') and the field byte length so the
|
|
418
|
+
# host stays curve-agnostic. Keys cross as SPKI / PKCS#8 DER.
|
|
419
|
+
'__csim_ecGenerate' => lambda {|*a|
|
|
420
|
+
OpenSSL::PKey::EC.generate(a[0].to_s).private_to_der.bytes
|
|
421
|
+
},
|
|
422
|
+
'__csim_ecExport' => lambda {|*a|
|
|
423
|
+
key = RuntimeShared.pkey_read(a[0])
|
|
424
|
+
case a[1].to_s
|
|
425
|
+
when 'spki' then key.public_to_der.bytes
|
|
426
|
+
when 'pkcs8' then key.private_to_der.bytes
|
|
427
|
+
else key.public_key.to_octet_string(:uncompressed).bytes # 'raw' → uncompressed point
|
|
428
|
+
end
|
|
429
|
+
},
|
|
430
|
+
'__csim_ecKeyInfo' => lambda {|*a|
|
|
431
|
+
{ 'curve' => RuntimeShared.pkey_read(a[0]).group.curve_name }
|
|
432
|
+
},
|
|
433
|
+
'__csim_ecImportRaw' => lambda {|*a|
|
|
434
|
+
RuntimeShared.ec_point_spki(a[0].to_s, crypto_bytes(a[1])).bytes
|
|
435
|
+
},
|
|
436
|
+
# args: (curve, isPrivate, x, y, [d, n]). The uncompressed public point is
|
|
437
|
+
# 0x04 ‖ x ‖ y; a private key additionally carries the scalar d (padded to n).
|
|
438
|
+
'__csim_ecImportJwk' => lambda {|*a|
|
|
439
|
+
curve = a[0].to_s
|
|
440
|
+
point = "\x04".b + crypto_bytes(a[2]) + crypto_bytes(a[3])
|
|
441
|
+
if a[1]
|
|
442
|
+
RuntimeShared.ec_priv_pkcs8(curve, crypto_bytes(a[4]), point, a[5].to_i).bytes
|
|
443
|
+
else
|
|
444
|
+
RuntimeShared.ec_point_spki(curve, point).bytes
|
|
445
|
+
end
|
|
446
|
+
},
|
|
447
|
+
'__csim_ecExportJwk' => lambda {|*a|
|
|
448
|
+
RuntimeShared.ec_der_to_jwk(a[0], a[1].to_i)
|
|
449
|
+
},
|
|
450
|
+
'__csim_ecdsaSign' => lambda {|*a|
|
|
451
|
+
key = RuntimeShared.pkey_read(a[0])
|
|
452
|
+
der = key.sign(OpenSSL::Digest.new(a[1].to_s), crypto_bytes(a[2]))
|
|
453
|
+
RuntimeShared.ecdsa_der_to_raw(der, a[3].to_i).bytes
|
|
454
|
+
},
|
|
455
|
+
'__csim_ecdsaVerify' => lambda {|*a|
|
|
456
|
+
key = RuntimeShared.pkey_read(a[0])
|
|
457
|
+
der = RuntimeShared.ecdsa_raw_to_der(crypto_bytes(a[3]), a[4].to_i)
|
|
458
|
+
begin
|
|
459
|
+
der ? key.verify(OpenSSL::Digest.new(a[1].to_s), der, crypto_bytes(a[2])) : false
|
|
460
|
+
rescue OpenSSL::PKey::PKeyError
|
|
461
|
+
false
|
|
462
|
+
end
|
|
463
|
+
},
|
|
464
|
+
# Key derivation — HKDF / PBKDF2 produce `nbytes` bytes; ECDH computes the raw
|
|
465
|
+
# shared secret (field size). The JS side truncates to the requested bit length.
|
|
466
|
+
'__csim_hkdf' => lambda {|*a|
|
|
467
|
+
OpenSSL::KDF.hkdf(crypto_bytes(a[1]), salt: crypto_bytes(a[2]), info: crypto_bytes(a[3]), length: a[4].to_i, hash: a[0].to_s).bytes
|
|
468
|
+
},
|
|
469
|
+
'__csim_pbkdf2' => lambda {|*a|
|
|
470
|
+
OpenSSL::KDF.pbkdf2_hmac(crypto_bytes(a[1]), salt: crypto_bytes(a[2]), iterations: a[3].to_i, length: a[4].to_i, hash: a[0].to_s).bytes
|
|
471
|
+
},
|
|
472
|
+
'__csim_ecdhDerive' => lambda {|*a|
|
|
473
|
+
priv = RuntimeShared.pkey_read(a[0])
|
|
474
|
+
pub = RuntimeShared.pkey_read(a[1])
|
|
475
|
+
priv.dh_compute_key(pub.public_key).bytes
|
|
476
|
+
},
|
|
477
|
+
# AES-KW (RFC 3394 key wrap). The cipher width follows the wrapping key; OpenSSL's
|
|
478
|
+
# wrap mode supplies the default A6A6… IV. A tampered wrap raises on unwrap, which
|
|
479
|
+
# the JS layer maps to OperationError.
|
|
480
|
+
'__csim_aesKwWrap' => lambda {|*a|
|
|
481
|
+
key = crypto_bytes(a[0])
|
|
482
|
+
c = OpenSSL::Cipher.new("aes-#{key.bytesize * 8}-wrap")
|
|
483
|
+
c.encrypt
|
|
484
|
+
c.key = key
|
|
485
|
+
(c.update(crypto_bytes(a[1])) + c.final).bytes
|
|
486
|
+
},
|
|
487
|
+
'__csim_aesKwUnwrap' => lambda {|*a|
|
|
488
|
+
key = crypto_bytes(a[0])
|
|
489
|
+
c = OpenSSL::Cipher.new("aes-#{key.bytesize * 8}-wrap")
|
|
490
|
+
c.decrypt
|
|
491
|
+
c.key = key
|
|
492
|
+
(c.update(crypto_bytes(a[1])) + c.final).bytes
|
|
493
|
+
},
|
|
494
|
+
# OKP curves (Ed25519 signatures / X25519 key agreement). The JS side passes the
|
|
495
|
+
# OpenSSL curve name ('ED25519' / 'X25519'); keys cross as SPKI / PKCS#8 DER, and
|
|
496
|
+
# the raw form is the 32-byte public (or private, via JWK `d`) key.
|
|
497
|
+
'__csim_okpGenerate' => lambda {|*a|
|
|
498
|
+
OpenSSL::PKey.generate_key(a[0].to_s).private_to_der.bytes
|
|
499
|
+
},
|
|
500
|
+
'__csim_okpExport' => lambda {|*a|
|
|
501
|
+
key = RuntimeShared.pkey_read(a[0])
|
|
502
|
+
case a[1].to_s
|
|
503
|
+
when 'spki' then key.public_to_der.bytes
|
|
504
|
+
when 'pkcs8' then key.private_to_der.bytes
|
|
505
|
+
else key.raw_public_key.bytes # 'raw' → 32-byte public key
|
|
506
|
+
end
|
|
507
|
+
},
|
|
508
|
+
'__csim_okpImportRaw' => lambda {|*a|
|
|
509
|
+
OpenSSL::PKey.new_raw_public_key(a[0].to_s, crypto_bytes(a[1])).public_to_der.bytes
|
|
510
|
+
},
|
|
511
|
+
# Parse + re-emit a canonical OKP DER — validates the SPKI/PKCS#8 structure at
|
|
512
|
+
# import time (a truncated / malformed key raises, which JS maps to DataError)
|
|
513
|
+
# instead of storing the bytes blindly and only failing on first use.
|
|
514
|
+
'__csim_okpImportDer' => lambda {|*a|
|
|
515
|
+
key = RuntimeShared.pkey_read(a[1])
|
|
516
|
+
a[0].to_s == 'private' ? key.private_to_der.bytes : key.public_to_der.bytes
|
|
517
|
+
},
|
|
518
|
+
'__csim_okpImportJwk' => lambda {|*a|
|
|
519
|
+
type = a[0].to_s
|
|
520
|
+
if a[1]
|
|
521
|
+
key = OpenSSL::PKey.new_raw_private_key(type, crypto_bytes(a[3]))
|
|
522
|
+
# The JWK "x" (public) must equal the public key derived from "d" — a mismatch is
|
|
523
|
+
# an invalid key pair (DataError on the JS side).
|
|
524
|
+
raise OpenSSL::PKey::PKeyError, 'OKP JWK x does not match d' unless key.raw_public_key == crypto_bytes(a[2])
|
|
525
|
+
key.private_to_der.bytes
|
|
526
|
+
else
|
|
527
|
+
OpenSSL::PKey.new_raw_public_key(type, crypto_bytes(a[2])).public_to_der.bytes
|
|
528
|
+
end
|
|
529
|
+
},
|
|
530
|
+
'__csim_okpExportJwk' => lambda {|*a|
|
|
531
|
+
key = RuntimeShared.pkey_read(a[0])
|
|
532
|
+
jwk = { 'x' => key.raw_public_key.bytes }
|
|
533
|
+
jwk['d'] = key.raw_private_key.bytes if a[1] # a[1] = the key is private
|
|
534
|
+
jwk
|
|
535
|
+
},
|
|
536
|
+
'__csim_ed25519Sign' => lambda {|*a|
|
|
537
|
+
RuntimeShared.pkey_read(a[0]).sign(nil, crypto_bytes(a[1])).bytes
|
|
538
|
+
},
|
|
539
|
+
'__csim_ed25519Verify' => lambda {|*a|
|
|
540
|
+
key = RuntimeShared.pkey_read(a[0])
|
|
541
|
+
begin
|
|
542
|
+
key.verify(nil, crypto_bytes(a[2]), crypto_bytes(a[1]))
|
|
543
|
+
rescue OpenSSL::PKey::PKeyError
|
|
544
|
+
false
|
|
545
|
+
end
|
|
546
|
+
},
|
|
547
|
+
'__csim_x25519Derive' => lambda {|*a|
|
|
548
|
+
RuntimeShared.pkey_read(a[0]).derive(RuntimeShared.pkey_read(a[1])).bytes
|
|
224
549
|
}
|
|
225
550
|
}.freeze
|
|
226
551
|
|
|
552
|
+
# Pack a host-fn byte argument (a JS BufferSource crosses as an Array) into a
|
|
553
|
+
# binary String for OpenSSL.
|
|
554
|
+
def self.crypto_bytes(a)
|
|
555
|
+
a.is_a?(Array) ? a.pack('C*') : a.to_s
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
# Parse an RSA / EC key from its DER bytes (SPKI public or PKCS#8 private —
|
|
559
|
+
# OpenSSL::PKey.read auto-detects both).
|
|
560
|
+
def self.pkey_read(der_bytes)
|
|
561
|
+
OpenSSL::PKey.read(crypto_bytes(der_bytes))
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
# ECDSA signatures cross as the WebCrypto IEEE-P1363 form — r ‖ s, each padded to
|
|
565
|
+
# the curve's field byte length — while OpenSSL speaks DER. Convert both ways.
|
|
566
|
+
def self.ecdsa_der_to_raw(der, n)
|
|
567
|
+
asn = OpenSSL::ASN1.decode(der)
|
|
568
|
+
r = asn.value[0].value.to_s(2).rjust(n, "\x00".b)
|
|
569
|
+
s = asn.value[1].value.to_s(2).rjust(n, "\x00".b)
|
|
570
|
+
r + s
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
def self.ecdsa_raw_to_der(raw, n)
|
|
574
|
+
return nil unless raw.bytesize == 2 * n # wrong length → verify fails, not raises
|
|
575
|
+
r = OpenSSL::BN.new(raw[0, n], 2)
|
|
576
|
+
s = OpenSSL::BN.new(raw[n, n], 2)
|
|
577
|
+
OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer(r), OpenSSL::ASN1::Integer(s)]).to_der
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def self.ec_algid(curve)
|
|
581
|
+
OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId('id-ecPublicKey'), OpenSSL::ASN1::ObjectId(curve)])
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
# SPKI DER for an EC public key from its uncompressed point (0x04 ‖ x ‖ y).
|
|
585
|
+
def self.ec_point_spki(curve, point)
|
|
586
|
+
OpenSSL::ASN1::Sequence([ec_algid(curve), OpenSSL::ASN1::BitString(point)]).to_der
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
# PKCS#8 DER for an EC private key from the private scalar `d` and public point,
|
|
590
|
+
# via the RFC 5915 ECPrivateKey structure (OpenSSL 3 dropped component setters).
|
|
591
|
+
def self.ec_priv_pkcs8(curve, d, point, n)
|
|
592
|
+
ec_priv = OpenSSL::ASN1::Sequence([
|
|
593
|
+
OpenSSL::ASN1::Integer(1),
|
|
594
|
+
OpenSSL::ASN1::OctetString(d.rjust(n, "\x00".b)),
|
|
595
|
+
OpenSSL::ASN1::ASN1Data.new([OpenSSL::ASN1::ObjectId(curve)], 0, :CONTEXT_SPECIFIC),
|
|
596
|
+
OpenSSL::ASN1::ASN1Data.new([OpenSSL::ASN1::BitString(point)], 1, :CONTEXT_SPECIFIC)
|
|
597
|
+
]).to_der
|
|
598
|
+
OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer(0), ec_algid(curve), OpenSSL::ASN1::OctetString(ec_priv)]).to_der
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
# Explode an EC key DER into JWK coordinates (x, y, and d for a private key), each a
|
|
602
|
+
# big-endian byte Array padded to the field length; plus the OpenSSL curve name.
|
|
603
|
+
def self.ec_der_to_jwk(der_bytes, n)
|
|
604
|
+
key = pkey_read(der_bytes)
|
|
605
|
+
point = key.public_key.to_octet_string(:uncompressed) # 0x04 ‖ x ‖ y
|
|
606
|
+
jwk = { 'curve' => key.group.curve_name, 'x' => point[1, n].bytes, 'y' => point[1 + n, n].bytes }
|
|
607
|
+
jwk['d'] = key.private_key.to_s(2).rjust(n, "\x00".b).bytes if key.private_key?
|
|
608
|
+
jwk
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
# Assemble an RSA key DER from JWK components. `is_private` selects between a
|
|
612
|
+
# public SPKI (n, e) and a private PKCS#8 with the full CRT set. Each component is
|
|
613
|
+
# a big-endian byte Array. Building the ASN.1 by hand is the OpenSSL-3-supported
|
|
614
|
+
# path (the deprecated `rsa.n = …` setters are gone).
|
|
615
|
+
def self.rsa_jwk_to_der(is_private, n, e, d = nil, p = nil, q = nil, dp = nil, dq = nil, qi = nil)
|
|
616
|
+
bn = ->(b) { OpenSSL::BN.new(crypto_bytes(b), 2) }
|
|
617
|
+
alg_id = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId('rsaEncryption'), OpenSSL::ASN1::Null(nil)])
|
|
618
|
+
if is_private
|
|
619
|
+
pkcs1 = OpenSSL::ASN1::Sequence([
|
|
620
|
+
OpenSSL::ASN1::Integer(0),
|
|
621
|
+
OpenSSL::ASN1::Integer(bn.(n)), OpenSSL::ASN1::Integer(bn.(e)), OpenSSL::ASN1::Integer(bn.(d)),
|
|
622
|
+
OpenSSL::ASN1::Integer(bn.(p)), OpenSSL::ASN1::Integer(bn.(q)),
|
|
623
|
+
OpenSSL::ASN1::Integer(bn.(dp)), OpenSSL::ASN1::Integer(bn.(dq)), OpenSSL::ASN1::Integer(bn.(qi))
|
|
624
|
+
]).to_der
|
|
625
|
+
OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer(0), alg_id, OpenSSL::ASN1::OctetString(pkcs1)]).to_der
|
|
626
|
+
else
|
|
627
|
+
pkcs1 = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer(bn.(n)), OpenSSL::ASN1::Integer(bn.(e))]).to_der
|
|
628
|
+
OpenSSL::ASN1::Sequence([alg_id, OpenSSL::ASN1::BitString(pkcs1)]).to_der
|
|
629
|
+
end
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
# Explode an RSA key DER into JWK components (big-endian byte Arrays). Private keys
|
|
633
|
+
# carry the full CRT set; public keys only n/e.
|
|
634
|
+
def self.rsa_der_to_jwk(der_bytes)
|
|
635
|
+
key = pkey_read(der_bytes)
|
|
636
|
+
jwk = { 'n' => key.n.to_s(2).bytes, 'e' => key.e.to_s(2).bytes }
|
|
637
|
+
if key.private?
|
|
638
|
+
jwk.merge!(
|
|
639
|
+
'd' => key.d.to_s(2).bytes, 'p' => key.p.to_s(2).bytes, 'q' => key.q.to_s(2).bytes,
|
|
640
|
+
'dp' => key.dmp1.to_s(2).bytes, 'dq' => key.dmq1.to_s(2).bytes, 'qi' => key.iqmp.to_s(2).bytes
|
|
641
|
+
)
|
|
642
|
+
end
|
|
643
|
+
jwk
|
|
644
|
+
end
|
|
645
|
+
|
|
227
646
|
def self.safe_call
|
|
228
647
|
yield
|
|
229
648
|
rescue StandardError => e
|