iodine 0.7.59 → 0.8.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/.clang-format +15 -0
- data/.rspec +1 -0
- data/.yardopts +1 -3
- data/CHANGELOG.md +132 -12
- data/Gemfile +5 -7
- data/LICENSE.txt +1 -1
- data/README.md +147 -671
- data/Rakefile +9 -16
- data/examples/chatroom.nru +9 -0
- data/examples/client +74 -0
- data/examples/config-hello.nru +9 -0
- data/examples/config-hello.ru +11 -0
- data/examples/config-hijack.ru +13 -0
- data/examples/config-shootout.ru +43 -0
- data/examples/config.nru +120 -0
- data/examples/config.rb +37 -0
- data/examples/config.ru +42 -53
- data/examples/fake +24 -0
- data/examples/hello_neorack +137 -0
- data/examples/routes.nru +29 -0
- data/examples/routes.ru +42 -0
- data/exe/iodine +218 -258
- data/ext/iodine/extconf.rb +27 -100
- data/ext/iodine/fio-stl.h +128336 -0
- data/ext/iodine/fio-stl.md +26396 -0
- data/ext/iodine/iodine.c +213 -1393
- data/ext/iodine/iodine.h +250 -55
- data/ext/iodine/iodine_arg_helper.h +329 -0
- data/ext/iodine/iodine_caller.h +234 -25
- data/ext/iodine/iodine_cli.h +434 -0
- data/ext/iodine/iodine_compression.h +312 -0
- data/ext/iodine/iodine_connection.h +3484 -34
- data/ext/iodine/iodine_core.h +363 -0
- data/ext/iodine/iodine_crypto.h +1441 -0
- data/ext/iodine/iodine_defer.h +225 -4
- data/ext/iodine/iodine_json.h +738 -4
- data/ext/iodine/iodine_listener.h +263 -0
- data/ext/iodine/iodine_log.h +297 -0
- data/ext/iodine/iodine_minimap.h +543 -0
- data/ext/iodine/iodine_mustache.h +461 -4
- data/ext/iodine/iodine_pubsub_eng.h +729 -0
- data/ext/iodine/iodine_pubsub_history.h +547 -0
- data/ext/iodine/iodine_pubsub_msg.h +228 -0
- data/ext/iodine/iodine_redis.h +322 -0
- data/ext/iodine/iodine_store.h +418 -16
- data/ext/iodine/iodine_threads.h +279 -0
- data/ext/iodine/iodine_tls.h +515 -6
- data/ext/iodine/iodine_utils.h +923 -0
- data/iodine.gemspec +37 -42
- data/lib/iodine/benchmark.rb +1690 -0
- data/lib/iodine/documentation.rb +3085 -0
- data/lib/iodine/version.rb +17 -1
- data/lib/iodine.rb +54 -255
- data/logo.svg +36 -0
- data/mustache.md +55 -0
- data/sig/iodine.rbs +4 -0
- metadata +58 -184
- data/.github/ISSUE_TEMPLATE/bug_report.md +0 -40
- data/.github/workflows/ruby.yml +0 -42
- data/.gitignore +0 -20
- data/LIMITS.md +0 -41
- data/SPEC-PubSub-Draft.md +0 -159
- data/SPEC-WebSocket-Draft.md +0 -239
- data/bin/console +0 -22
- data/bin/info.md +0 -353
- data/bin/mustache_bench.rb +0 -100
- data/bin/poc/Gemfile.lock +0 -23
- data/bin/poc/README.md +0 -37
- data/bin/poc/config.ru +0 -66
- data/bin/poc/gemfile +0 -1
- data/bin/poc/www/index.html +0 -57
- data/examples/async_task.ru +0 -92
- data/examples/bates/README.md +0 -3
- data/examples/bates/config.ru +0 -342
- data/examples/bates/david+bold.pdf +0 -0
- data/examples/bates/public/drop-pdf.png +0 -0
- data/examples/bates/public/index.html +0 -600
- data/examples/echo.ru +0 -59
- data/examples/etag.ru +0 -16
- data/examples/hello.ru +0 -29
- data/examples/pubsub_engine.ru +0 -81
- data/examples/rack3.ru +0 -12
- data/examples/redis.ru +0 -70
- data/examples/shootout.ru +0 -73
- data/examples/sub-protocols.ru +0 -90
- data/examples/tcp_client.rb +0 -66
- data/examples/x-sendfile.ru +0 -14
- data/ext/iodine/fio.c +0 -11995
- data/ext/iodine/fio.h +0 -6371
- data/ext/iodine/fio_cli.c +0 -431
- data/ext/iodine/fio_cli.h +0 -189
- data/ext/iodine/fio_json_parser.h +0 -688
- data/ext/iodine/fio_siphash.c +0 -157
- data/ext/iodine/fio_siphash.h +0 -37
- data/ext/iodine/fio_tls.h +0 -129
- data/ext/iodine/fio_tls_missing.c +0 -649
- data/ext/iodine/fio_tls_openssl.c +0 -1106
- data/ext/iodine/fio_tmpfile.h +0 -50
- data/ext/iodine/fiobj.h +0 -44
- data/ext/iodine/fiobj4fio.h +0 -21
- data/ext/iodine/fiobj_ary.c +0 -333
- data/ext/iodine/fiobj_ary.h +0 -139
- data/ext/iodine/fiobj_data.c +0 -1187
- data/ext/iodine/fiobj_data.h +0 -167
- data/ext/iodine/fiobj_hash.c +0 -409
- data/ext/iodine/fiobj_hash.h +0 -176
- data/ext/iodine/fiobj_json.c +0 -622
- data/ext/iodine/fiobj_json.h +0 -68
- data/ext/iodine/fiobj_mem.h +0 -71
- data/ext/iodine/fiobj_mustache.c +0 -317
- data/ext/iodine/fiobj_mustache.h +0 -62
- data/ext/iodine/fiobj_numbers.c +0 -344
- data/ext/iodine/fiobj_numbers.h +0 -127
- data/ext/iodine/fiobj_str.c +0 -433
- data/ext/iodine/fiobj_str.h +0 -172
- data/ext/iodine/fiobject.c +0 -620
- data/ext/iodine/fiobject.h +0 -655
- data/ext/iodine/hpack.h +0 -1923
- data/ext/iodine/http.c +0 -2743
- data/ext/iodine/http.h +0 -1002
- data/ext/iodine/http1.c +0 -910
- data/ext/iodine/http1.h +0 -29
- data/ext/iodine/http1_parser.h +0 -878
- data/ext/iodine/http_internal.c +0 -1279
- data/ext/iodine/http_internal.h +0 -237
- data/ext/iodine/http_mime_parser.h +0 -350
- data/ext/iodine/iodine_caller.c +0 -230
- data/ext/iodine/iodine_connection.c +0 -941
- data/ext/iodine/iodine_defer.c +0 -420
- data/ext/iodine/iodine_fiobj2rb.h +0 -120
- data/ext/iodine/iodine_helpers.c +0 -282
- data/ext/iodine/iodine_helpers.h +0 -12
- data/ext/iodine/iodine_http.c +0 -1181
- data/ext/iodine/iodine_http.h +0 -23
- data/ext/iodine/iodine_json.c +0 -302
- data/ext/iodine/iodine_mustache.c +0 -567
- data/ext/iodine/iodine_pubsub.c +0 -580
- data/ext/iodine/iodine_pubsub.h +0 -26
- data/ext/iodine/iodine_rack_io.c +0 -273
- data/ext/iodine/iodine_rack_io.h +0 -20
- data/ext/iodine/iodine_store.c +0 -146
- data/ext/iodine/iodine_tcp.c +0 -346
- data/ext/iodine/iodine_tcp.h +0 -13
- data/ext/iodine/iodine_tls.c +0 -261
- data/ext/iodine/mustache_parser.h +0 -1546
- data/ext/iodine/redis_engine.c +0 -957
- data/ext/iodine/redis_engine.h +0 -79
- data/ext/iodine/resp_parser.h +0 -317
- data/ext/iodine/websocket_parser.h +0 -506
- data/ext/iodine/websockets.c +0 -743
- data/ext/iodine/websockets.h +0 -185
- data/lib/iodine/connection.rb +0 -61
- data/lib/iodine/json.rb +0 -42
- data/lib/iodine/mustache.rb +0 -113
- data/lib/iodine/pubsub.rb +0 -55
- data/lib/iodine/rack_utils.rb +0 -43
- data/lib/iodine/tls.rb +0 -16
- data/lib/rack/handler/iodine.rb +0 -33
- data/logo.png +0 -0
|
@@ -0,0 +1,1690 @@
|
|
|
1
|
+
require 'iodine'
|
|
2
|
+
|
|
3
|
+
module Iodine
|
|
4
|
+
# This module includes benchmarking methods to test performance.
|
|
5
|
+
#
|
|
6
|
+
# Before running the benchmarks, require the `Iodine::Benchmark` module:
|
|
7
|
+
#
|
|
8
|
+
# require 'iodine/benchmark'
|
|
9
|
+
#
|
|
10
|
+
# This module does **not** include benchmarking for the Iodine server itself.
|
|
11
|
+
#
|
|
12
|
+
# These methods and the results shown should be used with caution, as they often tell a partial story.
|
|
13
|
+
#
|
|
14
|
+
# To run the benchmarks, you'll need to install the `benchmark-ips` gem.
|
|
15
|
+
#
|
|
16
|
+
# To install the benchmarking gem add `benchmark-ips` to your `Gemfile` or run:
|
|
17
|
+
#
|
|
18
|
+
# gem install benchmark-ips
|
|
19
|
+
#
|
|
20
|
+
module Benchmark
|
|
21
|
+
# Benchmarks Iodine::JSON vs native Ruby JSON and vs Oj (if installed).
|
|
22
|
+
def self.json(data_length = 1000)
|
|
23
|
+
begin
|
|
24
|
+
require 'oj'
|
|
25
|
+
rescue LoadError
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
require 'benchmark/ips'
|
|
29
|
+
require 'json'
|
|
30
|
+
# make a big data store with nothings
|
|
31
|
+
data_1000 = []
|
|
32
|
+
data_length.times do
|
|
33
|
+
tmp = { f: rand }
|
|
34
|
+
tmp[:i] = (tmp[:f] * 1_000_000).to_i
|
|
35
|
+
tmp[:str] = tmp[:i].to_s
|
|
36
|
+
tmp[:escaped] = "Hello\nNew\nLines!"
|
|
37
|
+
tmp[:sym] = tmp[:str].to_sym
|
|
38
|
+
tmp[:ary] = []
|
|
39
|
+
tmp[:ary_empty] = []
|
|
40
|
+
tmp[:hash_empty] = {}
|
|
41
|
+
100.times { |i| tmp[:ary] << i }
|
|
42
|
+
data_1000 << tmp
|
|
43
|
+
end
|
|
44
|
+
json_string = data_1000.to_json
|
|
45
|
+
puts '-----'
|
|
46
|
+
puts "Benchmark #{data_1000.length} item tree, and #{json_string.length} bytes of JSON"
|
|
47
|
+
# benchmark stringification
|
|
48
|
+
::Benchmark.ips do |x|
|
|
49
|
+
x.report(' Ruby obj.to_json') do |_times|
|
|
50
|
+
data_1000.to_json
|
|
51
|
+
end
|
|
52
|
+
x.report('::JSON.pretty_generate') do |_times|
|
|
53
|
+
::JSON.pretty_generate(data_1000)
|
|
54
|
+
end
|
|
55
|
+
x.report('Iodine::JSON.stringify') do |_times|
|
|
56
|
+
Iodine::JSON.stringify(data_1000)
|
|
57
|
+
end
|
|
58
|
+
x.report('Iodine::JSON.beautify') do |_times|
|
|
59
|
+
Iodine::JSON.beautify(data_1000)
|
|
60
|
+
end
|
|
61
|
+
if defined?(Oj)
|
|
62
|
+
x.report(' Oj.dump') do |_times|
|
|
63
|
+
Oj.dump(data_1000)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
x.compare!
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# benchmark parsing
|
|
70
|
+
::Benchmark.ips do |x|
|
|
71
|
+
x.report(' Ruby JSON.parse') do |_times|
|
|
72
|
+
JSON.parse(json_string)
|
|
73
|
+
end
|
|
74
|
+
x.report('Iodine::JSON.parse') do |_times|
|
|
75
|
+
Iodine::JSON.parse(json_string)
|
|
76
|
+
end
|
|
77
|
+
if defined?(Oj)
|
|
78
|
+
x.report(' Oj.load') do |_times|
|
|
79
|
+
Oj.load(json_string)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
x.compare!
|
|
83
|
+
end
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Benchmarks Iodine::Mustache vs the original Mustache Ruby gem (if installed).
|
|
88
|
+
#
|
|
89
|
+
# Benchmark code was copied, in part, from:
|
|
90
|
+
# https://github.com/mustache/mustache/blob/master/benchmarks/render_collection_benchmark.rb
|
|
91
|
+
#
|
|
92
|
+
# The test is, sadly, biased and doesn't test for missing elements, proc/method resolution or template partials.
|
|
93
|
+
#
|
|
94
|
+
def self.mustache(items = 1000)
|
|
95
|
+
require 'benchmark/ips'
|
|
96
|
+
require 'mustache'
|
|
97
|
+
template = "
|
|
98
|
+
{{#products}}
|
|
99
|
+
<div class='product_brick'>
|
|
100
|
+
<div class='container'>
|
|
101
|
+
<div class='element'>
|
|
102
|
+
<img src='images/{{image}}' class='product_miniature' />
|
|
103
|
+
</div>
|
|
104
|
+
<div class='element description'>
|
|
105
|
+
<a href={{url}} class='product_name block bold'>
|
|
106
|
+
{{external_index}}
|
|
107
|
+
</a>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
{{/products}}
|
|
112
|
+
"
|
|
113
|
+
|
|
114
|
+
# fill Hash objects with values for template rendering
|
|
115
|
+
data = {
|
|
116
|
+
products: []
|
|
117
|
+
}
|
|
118
|
+
data_escaped = {
|
|
119
|
+
products: []
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
items.times do
|
|
123
|
+
data[:products] << {
|
|
124
|
+
external_index: 'product',
|
|
125
|
+
url: '/products/7',
|
|
126
|
+
image: 'products/product.jpg'
|
|
127
|
+
}
|
|
128
|
+
data_escaped[:products] << {
|
|
129
|
+
external_index: "This <product> should've been \"properly\" escaped.",
|
|
130
|
+
url: '/products/7',
|
|
131
|
+
image: 'products/product.jpg'
|
|
132
|
+
}
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# prepare Iodine::Mustache reduced Mustache template engine
|
|
136
|
+
mus_view = Iodine::Mustache.new template: template
|
|
137
|
+
|
|
138
|
+
# prepare official Mustache template engine
|
|
139
|
+
view = ::Mustache.new
|
|
140
|
+
view.template = template
|
|
141
|
+
view.render # Call render once so the template will be compiled
|
|
142
|
+
|
|
143
|
+
# benchmark different use cases
|
|
144
|
+
::Benchmark.ips do |x|
|
|
145
|
+
x.report("Ruby Mustache render list of #{items}") do |_times|
|
|
146
|
+
view.render(data)
|
|
147
|
+
end
|
|
148
|
+
x.report("Iodine::Mustache render list of #{items}") do |_times|
|
|
149
|
+
mus_view.render(data)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
x.report("Ruby Mustache render list of #{items} with escaped data") do |_times|
|
|
153
|
+
view.render(data_escaped)
|
|
154
|
+
end
|
|
155
|
+
x.report("Iodine::Mustache render list of #{items} with escaped data") do |_times|
|
|
156
|
+
mus_view.render(data_escaped)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
x.report("Ruby Mustache - no caching - render list of #{items}") do |_times|
|
|
160
|
+
::Mustache.render(template, data)
|
|
161
|
+
end
|
|
162
|
+
x.report("Iodine::Mustache - no caching - render list of #{items}") do |_times|
|
|
163
|
+
Iodine::Mustache.render(template: template, ctx: data)
|
|
164
|
+
end
|
|
165
|
+
x.compare!
|
|
166
|
+
end
|
|
167
|
+
nil
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Benchmarks keyed MACs (including non-cryptographic Risky Hash MACs).
|
|
171
|
+
def self.hmac
|
|
172
|
+
require 'benchmark/ips'
|
|
173
|
+
begin
|
|
174
|
+
require 'openssl'
|
|
175
|
+
rescue LoadError
|
|
176
|
+
nil
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# -----------------------------------------------------------------------
|
|
180
|
+
# HMAC benchmarks — all MACs (cryptographic and non-cryptographic) ranked
|
|
181
|
+
#
|
|
182
|
+
# NOTE on algorithm mapping:
|
|
183
|
+
# hmac128 => Poly1305 MAC (not HMAC-SHA1/SHA256; no standard OpenSSL equivalent)
|
|
184
|
+
# hmac160 => HMAC-SHA1 (compared against OpenSSL HMAC-SHA1)
|
|
185
|
+
# hmac256 => HMAC-SHA256 (compared against OpenSSL HMAC-SHA256)
|
|
186
|
+
# hmac512 => HMAC-SHA512 (compared against OpenSSL HMAC-SHA512)
|
|
187
|
+
# risky256_hmac => keyed MAC on non-crypto Risky Hash — NOT for security-critical use
|
|
188
|
+
# risky512_hmac => keyed MAC on non-crypto Risky Hash — NOT for security-critical use
|
|
189
|
+
#
|
|
190
|
+
# Payload: 512 bytes of the benchmark file itself (representative of a
|
|
191
|
+
# short-to-medium authenticated payload).
|
|
192
|
+
# -----------------------------------------------------------------------
|
|
193
|
+
benchmark_secret = Iodine.secret
|
|
194
|
+
benchmark_payload = (File.exist?(__FILE__) ? IO.binread(__FILE__)[0...512] : 'my message payload')
|
|
195
|
+
|
|
196
|
+
puts "HMAC benchmarks — all MACs ranked — #{benchmark_payload.bytesize}-byte message, #{benchmark_secret.bytesize}-byte secret"
|
|
197
|
+
puts " [WARNING: risky256_hmac / risky512_hmac are NOT suitable for security-critical authentication]"
|
|
198
|
+
|
|
199
|
+
# Pre-create OpenSSL digest objects outside the hot loop. Compare the
|
|
200
|
+
# same Base64 output format as Iodine's hmac* methods.
|
|
201
|
+
if defined?(OpenSSL)
|
|
202
|
+
digest_sha1 = OpenSSL::Digest.new('SHA1')
|
|
203
|
+
digest_sha256 = OpenSSL::Digest.new('SHA256')
|
|
204
|
+
digest_sha512 = OpenSSL::Digest.new('SHA512')
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
::Benchmark.ips do |bm|
|
|
208
|
+
# Non-cryptographic MACs — no standard equivalent; included for throughput ranking
|
|
209
|
+
bm.report('Iodine::Utils.risky256_hmac (non-crypto, 32B)') do
|
|
210
|
+
Iodine::Utils.risky256_hmac(benchmark_secret, benchmark_payload)
|
|
211
|
+
end
|
|
212
|
+
bm.report('Iodine::Utils.risky512_hmac (non-crypto, 64B)') do
|
|
213
|
+
Iodine::Utils.risky512_hmac(benchmark_secret, benchmark_payload)
|
|
214
|
+
end
|
|
215
|
+
# Poly1305 MAC — no direct OpenSSL equivalent; shown for reference only
|
|
216
|
+
bm.report('Iodine::Utils.hmac128 (Poly1305) ') do
|
|
217
|
+
Iodine::Utils.hmac128(benchmark_secret, benchmark_payload)
|
|
218
|
+
end
|
|
219
|
+
# HMAC-SHA1
|
|
220
|
+
bm.report('Iodine::Utils.hmac160 (HMAC-SHA1) ') do
|
|
221
|
+
Iodine::Utils.hmac160(benchmark_secret, benchmark_payload)
|
|
222
|
+
end
|
|
223
|
+
# HMAC-SHA256
|
|
224
|
+
bm.report('Iodine::Utils.hmac256 (HMAC-SHA256) ') do
|
|
225
|
+
Iodine::Utils.hmac256(benchmark_secret, benchmark_payload)
|
|
226
|
+
end
|
|
227
|
+
# HMAC-SHA512
|
|
228
|
+
bm.report('Iodine::Utils.hmac512 (HMAC-SHA512) ') do
|
|
229
|
+
Iodine::Utils.hmac512(benchmark_secret, benchmark_payload)
|
|
230
|
+
end
|
|
231
|
+
if defined?(OpenSSL)
|
|
232
|
+
bm.report('OpenSSL::HMAC.base64digest SHA1 ') do
|
|
233
|
+
OpenSSL::HMAC.base64digest(digest_sha1, benchmark_secret, benchmark_payload)
|
|
234
|
+
end
|
|
235
|
+
bm.report('OpenSSL::HMAC.base64digest SHA256 ') do
|
|
236
|
+
OpenSSL::HMAC.base64digest(digest_sha256, benchmark_secret, benchmark_payload)
|
|
237
|
+
end
|
|
238
|
+
bm.report('OpenSSL::HMAC.base64digest SHA512 ') do
|
|
239
|
+
OpenSSL::HMAC.base64digest(digest_sha512, benchmark_secret, benchmark_payload)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
bm.compare!
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
nil
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Benchmarks UUIDs, random bytes (including secure_random), and random floats.
|
|
249
|
+
def self.random
|
|
250
|
+
require 'benchmark/ips'
|
|
251
|
+
require 'securerandom'
|
|
252
|
+
begin
|
|
253
|
+
require 'openssl'
|
|
254
|
+
rescue LoadError
|
|
255
|
+
nil
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# -----------------------------------------------------------------------
|
|
259
|
+
# UUID generation
|
|
260
|
+
# -----------------------------------------------------------------------
|
|
261
|
+
puts "\nUUID generation (random v4)"
|
|
262
|
+
::Benchmark.ips do |bm|
|
|
263
|
+
bm.report('Iodine::Utils.uuid') { Iodine::Utils.uuid }
|
|
264
|
+
bm.report(' SecureRandom.uuid') { SecureRandom.uuid }
|
|
265
|
+
bm.report(' Random.uuid_v4') { Random.uuid_v4 }
|
|
266
|
+
bm.compare!
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# -----------------------------------------------------------------------
|
|
270
|
+
# Random bytes generation — all generators ranked per size
|
|
271
|
+
#
|
|
272
|
+
# Iodine::Utils.random uses a fast PRNG (not cryptographically secure).
|
|
273
|
+
# Iodine::Utils.secure_random requests system CSPRNG bytes (with a
|
|
274
|
+
# PRNG fallback if the system source fails); unlike random, it is
|
|
275
|
+
# intended for keys and nonces under normal operation.
|
|
276
|
+
# Compared against SecureRandom.random_bytes and OpenSSL::Random.random_bytes.
|
|
277
|
+
# -----------------------------------------------------------------------
|
|
278
|
+
puts "\nRandom bytes — all generators ranked — 16 bytes"
|
|
279
|
+
::Benchmark.ips do |bm|
|
|
280
|
+
bm.report('Iodine::Utils.random(16) ') { Iodine::Utils.random(16) }
|
|
281
|
+
bm.report('Random.bytes(16) ') { ::Random.bytes(16) }
|
|
282
|
+
bm.report('Iodine::Utils.secure_random(bytes: 16)') { Iodine::Utils.secure_random(bytes: 16) }
|
|
283
|
+
bm.report('SecureRandom.random_bytes(16) ') { SecureRandom.random_bytes(16) }
|
|
284
|
+
if defined?(OpenSSL)
|
|
285
|
+
bm.report('OpenSSL::Random.random_bytes(16) ') { OpenSSL::Random.random_bytes(16) }
|
|
286
|
+
end
|
|
287
|
+
bm.compare!
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
puts "\nRandom bytes — all generators ranked — 4096 bytes"
|
|
291
|
+
::Benchmark.ips do |bm|
|
|
292
|
+
bm.report('Iodine::Utils.random(4096) ') { Iodine::Utils.random(4096) }
|
|
293
|
+
bm.report('Random.bytes(4096) ') { ::Random.bytes(4096) }
|
|
294
|
+
bm.report('Iodine::Utils.secure_random(bytes: 4096)') { Iodine::Utils.secure_random(bytes: 4096) }
|
|
295
|
+
bm.report('SecureRandom.random_bytes(4096) ') { SecureRandom.random_bytes(4096) }
|
|
296
|
+
if defined?(OpenSSL)
|
|
297
|
+
bm.report('OpenSSL::Random.random_bytes(4096) ') { OpenSSL::Random.random_bytes(4096) }
|
|
298
|
+
end
|
|
299
|
+
bm.compare!
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
puts "\nRandom float (0.0..1.0)"
|
|
303
|
+
::Benchmark.ips do |bm|
|
|
304
|
+
bm.report('Iodine::Utils.random') { Iodine::Utils.random }
|
|
305
|
+
bm.report(' SecureRandom.rand') { SecureRandom.rand }
|
|
306
|
+
bm.report(' Random.rand') { ::Random.rand }
|
|
307
|
+
bm.compare!
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
nil
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Benchmark the {Iodine::Utils} module and see if you want to use {Iodine::Utils.monkey_patch} when using Rack.
|
|
314
|
+
def self.utils
|
|
315
|
+
require 'rack'
|
|
316
|
+
require 'cgi'
|
|
317
|
+
require 'benchmark/ips'
|
|
318
|
+
# a String in need of decoding
|
|
319
|
+
encoded = '%E3 + %83 + %AB + %E3 + %83 + %93 + %E3 + %82 + %A4 + %E3 + %82 + %B9 + %E3 + %81 + %A8'
|
|
320
|
+
decoded = ::Rack::Utils.unescape(encoded, 'binary')
|
|
321
|
+
html_xss = "<script>alert('avoid xss attacks')</script>"
|
|
322
|
+
html_xss_safe = Rack::Utils.escape_html html_xss
|
|
323
|
+
short_str1 = Array.new(64) { 'a' }.join
|
|
324
|
+
short_str2 = Array.new(64) { 'a' }.join
|
|
325
|
+
long_str1 = Array.new(4094) { 'a' }.join
|
|
326
|
+
long_str2 = Array.new(4094) { 'a' }.join
|
|
327
|
+
now_preclaculated = Time.now
|
|
328
|
+
::Benchmark.ips do |bm|
|
|
329
|
+
bm.report(' Iodine rfc2822') { Iodine::Utils.rfc2822(now_preclaculated) }
|
|
330
|
+
bm.report(' Rack rfc2822') { ::Rack::Utils.rfc2822(now_preclaculated) }
|
|
331
|
+
bm.compare!
|
|
332
|
+
end; ::Benchmark.ips do |bm|
|
|
333
|
+
bm.report('Iodine unescape') { Iodine::Utils.unescape encoded }
|
|
334
|
+
bm.report(' Rack unescape') { ::Rack::Utils.unescape encoded }
|
|
335
|
+
bm.compare!
|
|
336
|
+
end; ::Benchmark.ips do |bm|
|
|
337
|
+
bm.report('Iodine escape') { Iodine::Utils.escape decoded }
|
|
338
|
+
bm.report(' Rack escape') { ::Rack::Utils.escape decoded }
|
|
339
|
+
bm.compare!
|
|
340
|
+
end; ::Benchmark.ips do |bm|
|
|
341
|
+
bm.report('Iodine escape HTML') { Iodine::Utils.escape_html html_xss }
|
|
342
|
+
bm.report(' Rack escape HTML') { ::Rack::Utils.escape_html html_xss }
|
|
343
|
+
bm.compare!
|
|
344
|
+
end; ::Benchmark.ips do |bm|
|
|
345
|
+
bm.report('Iodine unescape HTML') { Iodine::Utils.unescape_html html_xss_safe }
|
|
346
|
+
bm.report(' CGI unescape HTML') { CGI.unescapeHTML html_xss_safe }
|
|
347
|
+
bm.compare!
|
|
348
|
+
end; ::Benchmark.ips do |bm|
|
|
349
|
+
bm.report("Iodine secure compare (#{short_str2.bytesize} Bytes)") do
|
|
350
|
+
Iodine::Utils.secure_compare short_str1, short_str2
|
|
351
|
+
end
|
|
352
|
+
bm.report(" Rack secure compare (#{short_str2.bytesize} Bytes)") do
|
|
353
|
+
::Rack::Utils.secure_compare short_str1, short_str2
|
|
354
|
+
end
|
|
355
|
+
bm.compare!
|
|
356
|
+
end; ::Benchmark.ips do |bm|
|
|
357
|
+
bm.report("Iodine secure compare (#{long_str1.bytesize} Bytes)") do
|
|
358
|
+
Iodine::Utils.secure_compare long_str1, long_str2
|
|
359
|
+
end
|
|
360
|
+
bm.report(" Rack secure compare (#{long_str1.bytesize} Bytes)") do
|
|
361
|
+
::Rack::Utils.secure_compare long_str1, long_str2
|
|
362
|
+
end
|
|
363
|
+
bm.compare!
|
|
364
|
+
end && nil
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Benchmark the internal mini-Hash map.
|
|
368
|
+
def self.minimap(counter = 20)
|
|
369
|
+
require 'benchmark/ips'
|
|
370
|
+
keys = []
|
|
371
|
+
(counter + 1).times { |i| keys << "counter_string_#{i}" }
|
|
372
|
+
frozen_keys = []
|
|
373
|
+
(counter + 1).times { |i| frozen_keys << "counter_string_#{i}".freeze }
|
|
374
|
+
|
|
375
|
+
tests = [
|
|
376
|
+
["New (#{counter * 2} Empty Maps)", proc { |m| counter.times { m.class.new } }, true],
|
|
377
|
+
["New + Set (#{counter} Numbers)", proc { |m| counter.times { |i| m[i] = i } }, true],
|
|
378
|
+
["Overwrite (#{counter} Numbers)", proc { |m| counter.times { |i| m[i] = i } }],
|
|
379
|
+
["Get (#{counter} Numbers + 1 missing)", proc { |m| (counter + 1).times { |i| m[i] } }],
|
|
380
|
+
["New + Set (#{counter} Strings)", proc { |m| counter.times { |i| m["counter_string_#{i}"] = i } }, true],
|
|
381
|
+
["Overwrite (#{counter} Strings)", proc { |m| counter.times { |i| m["counter_string_#{i}"] = i } }],
|
|
382
|
+
["Get (#{counter} Strings + 1 missing)", proc { |m| (counter + 1).times { |i| m["counter_string_#{i}"] } }],
|
|
383
|
+
["New + Set (#{counter} Strings.freeze)", proc { |m|
|
|
384
|
+
counter.times do |i|
|
|
385
|
+
m[frozen_keys[i]] = keys[i]
|
|
386
|
+
end
|
|
387
|
+
}, true],
|
|
388
|
+
["Overwrite (#{counter} Strings.freeze)", proc { |m| counter.times { |i| m[frozen_keys[i]] = keys[i] } }],
|
|
389
|
+
["Get (#{counter} Strings.freeze + 1 missing)", proc { |m| (counter + 1).times { |i| m[frozen_keys[i]] } }],
|
|
390
|
+
["each (#{counter} Strings)", proc { |m|
|
|
391
|
+
m.each do |k, v|
|
|
392
|
+
end
|
|
393
|
+
}]
|
|
394
|
+
]
|
|
395
|
+
|
|
396
|
+
klasses = [Iodine::Base::MiniMap, ::Hash]
|
|
397
|
+
|
|
398
|
+
maps = []
|
|
399
|
+
klasses.each { |k| maps << k.new }
|
|
400
|
+
|
|
401
|
+
tests.each do |t|
|
|
402
|
+
::Benchmark.ips do |bm|
|
|
403
|
+
klasses.each_index do |i|
|
|
404
|
+
if t[2]
|
|
405
|
+
bm.report("#{klasses[i].name.ljust(22)} #{t[0]}") do
|
|
406
|
+
maps[i] = klasses[i].new
|
|
407
|
+
t[1].call(maps[i])
|
|
408
|
+
end
|
|
409
|
+
else
|
|
410
|
+
bm.report("#{klasses[i].name.ljust(22)} #{t[0]}") { t[1].call(maps[i]) }
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
bm.compare!
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
nil
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# Benchmarks Iodine's AEAD symmetric encryption ciphers vs OpenSSL equivalents.
|
|
420
|
+
#
|
|
421
|
+
# Tests ChaCha20-Poly1305, XChaCha20-Poly1305, AES-128-GCM, and AES-256-GCM
|
|
422
|
+
# with various payload sizes (64 bytes, 1KB, 64KB).
|
|
423
|
+
#
|
|
424
|
+
# Requires OpenSSL for comparison benchmarks.
|
|
425
|
+
#
|
|
426
|
+
# FAIRNESS NOTES:
|
|
427
|
+
# - All ciphertexts for decryption are pre-computed OUTSIDE the hot loop so
|
|
428
|
+
# that only the actual encrypt/decrypt operation is timed.
|
|
429
|
+
# - OpenSSL cipher setup (new/encrypt/key/iv/auth_data) is inside the hot
|
|
430
|
+
# loop because Iodine also re-initialises state per call — both sides pay
|
|
431
|
+
# the same per-call setup cost.
|
|
432
|
+
# - XChaCha20-Poly1305 has no OpenSSL equivalent; it is included in the
|
|
433
|
+
# unified block with a label suffix so its throughput is visible in context.
|
|
434
|
+
# - All implementations use the same key, nonce, and additional data.
|
|
435
|
+
def self.crypto_symmetric
|
|
436
|
+
require 'benchmark/ips'
|
|
437
|
+
begin
|
|
438
|
+
require 'openssl'
|
|
439
|
+
rescue LoadError
|
|
440
|
+
nil
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# Test data sizes
|
|
444
|
+
sizes = {
|
|
445
|
+
'64B' => 64,
|
|
446
|
+
'1KB' => 1024,
|
|
447
|
+
'64KB' => 65_536
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
# Keys and nonces — generated once, reused across all benchmarks
|
|
451
|
+
key_32 = Iodine::Utils.random(32)
|
|
452
|
+
key_16 = Iodine::Utils.random(16)
|
|
453
|
+
nonce_12 = Iodine::Utils.random(12)
|
|
454
|
+
nonce_24 = Iodine::Utils.random(24)
|
|
455
|
+
ad = 'additional authenticated data'
|
|
456
|
+
|
|
457
|
+
# Check which OpenSSL ciphers are available
|
|
458
|
+
openssl_chacha_available = defined?(OpenSSL) &&
|
|
459
|
+
OpenSSL::Cipher.ciphers.include?('chacha20-poly1305')
|
|
460
|
+
openssl_aes_available = defined?(OpenSSL)
|
|
461
|
+
|
|
462
|
+
# Label width — wide enough for 'Iodine XChaCha20-Poly1305 encrypt (24B nonce)'
|
|
463
|
+
lw = 36
|
|
464
|
+
|
|
465
|
+
puts '=' * 70
|
|
466
|
+
puts 'AEAD Symmetric Encryption Benchmarks'
|
|
467
|
+
puts '=' * 70
|
|
468
|
+
puts
|
|
469
|
+
puts 'NOTE: All ciphertexts are pre-computed outside the hot loop.'
|
|
470
|
+
puts 'OpenSSL cipher setup is inside the loop (same cost as Iodine).'
|
|
471
|
+
puts 'XChaCha20-Poly1305 has no OpenSSL equivalent — included in the'
|
|
472
|
+
puts 'unified block with a label note so throughput is visible in context.'
|
|
473
|
+
puts
|
|
474
|
+
|
|
475
|
+
sizes.each do |size_name, size|
|
|
476
|
+
plaintext = Iodine::Utils.random(size)
|
|
477
|
+
|
|
478
|
+
puts "\n--- Payload Size: #{size_name} — all ciphers ranked together ---\n\n"
|
|
479
|
+
|
|
480
|
+
# ------------------------------------------------------------------
|
|
481
|
+
# Pre-compute all ciphertexts outside the hot loop so that only the
|
|
482
|
+
# decrypt operation itself is timed in the decrypt block below.
|
|
483
|
+
# ------------------------------------------------------------------
|
|
484
|
+
iodine_ct_chacha, iodine_mac_chacha =
|
|
485
|
+
Iodine::Base::Crypto::ChaCha20Poly1305.encrypt(plaintext, key: key_32, nonce: nonce_12, ad: ad)
|
|
486
|
+
|
|
487
|
+
iodine_ct_xchacha, iodine_mac_xchacha =
|
|
488
|
+
Iodine::Base::Crypto::XChaCha20Poly1305.encrypt(plaintext, key: key_32, nonce: nonce_24, ad: ad)
|
|
489
|
+
|
|
490
|
+
iodine_ct_aes128, iodine_mac_aes128 =
|
|
491
|
+
Iodine::Base::Crypto::AES128GCM.encrypt(plaintext, key: key_16, nonce: nonce_12, ad: ad)
|
|
492
|
+
|
|
493
|
+
iodine_ct_aes256, iodine_mac_aes256 =
|
|
494
|
+
Iodine::Base::Crypto::AES256GCM.encrypt(plaintext, key: key_32, nonce: nonce_12, ad: ad)
|
|
495
|
+
|
|
496
|
+
openssl_ct_chacha = nil
|
|
497
|
+
openssl_tag_chacha = nil
|
|
498
|
+
if openssl_chacha_available
|
|
499
|
+
c = OpenSSL::Cipher.new('chacha20-poly1305')
|
|
500
|
+
c.encrypt
|
|
501
|
+
c.key = key_32
|
|
502
|
+
c.iv = nonce_12
|
|
503
|
+
c.auth_data = ad
|
|
504
|
+
openssl_ct_chacha = c.update(plaintext) + c.final
|
|
505
|
+
openssl_tag_chacha = c.auth_tag
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
openssl_ct_aes128 = nil
|
|
509
|
+
openssl_tag_aes128 = nil
|
|
510
|
+
openssl_ct_aes256 = nil
|
|
511
|
+
openssl_tag_aes256 = nil
|
|
512
|
+
if openssl_aes_available
|
|
513
|
+
c = OpenSSL::Cipher.new('aes-128-gcm')
|
|
514
|
+
c.encrypt
|
|
515
|
+
c.key = key_16
|
|
516
|
+
c.iv = nonce_12
|
|
517
|
+
c.auth_data = ad
|
|
518
|
+
openssl_ct_aes128 = c.update(plaintext) + c.final
|
|
519
|
+
openssl_tag_aes128 = c.auth_tag
|
|
520
|
+
|
|
521
|
+
c = OpenSSL::Cipher.new('aes-256-gcm')
|
|
522
|
+
c.encrypt
|
|
523
|
+
c.key = key_32
|
|
524
|
+
c.iv = nonce_12
|
|
525
|
+
c.auth_data = ad
|
|
526
|
+
openssl_ct_aes256 = c.update(plaintext) + c.final
|
|
527
|
+
openssl_tag_aes256 = c.auth_tag
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
# ------------------------------------------------------------------
|
|
531
|
+
# Unified encrypt block — all ciphers compete side-by-side.
|
|
532
|
+
# XChaCha20 has no OpenSSL equivalent — labelled with (no OSSl equiv).
|
|
533
|
+
# x.compare! prints a ranked table so the fastest cipher is clear.
|
|
534
|
+
# ------------------------------------------------------------------
|
|
535
|
+
puts "Encrypt — all ciphers ranked:"
|
|
536
|
+
puts " (XChaCha20-Poly1305 has no OpenSSL equivalent — included for ranking context)"
|
|
537
|
+
::Benchmark.ips do |x|
|
|
538
|
+
x.report('Iodine ChaCha20-Poly1305 encrypt'.ljust(lw)) do
|
|
539
|
+
Iodine::Base::Crypto::ChaCha20Poly1305.encrypt(plaintext, key: key_32, nonce: nonce_12, ad: ad)
|
|
540
|
+
end
|
|
541
|
+
if openssl_chacha_available
|
|
542
|
+
# OpenSSL cipher setup is inside the loop — same per-call cost as Iodine
|
|
543
|
+
x.report('OpenSSL ChaCha20-Poly1305 encrypt'.ljust(lw)) do
|
|
544
|
+
c = OpenSSL::Cipher.new('chacha20-poly1305')
|
|
545
|
+
c.encrypt
|
|
546
|
+
c.key = key_32
|
|
547
|
+
c.iv = nonce_12
|
|
548
|
+
c.auth_data = ad
|
|
549
|
+
_ = c.update(plaintext) + c.final
|
|
550
|
+
c.auth_tag
|
|
551
|
+
end
|
|
552
|
+
end
|
|
553
|
+
# XChaCha20 has no OpenSSL equivalent — included for ranking context
|
|
554
|
+
x.report('Iodine XChaCha20-Poly1305 encrypt (24B nonce)'.ljust(lw)) do
|
|
555
|
+
Iodine::Base::Crypto::XChaCha20Poly1305.encrypt(plaintext, key: key_32, nonce: nonce_24, ad: ad)
|
|
556
|
+
end
|
|
557
|
+
x.report('Iodine AES-128-GCM encrypt'.ljust(lw)) do
|
|
558
|
+
Iodine::Base::Crypto::AES128GCM.encrypt(plaintext, key: key_16, nonce: nonce_12, ad: ad)
|
|
559
|
+
end
|
|
560
|
+
if openssl_aes_available
|
|
561
|
+
x.report('OpenSSL AES-128-GCM encrypt'.ljust(lw)) do
|
|
562
|
+
c = OpenSSL::Cipher.new('aes-128-gcm')
|
|
563
|
+
c.encrypt
|
|
564
|
+
c.key = key_16
|
|
565
|
+
c.iv = nonce_12
|
|
566
|
+
c.auth_data = ad
|
|
567
|
+
_ = c.update(plaintext) + c.final
|
|
568
|
+
c.auth_tag
|
|
569
|
+
end
|
|
570
|
+
end
|
|
571
|
+
x.report('Iodine AES-256-GCM encrypt'.ljust(lw)) do
|
|
572
|
+
Iodine::Base::Crypto::AES256GCM.encrypt(plaintext, key: key_32, nonce: nonce_12, ad: ad)
|
|
573
|
+
end
|
|
574
|
+
if openssl_aes_available
|
|
575
|
+
x.report('OpenSSL AES-256-GCM encrypt'.ljust(lw)) do
|
|
576
|
+
c = OpenSSL::Cipher.new('aes-256-gcm')
|
|
577
|
+
c.encrypt
|
|
578
|
+
c.key = key_32
|
|
579
|
+
c.iv = nonce_12
|
|
580
|
+
c.auth_data = ad
|
|
581
|
+
_ = c.update(plaintext) + c.final
|
|
582
|
+
c.auth_tag
|
|
583
|
+
end
|
|
584
|
+
end
|
|
585
|
+
x.compare!
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
# ------------------------------------------------------------------
|
|
589
|
+
# Unified decrypt block — all ciphers compete side-by-side.
|
|
590
|
+
# Ciphertexts were pre-computed above (outside the hot loop).
|
|
591
|
+
# x.compare! prints a ranked table so the fastest cipher is clear.
|
|
592
|
+
# ------------------------------------------------------------------
|
|
593
|
+
puts "\nDecrypt — all ciphers ranked:"
|
|
594
|
+
puts " (XChaCha20-Poly1305 has no OpenSSL equivalent — included for ranking context)"
|
|
595
|
+
::Benchmark.ips do |x|
|
|
596
|
+
x.report('Iodine ChaCha20-Poly1305 decrypt'.ljust(lw)) do
|
|
597
|
+
Iodine::Base::Crypto::ChaCha20Poly1305.decrypt(
|
|
598
|
+
iodine_ct_chacha, mac: iodine_mac_chacha, key: key_32, nonce: nonce_12, ad: ad
|
|
599
|
+
)
|
|
600
|
+
end
|
|
601
|
+
if openssl_chacha_available
|
|
602
|
+
x.report('OpenSSL ChaCha20-Poly1305 decrypt'.ljust(lw)) do
|
|
603
|
+
d = OpenSSL::Cipher.new('chacha20-poly1305')
|
|
604
|
+
d.decrypt
|
|
605
|
+
d.key = key_32
|
|
606
|
+
d.iv = nonce_12
|
|
607
|
+
d.auth_data = ad
|
|
608
|
+
d.auth_tag = openssl_tag_chacha
|
|
609
|
+
d.update(openssl_ct_chacha) + d.final
|
|
610
|
+
end
|
|
611
|
+
end
|
|
612
|
+
# XChaCha20 has no OpenSSL equivalent — included for ranking context
|
|
613
|
+
x.report('Iodine XChaCha20-Poly1305 decrypt (24B nonce)'.ljust(lw)) do
|
|
614
|
+
Iodine::Base::Crypto::XChaCha20Poly1305.decrypt(
|
|
615
|
+
iodine_ct_xchacha, mac: iodine_mac_xchacha, key: key_32, nonce: nonce_24, ad: ad
|
|
616
|
+
)
|
|
617
|
+
end
|
|
618
|
+
x.report('Iodine AES-128-GCM decrypt'.ljust(lw)) do
|
|
619
|
+
Iodine::Base::Crypto::AES128GCM.decrypt(
|
|
620
|
+
iodine_ct_aes128, mac: iodine_mac_aes128, key: key_16, nonce: nonce_12, ad: ad
|
|
621
|
+
)
|
|
622
|
+
end
|
|
623
|
+
if openssl_aes_available
|
|
624
|
+
x.report('OpenSSL AES-128-GCM decrypt'.ljust(lw)) do
|
|
625
|
+
d = OpenSSL::Cipher.new('aes-128-gcm')
|
|
626
|
+
d.decrypt
|
|
627
|
+
d.key = key_16
|
|
628
|
+
d.iv = nonce_12
|
|
629
|
+
d.auth_data = ad
|
|
630
|
+
d.auth_tag = openssl_tag_aes128
|
|
631
|
+
d.update(openssl_ct_aes128) + d.final
|
|
632
|
+
end
|
|
633
|
+
end
|
|
634
|
+
x.report('Iodine AES-256-GCM decrypt'.ljust(lw)) do
|
|
635
|
+
Iodine::Base::Crypto::AES256GCM.decrypt(
|
|
636
|
+
iodine_ct_aes256, mac: iodine_mac_aes256, key: key_32, nonce: nonce_12, ad: ad
|
|
637
|
+
)
|
|
638
|
+
end
|
|
639
|
+
if openssl_aes_available
|
|
640
|
+
x.report('OpenSSL AES-256-GCM decrypt'.ljust(lw)) do
|
|
641
|
+
d = OpenSSL::Cipher.new('aes-256-gcm')
|
|
642
|
+
d.decrypt
|
|
643
|
+
d.key = key_32
|
|
644
|
+
d.iv = nonce_12
|
|
645
|
+
d.auth_data = ad
|
|
646
|
+
d.auth_tag = openssl_tag_aes256
|
|
647
|
+
d.update(openssl_ct_aes256) + d.final
|
|
648
|
+
end
|
|
649
|
+
end
|
|
650
|
+
x.compare!
|
|
651
|
+
end
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
nil
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
# Benchmarks Iodine's asymmetric cryptography vs OpenSSL/RbNaCl equivalents.
|
|
658
|
+
#
|
|
659
|
+
# Tests Ed25519 signing/verification and X25519 key exchange.
|
|
660
|
+
#
|
|
661
|
+
# Optional dependencies: OpenSSL (>= 1.1.1 for Ed25519/X25519), RbNaCl gem.
|
|
662
|
+
def self.crypto_asymmetric
|
|
663
|
+
require 'benchmark/ips'
|
|
664
|
+
begin
|
|
665
|
+
require 'openssl'
|
|
666
|
+
rescue LoadError
|
|
667
|
+
nil
|
|
668
|
+
end
|
|
669
|
+
begin
|
|
670
|
+
require 'rbnacl'
|
|
671
|
+
rescue LoadError
|
|
672
|
+
nil
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
puts '=' * 70
|
|
676
|
+
puts 'Asymmetric Cryptography Benchmarks'
|
|
677
|
+
puts '=' * 70
|
|
678
|
+
|
|
679
|
+
# Ed25519 Signing
|
|
680
|
+
puts "\n--- Ed25519 Digital Signatures ---\n\n"
|
|
681
|
+
|
|
682
|
+
iodine_sk, iodine_pk = Iodine::Base::Crypto::Ed25519.keypair
|
|
683
|
+
message_short = 'Short message for signing'
|
|
684
|
+
message_long = Iodine::Utils.random(4096)
|
|
685
|
+
|
|
686
|
+
# Prepare OpenSSL Ed25519 keys if available
|
|
687
|
+
openssl_ed25519_available = false
|
|
688
|
+
if defined?(OpenSSL)
|
|
689
|
+
begin
|
|
690
|
+
openssl_ed_key = OpenSSL::PKey.generate_key('ED25519')
|
|
691
|
+
openssl_ed25519_available = true
|
|
692
|
+
rescue StandardError => e
|
|
693
|
+
puts "OpenSSL Ed25519 not available: #{e.message}"
|
|
694
|
+
end
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
# Prepare RbNaCl keys if available — probe via actual instantiation so
|
|
698
|
+
# that a partially-loaded gem (defined?(RbNaCl) true but sub-constants
|
|
699
|
+
# missing) is caught cleanly rather than raising NameError later.
|
|
700
|
+
rbnacl_available = false
|
|
701
|
+
rbnacl_signing_key = nil
|
|
702
|
+
rbnacl_verify_key = nil
|
|
703
|
+
begin
|
|
704
|
+
rbnacl_signing_key = RbNaCl::SigningKey.new(iodine_sk)
|
|
705
|
+
rbnacl_verify_key = rbnacl_signing_key.verify_key
|
|
706
|
+
rbnacl_available = true
|
|
707
|
+
rescue NameError, LoadError => e
|
|
708
|
+
puts "RbNaCl not available (#{e.message}); skipping RbNaCl comparisons."
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
# Label width — wide enough for 'Iodine Ed25519 sign (4KB) '
|
|
712
|
+
lw = 34
|
|
713
|
+
|
|
714
|
+
puts 'Ed25519 Key Generation:'
|
|
715
|
+
::Benchmark.ips do |x|
|
|
716
|
+
x.report('Iodine Ed25519.keypair') do
|
|
717
|
+
Iodine::Base::Crypto::Ed25519.keypair
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
if openssl_ed25519_available
|
|
721
|
+
x.report('OpenSSL Ed25519 generate') do
|
|
722
|
+
OpenSSL::PKey.generate_key('ED25519')
|
|
723
|
+
end
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
if rbnacl_available
|
|
727
|
+
x.report('RbNaCl SigningKey.generate') do
|
|
728
|
+
RbNaCl::SigningKey.generate
|
|
729
|
+
end
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
x.compare!
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
# ------------------------------------------------------------------
|
|
736
|
+
# Unified signing block — both message sizes compete side-by-side.
|
|
737
|
+
# This lets readers see how message length affects signing throughput.
|
|
738
|
+
# x.compare! prints a ranked table across all entries.
|
|
739
|
+
# ------------------------------------------------------------------
|
|
740
|
+
puts "\nEd25519 Signing — all implementations and message sizes ranked:"
|
|
741
|
+
::Benchmark.ips do |x|
|
|
742
|
+
x.report("Iodine Ed25519 sign (#{message_short.bytesize}B)".ljust(lw)) do
|
|
743
|
+
Iodine::Base::Crypto::Ed25519.sign(message_short, secret_key: iodine_sk, public_key: iodine_pk)
|
|
744
|
+
end
|
|
745
|
+
x.report("Iodine Ed25519 sign (#{message_long.bytesize / 1024}KB)".ljust(lw)) do
|
|
746
|
+
Iodine::Base::Crypto::Ed25519.sign(message_long, secret_key: iodine_sk, public_key: iodine_pk)
|
|
747
|
+
end
|
|
748
|
+
if openssl_ed25519_available
|
|
749
|
+
x.report("OpenSSL Ed25519 sign (#{message_short.bytesize}B)".ljust(lw)) do
|
|
750
|
+
openssl_ed_key.sign(nil, message_short)
|
|
751
|
+
end
|
|
752
|
+
x.report("OpenSSL Ed25519 sign (#{message_long.bytesize / 1024}KB)".ljust(lw)) do
|
|
753
|
+
openssl_ed_key.sign(nil, message_long)
|
|
754
|
+
end
|
|
755
|
+
end
|
|
756
|
+
if rbnacl_available
|
|
757
|
+
x.report("RbNaCl sign (#{message_short.bytesize}B)".ljust(lw)) do
|
|
758
|
+
rbnacl_signing_key.sign(message_short)
|
|
759
|
+
end
|
|
760
|
+
x.report("RbNaCl sign (#{message_long.bytesize / 1024}KB)".ljust(lw)) do
|
|
761
|
+
rbnacl_signing_key.sign(message_long)
|
|
762
|
+
end
|
|
763
|
+
end
|
|
764
|
+
x.compare!
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
# Verification benchmark
|
|
768
|
+
iodine_signature = Iodine::Base::Crypto::Ed25519.sign(message_short, secret_key: iodine_sk, public_key: iodine_pk)
|
|
769
|
+
|
|
770
|
+
puts "\nEd25519 Verification (short message):"
|
|
771
|
+
::Benchmark.ips do |x|
|
|
772
|
+
x.report('Iodine Ed25519.verify') do
|
|
773
|
+
Iodine::Base::Crypto::Ed25519.verify(message_short, iodine_signature, public_key: iodine_pk)
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
if openssl_ed25519_available
|
|
777
|
+
openssl_sig = openssl_ed_key.sign(nil, message_short)
|
|
778
|
+
x.report('OpenSSL Ed25519 verify') do
|
|
779
|
+
openssl_ed_key.verify(nil, openssl_sig, message_short)
|
|
780
|
+
end
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
if rbnacl_available
|
|
784
|
+
rbnacl_sig = rbnacl_signing_key.sign(message_short)
|
|
785
|
+
x.report('RbNaCl verify') do
|
|
786
|
+
rbnacl_verify_key.verify(rbnacl_sig, message_short)
|
|
787
|
+
end
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
x.compare!
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
# X25519 Key Exchange
|
|
794
|
+
puts "\n--- X25519 Key Exchange (ECDH) ---\n\n"
|
|
795
|
+
|
|
796
|
+
alice_sk, = Iodine::Base::Crypto::X25519.keypair
|
|
797
|
+
bob_sk, bob_pk = Iodine::Base::Crypto::X25519.keypair
|
|
798
|
+
|
|
799
|
+
# Prepare OpenSSL X25519 keys if available
|
|
800
|
+
openssl_x25519_available = false
|
|
801
|
+
if defined?(OpenSSL)
|
|
802
|
+
begin
|
|
803
|
+
openssl_alice = OpenSSL::PKey.generate_key('X25519')
|
|
804
|
+
openssl_bob = OpenSSL::PKey.generate_key('X25519')
|
|
805
|
+
openssl_x25519_available = true
|
|
806
|
+
rescue StandardError => e
|
|
807
|
+
puts "OpenSSL X25519 not available: #{e.message}"
|
|
808
|
+
end
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
# Prepare RbNaCl keys if available
|
|
812
|
+
if rbnacl_available
|
|
813
|
+
rbnacl_alice = RbNaCl::PrivateKey.new(alice_sk)
|
|
814
|
+
rbnacl_bob = RbNaCl::PrivateKey.new(bob_sk)
|
|
815
|
+
end
|
|
816
|
+
|
|
817
|
+
puts 'X25519 Key Generation:'
|
|
818
|
+
::Benchmark.ips do |x|
|
|
819
|
+
x.report('Iodine X25519.keypair') do
|
|
820
|
+
Iodine::Base::Crypto::X25519.keypair
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
if openssl_x25519_available
|
|
824
|
+
x.report('OpenSSL X25519 generate') do
|
|
825
|
+
OpenSSL::PKey.generate_key('X25519')
|
|
826
|
+
end
|
|
827
|
+
end
|
|
828
|
+
|
|
829
|
+
if rbnacl_available
|
|
830
|
+
x.report('RbNaCl PrivateKey.generate') do
|
|
831
|
+
RbNaCl::PrivateKey.generate
|
|
832
|
+
end
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
x.compare!
|
|
836
|
+
end
|
|
837
|
+
|
|
838
|
+
puts "\nX25519 Shared Secret Computation:"
|
|
839
|
+
::Benchmark.ips do |x|
|
|
840
|
+
x.report('Iodine X25519.shared_secret') do
|
|
841
|
+
Iodine::Base::Crypto::X25519.shared_secret(secret_key: alice_sk, their_public: bob_pk)
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
if openssl_x25519_available
|
|
845
|
+
x.report('OpenSSL X25519 derive') do
|
|
846
|
+
openssl_alice.derive(openssl_bob)
|
|
847
|
+
end
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
if rbnacl_available
|
|
851
|
+
x.report('RbNaCl GroupElement') do
|
|
852
|
+
RbNaCl::GroupElement.new(rbnacl_bob.public_key.to_bytes).mult(rbnacl_alice.to_bytes)
|
|
853
|
+
end
|
|
854
|
+
end
|
|
855
|
+
|
|
856
|
+
x.compare!
|
|
857
|
+
end
|
|
858
|
+
|
|
859
|
+
nil
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
# Benchmarks Iodine's hashing functions vs OpenSSL/Digest equivalents.
|
|
863
|
+
#
|
|
864
|
+
# Tests SHA-256, SHA-512, SHA3-224, SHA3-256, SHA3-512, BLAKE2b, BLAKE2s,
|
|
865
|
+
# SHAKE128, SHAKE256, risky256, and risky512 across three payload sizes
|
|
866
|
+
# (64B, 1KB, 64KB). risky256/risky512 are non-cryptographic but are
|
|
867
|
+
# included as faster alternatives suitable for message authentication where
|
|
868
|
+
# cryptographic strength is not required.
|
|
869
|
+
#
|
|
870
|
+
# For each payload size, ALL algorithms compete in a single Benchmark.ips
|
|
871
|
+
# block so results can be used directly for algorithm selection — you can
|
|
872
|
+
# immediately see "at 1KB, which hash is fastest?"
|
|
873
|
+
#
|
|
874
|
+
# Requires OpenSSL for comparison benchmarks.
|
|
875
|
+
#
|
|
876
|
+
# FAIRNESS NOTES:
|
|
877
|
+
# - OpenSSL::Digest objects are pre-instantiated ONCE before the sizes loop
|
|
878
|
+
# and reused across all iterations. Calling instance.digest(data) resets
|
|
879
|
+
# the existing EVP_MD_CTX state (via EVP_DigestInit_ex) without allocating
|
|
880
|
+
# a new context — only the hash computation itself is timed.
|
|
881
|
+
# - By contrast, the class methods OpenSSL::Digest.digest('SHA3-256', data)
|
|
882
|
+
# and OpenSSL::Digest::SHA256.digest(data) allocate a new EVP_MD_CTX AND
|
|
883
|
+
# run EVP_get_digestbyname() on every call — unfairly penalising OpenSSL.
|
|
884
|
+
# - Digest::SHA256/SHA512 (Ruby stdlib) delegates to OpenSSL internally.
|
|
885
|
+
# The same pre-instantiation fix is applied: Digest::SHA256.new is created
|
|
886
|
+
# once and instance.digest(data) is called in the hot loop.
|
|
887
|
+
# - SHA3 and BLAKE2 availability depends on the OpenSSL build. If not
|
|
888
|
+
# available, only Iodine results are shown for those algorithms.
|
|
889
|
+
# - SHAKE128/SHAKE256 have no OpenSSL Ruby binding; they are included in
|
|
890
|
+
# the unified block so their throughput is visible in the ranking.
|
|
891
|
+
#
|
|
892
|
+
# NOTE on digest == hash: Iodine's sha256/sha3_256/blake2b etc. and
|
|
893
|
+
# OpenSSL::Digest both compute cryptographic message digests — they are the
|
|
894
|
+
# same operation (deterministic, fixed-output-length, one-way hash functions
|
|
895
|
+
# over arbitrary input). The comparison is apples-to-apples.
|
|
896
|
+
def self.hashing
|
|
897
|
+
require 'benchmark/ips'
|
|
898
|
+
begin
|
|
899
|
+
require 'openssl'
|
|
900
|
+
rescue LoadError
|
|
901
|
+
nil
|
|
902
|
+
end
|
|
903
|
+
begin
|
|
904
|
+
require 'digest'
|
|
905
|
+
rescue LoadError
|
|
906
|
+
nil
|
|
907
|
+
end
|
|
908
|
+
|
|
909
|
+
puts '=' * 70
|
|
910
|
+
puts 'Hashing Benchmarks'
|
|
911
|
+
puts '=' * 70
|
|
912
|
+
puts
|
|
913
|
+
puts 'NOTE: All algorithms are benchmarked together per payload size so'
|
|
914
|
+
puts 'results can be used for algorithm selection. OpenSSL::Digest and'
|
|
915
|
+
puts 'Digest:: objects are pre-instantiated once before the sizes loop;'
|
|
916
|
+
puts 'instance.digest(data) reuses the EVP_MD_CTX (only resets state) —'
|
|
917
|
+
puts 'no per-call allocation or string lookup overhead.'
|
|
918
|
+
puts
|
|
919
|
+
|
|
920
|
+
# Test data sizes
|
|
921
|
+
sizes = {
|
|
922
|
+
'64B' => 64,
|
|
923
|
+
'1KB' => 1024,
|
|
924
|
+
'64KB' => 65_536
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
# ------------------------------------------------------------------
|
|
928
|
+
# Pre-check which OpenSSL algorithms are available and pre-instantiate
|
|
929
|
+
# digest objects ONCE here — before the sizes loop — so they are
|
|
930
|
+
# allocated exactly once regardless of how many size iterations run.
|
|
931
|
+
# ------------------------------------------------------------------
|
|
932
|
+
openssl_sha3_256_available = false
|
|
933
|
+
openssl_sha3_512_available = false
|
|
934
|
+
openssl_blake2b_available = false
|
|
935
|
+
openssl_blake2s_available = false
|
|
936
|
+
openssl_sha3_224_available = false
|
|
937
|
+
|
|
938
|
+
# Pre-instantiated digest objects (nil if algorithm unavailable)
|
|
939
|
+
ossl_sha256 = nil
|
|
940
|
+
ossl_sha512 = nil
|
|
941
|
+
ossl_sha3_256 = nil
|
|
942
|
+
ossl_sha3_512 = nil
|
|
943
|
+
ossl_blake2b = nil
|
|
944
|
+
ossl_blake2s = nil
|
|
945
|
+
ossl_sha3_224 = nil
|
|
946
|
+
digest_sha256 = nil
|
|
947
|
+
digest_sha512 = nil
|
|
948
|
+
|
|
949
|
+
if defined?(OpenSSL)
|
|
950
|
+
# SHA-256 and SHA-512 are always available in any OpenSSL build
|
|
951
|
+
ossl_sha256 = OpenSSL::Digest.new('SHA256')
|
|
952
|
+
ossl_sha512 = OpenSSL::Digest.new('SHA512')
|
|
953
|
+
|
|
954
|
+
begin
|
|
955
|
+
ossl_sha3_256 = OpenSSL::Digest.new('SHA3-256')
|
|
956
|
+
ossl_sha3_256.digest('') # probe — raises if unavailable
|
|
957
|
+
openssl_sha3_256_available = true
|
|
958
|
+
rescue OpenSSL::Digest::DigestError, RuntimeError
|
|
959
|
+
ossl_sha3_256 = nil
|
|
960
|
+
# SHA3-256 not available in this OpenSSL build
|
|
961
|
+
end
|
|
962
|
+
begin
|
|
963
|
+
ossl_sha3_512 = OpenSSL::Digest.new('SHA3-512')
|
|
964
|
+
ossl_sha3_512.digest('')
|
|
965
|
+
openssl_sha3_512_available = true
|
|
966
|
+
rescue OpenSSL::Digest::DigestError, RuntimeError
|
|
967
|
+
ossl_sha3_512 = nil
|
|
968
|
+
# SHA3-512 not available
|
|
969
|
+
end
|
|
970
|
+
begin
|
|
971
|
+
ossl_blake2b = OpenSSL::Digest.new('BLAKE2b512')
|
|
972
|
+
ossl_blake2b.digest('')
|
|
973
|
+
openssl_blake2b_available = true
|
|
974
|
+
rescue OpenSSL::Digest::DigestError, RuntimeError
|
|
975
|
+
ossl_blake2b = nil
|
|
976
|
+
# BLAKE2b not available
|
|
977
|
+
end
|
|
978
|
+
begin
|
|
979
|
+
ossl_blake2s = OpenSSL::Digest.new('BLAKE2s256')
|
|
980
|
+
ossl_blake2s.digest('')
|
|
981
|
+
openssl_blake2s_available = true
|
|
982
|
+
rescue OpenSSL::Digest::DigestError, RuntimeError
|
|
983
|
+
ossl_blake2s = nil
|
|
984
|
+
# BLAKE2s not available
|
|
985
|
+
end
|
|
986
|
+
begin
|
|
987
|
+
ossl_sha3_224 = OpenSSL::Digest.new('SHA3-224')
|
|
988
|
+
ossl_sha3_224.digest('')
|
|
989
|
+
openssl_sha3_224_available = true
|
|
990
|
+
rescue OpenSSL::Digest::DigestError, RuntimeError
|
|
991
|
+
ossl_sha3_224 = nil
|
|
992
|
+
# SHA3-224 not available in this OpenSSL build
|
|
993
|
+
end
|
|
994
|
+
end
|
|
995
|
+
|
|
996
|
+
if defined?(Digest)
|
|
997
|
+
# Ruby stdlib Digest — pre-instantiate once; instance.digest(data)
|
|
998
|
+
# calls reset+update+finish on the existing object (no allocation).
|
|
999
|
+
digest_sha256 = Digest::SHA256.new
|
|
1000
|
+
digest_sha512 = Digest::SHA512.new
|
|
1001
|
+
end
|
|
1002
|
+
|
|
1003
|
+
# Label width — wide enough for the longest entry ("Iodine risky512 (non-crypto, 64B)")
|
|
1004
|
+
lw = 34
|
|
1005
|
+
|
|
1006
|
+
sizes.each do |size_name, size|
|
|
1007
|
+
data = Iodine::Utils.random(size)
|
|
1008
|
+
|
|
1009
|
+
puts "\n--- Data Size: #{size_name} — all algorithms ranked together ---\n\n"
|
|
1010
|
+
puts " (SHAKE128/SHAKE256 have no OpenSSL Ruby binding; risky256/risky512 are non-crypto"
|
|
1011
|
+
puts " alternatives suitable for fast message authentication; all included for ranking context)"
|
|
1012
|
+
puts
|
|
1013
|
+
|
|
1014
|
+
# ------------------------------------------------------------------
|
|
1015
|
+
# Unified block: all algorithms compete side-by-side for this size.
|
|
1016
|
+
# x.compare! prints a ranked table so the fastest algorithm is clear.
|
|
1017
|
+
# ------------------------------------------------------------------
|
|
1018
|
+
::Benchmark.ips do |x|
|
|
1019
|
+
# ---- Iodine entries (always present) ----
|
|
1020
|
+
x.report('Iodine sha256'.ljust(lw)) { Iodine::Utils.sha256(data) }
|
|
1021
|
+
x.report('Iodine sha512'.ljust(lw)) { Iodine::Utils.sha512(data) }
|
|
1022
|
+
x.report('Iodine sha3_224'.ljust(lw)) { Iodine::Utils.sha3_224(data) }
|
|
1023
|
+
x.report('Iodine sha3_256'.ljust(lw)) { Iodine::Utils.sha3_256(data) }
|
|
1024
|
+
x.report('Iodine sha3_512'.ljust(lw)) { Iodine::Utils.sha3_512(data) }
|
|
1025
|
+
x.report('Iodine blake2b (64B)'.ljust(lw)) { Iodine::Utils.blake2b(data, len: 64) }
|
|
1026
|
+
x.report('Iodine blake2s (32B)'.ljust(lw)) { Iodine::Utils.blake2s(data, len: 32) }
|
|
1027
|
+
x.report('Iodine shake128 (XOF, 32B)'.ljust(lw)) { Iodine::Utils.shake128(data, length: 32) }
|
|
1028
|
+
x.report('Iodine shake256 (XOF, 64B)'.ljust(lw)) { Iodine::Utils.shake256(data, length: 64) }
|
|
1029
|
+
## risky256/risky512: non-crypto alternatives for fast message authentication
|
|
1030
|
+
# x.report('Iodine risky256 (non-crypto, 32B)'.ljust(lw)) { Iodine::Utils.risky256(data) }
|
|
1031
|
+
# x.report('Iodine risky512 (non-crypto, 64B)'.ljust(lw)) { Iodine::Utils.risky512(data) }
|
|
1032
|
+
|
|
1033
|
+
# ---- OpenSSL entries (conditionally present) ----
|
|
1034
|
+
if ossl_sha256
|
|
1035
|
+
x.report('OpenSSL SHA256'.ljust(lw)) { ossl_sha256.digest(data) }
|
|
1036
|
+
end
|
|
1037
|
+
if ossl_sha512
|
|
1038
|
+
x.report('OpenSSL SHA512'.ljust(lw)) { ossl_sha512.digest(data) }
|
|
1039
|
+
end
|
|
1040
|
+
if openssl_sha3_224_available
|
|
1041
|
+
x.report('OpenSSL SHA3-224'.ljust(lw)) { ossl_sha3_224.digest(data) }
|
|
1042
|
+
end
|
|
1043
|
+
if openssl_sha3_256_available
|
|
1044
|
+
x.report('OpenSSL SHA3-256'.ljust(lw)) { ossl_sha3_256.digest(data) }
|
|
1045
|
+
end
|
|
1046
|
+
if openssl_sha3_512_available
|
|
1047
|
+
x.report('OpenSSL SHA3-512'.ljust(lw)) { ossl_sha3_512.digest(data) }
|
|
1048
|
+
end
|
|
1049
|
+
if openssl_blake2b_available
|
|
1050
|
+
x.report('OpenSSL BLAKE2b512'.ljust(lw)) { ossl_blake2b.digest(data) }
|
|
1051
|
+
end
|
|
1052
|
+
if openssl_blake2s_available
|
|
1053
|
+
x.report('OpenSSL BLAKE2s256'.ljust(lw)) { ossl_blake2s.digest(data) }
|
|
1054
|
+
end
|
|
1055
|
+
|
|
1056
|
+
# ---- Ruby stdlib Digest entries (conditionally present) ----
|
|
1057
|
+
if digest_sha256
|
|
1058
|
+
x.report('Digest::SHA256'.ljust(lw)) { digest_sha256.digest(data) }
|
|
1059
|
+
end
|
|
1060
|
+
if digest_sha512
|
|
1061
|
+
x.report('Digest::SHA512'.ljust(lw)) { digest_sha512.digest(data) }
|
|
1062
|
+
end
|
|
1063
|
+
|
|
1064
|
+
puts "\n--- Data Size: #{size_name} ---\n\n"
|
|
1065
|
+
x.compare!
|
|
1066
|
+
end
|
|
1067
|
+
end
|
|
1068
|
+
|
|
1069
|
+
nil
|
|
1070
|
+
end
|
|
1071
|
+
|
|
1072
|
+
# Benchmarks Iodine's X25519 ECIES public-key encryption vs RbNaCl sealed boxes.
|
|
1073
|
+
#
|
|
1074
|
+
# Tests encryption/decryption with various payload sizes and all cipher variants.
|
|
1075
|
+
#
|
|
1076
|
+
# Optional dependencies: RbNaCl gem for comparison.
|
|
1077
|
+
#
|
|
1078
|
+
# FAIRNESS NOTES:
|
|
1079
|
+
# - All ciphertexts for decryption are pre-computed OUTSIDE the hot loop so
|
|
1080
|
+
# that only the actual decrypt operation is timed.
|
|
1081
|
+
# - All three Iodine cipher variants (ChaCha20, AES-128, AES-256) compete in
|
|
1082
|
+
# the unified encrypt block so readers can choose the right cipher.
|
|
1083
|
+
# - RbNaCl SimpleBox uses XSalsa20-Poly1305 (different cipher) — included for
|
|
1084
|
+
# throughput reference; label makes the difference clear.
|
|
1085
|
+
def self.crypto_ecies
|
|
1086
|
+
require 'benchmark/ips'
|
|
1087
|
+
begin
|
|
1088
|
+
require 'rbnacl'
|
|
1089
|
+
rescue LoadError
|
|
1090
|
+
nil
|
|
1091
|
+
end
|
|
1092
|
+
|
|
1093
|
+
puts '=' * 70
|
|
1094
|
+
puts 'X25519 ECIES (Public-Key Encryption) Benchmarks'
|
|
1095
|
+
puts '=' * 70
|
|
1096
|
+
|
|
1097
|
+
# Generate recipient keypair
|
|
1098
|
+
recipient_sk, recipient_pk = Iodine::Base::Crypto::X25519.keypair
|
|
1099
|
+
|
|
1100
|
+
# Test data sizes
|
|
1101
|
+
sizes = {
|
|
1102
|
+
'64B' => 64,
|
|
1103
|
+
'1KB' => 1024,
|
|
1104
|
+
'16KB' => 16_384
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
# Label width — wide enough for 'Iodine X25519.decrypt_aes256 (AES-256)'
|
|
1108
|
+
lw = 32
|
|
1109
|
+
|
|
1110
|
+
rbnacl_available = false
|
|
1111
|
+
rbnacl_pk = nil
|
|
1112
|
+
rbnacl_sk = nil
|
|
1113
|
+
begin
|
|
1114
|
+
rbnacl_sk = RbNaCl::PrivateKey.new(recipient_sk)
|
|
1115
|
+
rbnacl_pk = rbnacl_sk.public_key
|
|
1116
|
+
rbnacl_available = true
|
|
1117
|
+
rescue NameError, LoadError => e
|
|
1118
|
+
puts "RbNaCl not available (#{e.message}); skipping RbNaCl comparisons."
|
|
1119
|
+
end
|
|
1120
|
+
|
|
1121
|
+
sizes.each do |size_name, size|
|
|
1122
|
+
plaintext = Iodine::Utils.random(size)
|
|
1123
|
+
|
|
1124
|
+
puts "\n--- Payload Size: #{size_name} — all cipher variants ranked ---\n\n"
|
|
1125
|
+
|
|
1126
|
+
# ------------------------------------------------------------------
|
|
1127
|
+
# Pre-compute all ciphertexts outside the hot loop so that only the
|
|
1128
|
+
# decrypt operation itself is timed in the decrypt block below.
|
|
1129
|
+
# ------------------------------------------------------------------
|
|
1130
|
+
iodine_ct_chacha = Iodine::Base::Crypto::X25519.encrypt(plaintext, recipient_pk: recipient_pk)
|
|
1131
|
+
iodine_ct_aes128 = Iodine::Base::Crypto::X25519.encrypt_aes128(plaintext, recipient_pk: recipient_pk)
|
|
1132
|
+
iodine_ct_aes256 = Iodine::Base::Crypto::X25519.encrypt_aes256(plaintext, recipient_pk: recipient_pk)
|
|
1133
|
+
rbnacl_ciphertext = rbnacl_available ? RbNaCl::SimpleBox.from_public_key(rbnacl_pk).encrypt(plaintext) : nil
|
|
1134
|
+
|
|
1135
|
+
# ------------------------------------------------------------------
|
|
1136
|
+
# Unified encrypt block — all cipher variants compete side-by-side.
|
|
1137
|
+
# x.compare! prints a ranked table so the fastest cipher is clear.
|
|
1138
|
+
# ------------------------------------------------------------------
|
|
1139
|
+
puts "Encrypt — all cipher variants ranked:"
|
|
1140
|
+
::Benchmark.ips do |x|
|
|
1141
|
+
x.report('Iodine X25519.encrypt (ChaCha20)'.ljust(lw)) do
|
|
1142
|
+
Iodine::Base::Crypto::X25519.encrypt(plaintext, recipient_pk: recipient_pk)
|
|
1143
|
+
end
|
|
1144
|
+
x.report('Iodine X25519.encrypt_aes128'.ljust(lw)) do
|
|
1145
|
+
Iodine::Base::Crypto::X25519.encrypt_aes128(plaintext, recipient_pk: recipient_pk)
|
|
1146
|
+
end
|
|
1147
|
+
x.report('Iodine X25519.encrypt_aes256'.ljust(lw)) do
|
|
1148
|
+
Iodine::Base::Crypto::X25519.encrypt_aes256(plaintext, recipient_pk: recipient_pk)
|
|
1149
|
+
end
|
|
1150
|
+
if rbnacl_available
|
|
1151
|
+
# RbNaCl SimpleBox uses XSalsa20-Poly1305 — different cipher, shown for reference
|
|
1152
|
+
x.report('RbNaCl SimpleBox seal'.ljust(lw)) do
|
|
1153
|
+
RbNaCl::SimpleBox.from_public_key(rbnacl_pk).encrypt(plaintext)
|
|
1154
|
+
end
|
|
1155
|
+
end
|
|
1156
|
+
x.compare!
|
|
1157
|
+
end
|
|
1158
|
+
|
|
1159
|
+
# ------------------------------------------------------------------
|
|
1160
|
+
# Unified decrypt block — all cipher variants compete side-by-side.
|
|
1161
|
+
# Ciphertexts were pre-computed above (outside the hot loop).
|
|
1162
|
+
# x.compare! prints a ranked table so the fastest cipher is clear.
|
|
1163
|
+
# ------------------------------------------------------------------
|
|
1164
|
+
puts "\nDecrypt — all cipher variants ranked:"
|
|
1165
|
+
::Benchmark.ips do |x|
|
|
1166
|
+
x.report('Iodine X25519.decrypt (ChaCha20)'.ljust(lw)) do
|
|
1167
|
+
Iodine::Base::Crypto::X25519.decrypt(iodine_ct_chacha, secret_key: recipient_sk)
|
|
1168
|
+
end
|
|
1169
|
+
x.report('Iodine X25519.decrypt_aes128'.ljust(lw)) do
|
|
1170
|
+
Iodine::Base::Crypto::X25519.decrypt_aes128(iodine_ct_aes128, secret_key: recipient_sk)
|
|
1171
|
+
end
|
|
1172
|
+
x.report('Iodine X25519.decrypt_aes256'.ljust(lw)) do
|
|
1173
|
+
Iodine::Base::Crypto::X25519.decrypt_aes256(iodine_ct_aes256, secret_key: recipient_sk)
|
|
1174
|
+
end
|
|
1175
|
+
if rbnacl_available
|
|
1176
|
+
# RbNaCl SimpleBox uses XSalsa20-Poly1305 — different cipher, shown for reference
|
|
1177
|
+
x.report('RbNaCl SimpleBox open'.ljust(lw)) do
|
|
1178
|
+
RbNaCl::SimpleBox.from_keypair(rbnacl_pk, rbnacl_sk).decrypt(rbnacl_ciphertext)
|
|
1179
|
+
end
|
|
1180
|
+
end
|
|
1181
|
+
x.compare!
|
|
1182
|
+
end
|
|
1183
|
+
end
|
|
1184
|
+
|
|
1185
|
+
nil
|
|
1186
|
+
end
|
|
1187
|
+
|
|
1188
|
+
# Benchmarks Iodine's HKDF key derivation vs OpenSSL HKDF.
|
|
1189
|
+
#
|
|
1190
|
+
# Tests key derivation with various output lengths.
|
|
1191
|
+
#
|
|
1192
|
+
# Requires OpenSSL >= 1.1.0 for comparison benchmarks.
|
|
1193
|
+
def self.crypto_kdf
|
|
1194
|
+
require 'benchmark/ips'
|
|
1195
|
+
begin
|
|
1196
|
+
require 'openssl'
|
|
1197
|
+
rescue LoadError
|
|
1198
|
+
nil
|
|
1199
|
+
end
|
|
1200
|
+
|
|
1201
|
+
puts '=' * 70
|
|
1202
|
+
puts 'HKDF Key Derivation Benchmarks'
|
|
1203
|
+
puts '=' * 70
|
|
1204
|
+
|
|
1205
|
+
# Input keying material (e.g., from X25519 shared secret)
|
|
1206
|
+
ikm = Iodine::Utils.random(32)
|
|
1207
|
+
salt = Iodine::Utils.random(32)
|
|
1208
|
+
info = 'application specific context'
|
|
1209
|
+
|
|
1210
|
+
# Output lengths to test
|
|
1211
|
+
lengths = [16, 32, 64, 128]
|
|
1212
|
+
|
|
1213
|
+
# Check if OpenSSL HKDF is available
|
|
1214
|
+
openssl_hkdf_available = false
|
|
1215
|
+
if defined?(OpenSSL) && defined?(OpenSSL::KDF)
|
|
1216
|
+
begin
|
|
1217
|
+
OpenSSL::KDF.hkdf(ikm, salt: salt, info: info, length: 32, hash: 'SHA256')
|
|
1218
|
+
openssl_hkdf_available = true
|
|
1219
|
+
rescue StandardError => e
|
|
1220
|
+
puts "OpenSSL HKDF not available: #{e.message}"
|
|
1221
|
+
end
|
|
1222
|
+
end
|
|
1223
|
+
|
|
1224
|
+
lengths.each do |length|
|
|
1225
|
+
puts "\n--- Output Length: #{length} bytes ---\n\n"
|
|
1226
|
+
|
|
1227
|
+
::Benchmark.ips do |x|
|
|
1228
|
+
x.report('Iodine HKDF.derive (SHA-256)') do
|
|
1229
|
+
Iodine::Base::Crypto::HKDF.derive(ikm: ikm, salt: salt, info: info, length: length, sha384: false)
|
|
1230
|
+
end
|
|
1231
|
+
|
|
1232
|
+
x.report('Iodine HKDF.derive (SHA-384)') do
|
|
1233
|
+
Iodine::Base::Crypto::HKDF.derive(ikm: ikm, salt: salt, info: info, length: length, sha384: true)
|
|
1234
|
+
end
|
|
1235
|
+
|
|
1236
|
+
if openssl_hkdf_available
|
|
1237
|
+
x.report('OpenSSL HKDF (SHA-256)') do
|
|
1238
|
+
OpenSSL::KDF.hkdf(ikm, salt: salt, info: info, length: length, hash: 'SHA256')
|
|
1239
|
+
end
|
|
1240
|
+
|
|
1241
|
+
x.report('OpenSSL HKDF (SHA-384)') do
|
|
1242
|
+
OpenSSL::KDF.hkdf(ikm, salt: salt, info: info, length: length, hash: 'SHA384')
|
|
1243
|
+
end
|
|
1244
|
+
end
|
|
1245
|
+
|
|
1246
|
+
x.compare!
|
|
1247
|
+
end
|
|
1248
|
+
end
|
|
1249
|
+
|
|
1250
|
+
nil
|
|
1251
|
+
end
|
|
1252
|
+
|
|
1253
|
+
# Benchmarks Iodine's TOTP implementation vs ROTP gem.
|
|
1254
|
+
#
|
|
1255
|
+
# Tests TOTP generation and verification.
|
|
1256
|
+
#
|
|
1257
|
+
# Optional dependencies: rotp gem for comparison.
|
|
1258
|
+
#
|
|
1259
|
+
# FAIRNESS NOTES:
|
|
1260
|
+
# - Both implementations use the SAME underlying secret bytes. Iodine
|
|
1261
|
+
# accepts raw bytes; ROTP accepts Base32-encoded strings. The Base32
|
|
1262
|
+
# encoding of the raw secret is decoded by ROTP back to the same bytes,
|
|
1263
|
+
# so both implementations compute TOTP for the same secret.
|
|
1264
|
+
# - Verification windows are matched: Iodine window:1 checks ±1 interval;
|
|
1265
|
+
# ROTP drift_behind/drift_ahead:30 also checks ±1 interval (30 seconds).
|
|
1266
|
+
def self.crypto_totp
|
|
1267
|
+
require 'benchmark/ips'
|
|
1268
|
+
begin
|
|
1269
|
+
require 'rotp'
|
|
1270
|
+
rescue LoadError
|
|
1271
|
+
nil
|
|
1272
|
+
end
|
|
1273
|
+
|
|
1274
|
+
puts '=' * 70
|
|
1275
|
+
puts 'TOTP (Time-based One-Time Password) Benchmarks'
|
|
1276
|
+
puts '=' * 70
|
|
1277
|
+
|
|
1278
|
+
# Generate a raw 20-byte secret (the canonical TOTP secret).
|
|
1279
|
+
# Iodine::Utils.totp expects raw bytes.
|
|
1280
|
+
# ROTP expects a Base32-encoded string — we use the Base32 encoding of
|
|
1281
|
+
# the same raw bytes so both implementations operate on the same secret.
|
|
1282
|
+
raw_secret = Iodine::Utils.random(20)
|
|
1283
|
+
|
|
1284
|
+
# totp_secret returns a Base32-encoded string from random bytes.
|
|
1285
|
+
# We need the Base32 encoding of our specific raw_secret for ROTP.
|
|
1286
|
+
# Use Iodine's own Base32 encoder via totp_secret if possible, otherwise
|
|
1287
|
+
# fall back to ROTP's Base32 encoder.
|
|
1288
|
+
rotp_available = defined?(ROTP)
|
|
1289
|
+
|
|
1290
|
+
if rotp_available
|
|
1291
|
+
# Encode raw_secret to Base32 for ROTP
|
|
1292
|
+
base32_secret = ROTP::Base32.encode(raw_secret)
|
|
1293
|
+
rotp_totp = ROTP::TOTP.new(base32_secret)
|
|
1294
|
+
end
|
|
1295
|
+
|
|
1296
|
+
puts "\nTOTP Secret Generation:"
|
|
1297
|
+
::Benchmark.ips do |x|
|
|
1298
|
+
x.report('Iodine totp_secret') do
|
|
1299
|
+
Iodine::Utils.totp_secret(len: 20)
|
|
1300
|
+
end
|
|
1301
|
+
|
|
1302
|
+
if rotp_available
|
|
1303
|
+
x.report('ROTP random_base32') do
|
|
1304
|
+
ROTP::Base32.random_base32(32)
|
|
1305
|
+
end
|
|
1306
|
+
end
|
|
1307
|
+
|
|
1308
|
+
x.compare!
|
|
1309
|
+
end
|
|
1310
|
+
|
|
1311
|
+
puts "\nTOTP Code Generation (same secret, same time window):"
|
|
1312
|
+
::Benchmark.ips do |x|
|
|
1313
|
+
x.report('Iodine totp') do
|
|
1314
|
+
Iodine::Utils.totp(secret: raw_secret)
|
|
1315
|
+
end
|
|
1316
|
+
|
|
1317
|
+
if rotp_available
|
|
1318
|
+
x.report('ROTP now') do
|
|
1319
|
+
rotp_totp.now
|
|
1320
|
+
end
|
|
1321
|
+
end
|
|
1322
|
+
|
|
1323
|
+
x.compare!
|
|
1324
|
+
end
|
|
1325
|
+
|
|
1326
|
+
# Verification: use the same window size for both.
|
|
1327
|
+
# Iodine window:1 = check current ± 1 interval (3 codes total).
|
|
1328
|
+
# ROTP drift_behind:30, drift_ahead:30 = check current ± 1 interval (3 codes total).
|
|
1329
|
+
current_code = Iodine::Utils.totp(secret: raw_secret).to_i
|
|
1330
|
+
|
|
1331
|
+
puts "\nTOTP Verification (window: ±1 interval = 3 codes checked):"
|
|
1332
|
+
::Benchmark.ips do |x|
|
|
1333
|
+
x.report('Iodine totp_verify') do
|
|
1334
|
+
Iodine::Utils.totp_verify(code: current_code, secret: raw_secret, window: 1)
|
|
1335
|
+
end
|
|
1336
|
+
|
|
1337
|
+
if rotp_available
|
|
1338
|
+
rotp_code = rotp_totp.now.to_s
|
|
1339
|
+
x.report('ROTP verify') do
|
|
1340
|
+
rotp_totp.verify(rotp_code, drift_behind: 30, drift_ahead: 30)
|
|
1341
|
+
end
|
|
1342
|
+
end
|
|
1343
|
+
|
|
1344
|
+
x.compare!
|
|
1345
|
+
end
|
|
1346
|
+
|
|
1347
|
+
nil
|
|
1348
|
+
end
|
|
1349
|
+
|
|
1350
|
+
# Benchmarks Iodine's compression vs Ruby's Zlib stdlib.
|
|
1351
|
+
#
|
|
1352
|
+
# Tests Deflate, Gzip, and Brotli compression/decompression with
|
|
1353
|
+
# various payload sizes and compressible content.
|
|
1354
|
+
#
|
|
1355
|
+
# FAIRNESS NOTES:
|
|
1356
|
+
# - Both Iodine and Zlib use the same compression level (6 = default).
|
|
1357
|
+
# - Payloads are compressible text (repeated English prose), not random
|
|
1358
|
+
# bytes. Random bytes are incompressible and would make compression
|
|
1359
|
+
# benchmarks meaningless.
|
|
1360
|
+
# - Brotli has no Ruby stdlib equivalent; it is shown solo with a note.
|
|
1361
|
+
# - Zlib::Deflate.deflate and Zlib::GzipWriter are the idiomatic one-shot
|
|
1362
|
+
# Ruby APIs and are used here.
|
|
1363
|
+
def self.compression
|
|
1364
|
+
require 'benchmark/ips'
|
|
1365
|
+
require 'zlib'
|
|
1366
|
+
|
|
1367
|
+
puts '=' * 70
|
|
1368
|
+
puts 'Compression Benchmarks'
|
|
1369
|
+
puts '=' * 70
|
|
1370
|
+
puts
|
|
1371
|
+
puts 'NOTE: Payloads are compressible English text (not random bytes).'
|
|
1372
|
+
puts 'Both Iodine and Zlib use compression level 6 (default).'
|
|
1373
|
+
puts
|
|
1374
|
+
|
|
1375
|
+
# Compressible payload: repeated English prose
|
|
1376
|
+
prose = "The quick brown fox jumps over the lazy dog. " \
|
|
1377
|
+
"Pack my box with five dozen liquor jugs. " \
|
|
1378
|
+
"How vexingly quick daft zebras jump! " \
|
|
1379
|
+
"The five boxing wizards jump quickly. "
|
|
1380
|
+
|
|
1381
|
+
sizes = {
|
|
1382
|
+
'1KB' => 1024,
|
|
1383
|
+
'16KB' => 16_384,
|
|
1384
|
+
'64KB' => 65_536
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
# Label width — wide enough for 'Iodine Brotli.compress (no Zlib equiv)'
|
|
1388
|
+
lw = 30
|
|
1389
|
+
|
|
1390
|
+
sizes.each do |size_name, size|
|
|
1391
|
+
# Build a compressible payload of the target size
|
|
1392
|
+
plaintext = (prose * ((size / prose.bytesize) + 1))[0, size]
|
|
1393
|
+
plaintext.force_encoding(Encoding::BINARY)
|
|
1394
|
+
|
|
1395
|
+
puts "\n--- Payload Size: #{size_name} (compressible text) ---\n\n"
|
|
1396
|
+
|
|
1397
|
+
# ------------------------------------------------------------------
|
|
1398
|
+
# Pre-compute all compressed inputs outside the hot loop so that
|
|
1399
|
+
# decompression benchmarks only measure decompression, not compression.
|
|
1400
|
+
# ------------------------------------------------------------------
|
|
1401
|
+
iodine_deflated = Iodine::Base::Compression::Deflate.compress(plaintext, level: 6)
|
|
1402
|
+
zlib_deflated = Zlib::Deflate.deflate(plaintext, Zlib::DEFAULT_COMPRESSION)
|
|
1403
|
+
|
|
1404
|
+
iodine_gzipped = Iodine::Base::Compression::Gzip.compress(plaintext, level: 6)
|
|
1405
|
+
zlib_gzipped = begin
|
|
1406
|
+
sio = StringIO.new(String.new, 'wb')
|
|
1407
|
+
gz = Zlib::GzipWriter.new(sio, Zlib::DEFAULT_COMPRESSION)
|
|
1408
|
+
gz.write(plaintext)
|
|
1409
|
+
gz.close
|
|
1410
|
+
sio.string
|
|
1411
|
+
end
|
|
1412
|
+
|
|
1413
|
+
iodine_brotli = Iodine::Base::Compression::Brotli.compress(plaintext, quality: 4)
|
|
1414
|
+
|
|
1415
|
+
# ------------------------------------------------------------------
|
|
1416
|
+
# Unified compress block — all algorithms compete side-by-side.
|
|
1417
|
+
# Brotli has no Zlib equivalent but is included for ranking context.
|
|
1418
|
+
# x.compare! prints a ranked table so the fastest algorithm is clear.
|
|
1419
|
+
# ------------------------------------------------------------------
|
|
1420
|
+
puts "Compress — all algorithms ranked (level 6 / quality 4):"
|
|
1421
|
+
puts " (Brotli has no Zlib equivalent — included for ranking context)"
|
|
1422
|
+
::Benchmark.ips do |x|
|
|
1423
|
+
x.report('Iodine Deflate.compress'.ljust(lw)) do
|
|
1424
|
+
Iodine::Base::Compression::Deflate.compress(plaintext, level: 6)
|
|
1425
|
+
end
|
|
1426
|
+
x.report('Zlib::Deflate.deflate'.ljust(lw)) do
|
|
1427
|
+
Zlib::Deflate.deflate(plaintext, Zlib::DEFAULT_COMPRESSION)
|
|
1428
|
+
end
|
|
1429
|
+
x.report('Iodine Gzip.compress'.ljust(lw)) do
|
|
1430
|
+
Iodine::Base::Compression::Gzip.compress(plaintext, level: 6)
|
|
1431
|
+
end
|
|
1432
|
+
x.report('Zlib gzip (StringIO)'.ljust(lw)) do
|
|
1433
|
+
sio = StringIO.new(String.new, 'wb')
|
|
1434
|
+
gz = Zlib::GzipWriter.new(sio, Zlib::DEFAULT_COMPRESSION)
|
|
1435
|
+
gz.write(plaintext)
|
|
1436
|
+
gz.close
|
|
1437
|
+
sio.string
|
|
1438
|
+
end
|
|
1439
|
+
# Brotli has no Zlib equivalent — included for throughput ranking context
|
|
1440
|
+
x.report('Iodine Brotli.compress (no Zlib equiv)'.ljust(lw)) do
|
|
1441
|
+
Iodine::Base::Compression::Brotli.compress(plaintext, quality: 4)
|
|
1442
|
+
end
|
|
1443
|
+
x.compare!
|
|
1444
|
+
end
|
|
1445
|
+
|
|
1446
|
+
# ------------------------------------------------------------------
|
|
1447
|
+
# Unified decompress block — all algorithms compete side-by-side.
|
|
1448
|
+
# Each implementation decompresses its own format (fair comparison).
|
|
1449
|
+
# Brotli has no Zlib equivalent but is included for ranking context.
|
|
1450
|
+
# ------------------------------------------------------------------
|
|
1451
|
+
puts "\nDecompress — all algorithms ranked:"
|
|
1452
|
+
puts " (Brotli has no Zlib equivalent — included for ranking context)"
|
|
1453
|
+
::Benchmark.ips do |x|
|
|
1454
|
+
x.report('Iodine Deflate.decompress'.ljust(lw)) do
|
|
1455
|
+
Iodine::Base::Compression::Deflate.decompress(iodine_deflated)
|
|
1456
|
+
end
|
|
1457
|
+
x.report('Zlib::Inflate.inflate'.ljust(lw)) do
|
|
1458
|
+
Zlib::Inflate.inflate(zlib_deflated)
|
|
1459
|
+
end
|
|
1460
|
+
x.report('Iodine Gzip.decompress'.ljust(lw)) do
|
|
1461
|
+
Iodine::Base::Compression::Gzip.decompress(iodine_gzipped)
|
|
1462
|
+
end
|
|
1463
|
+
x.report('Zlib::GzipReader'.ljust(lw)) do
|
|
1464
|
+
Zlib::GzipReader.new(StringIO.new(zlib_gzipped)).read
|
|
1465
|
+
end
|
|
1466
|
+
# Brotli has no Zlib equivalent — included for throughput ranking context
|
|
1467
|
+
x.report('Iodine Brotli.decompress (no Zlib equiv)'.ljust(lw)) do
|
|
1468
|
+
Iodine::Base::Compression::Brotli.decompress(iodine_brotli)
|
|
1469
|
+
end
|
|
1470
|
+
x.compare!
|
|
1471
|
+
end
|
|
1472
|
+
end
|
|
1473
|
+
|
|
1474
|
+
nil
|
|
1475
|
+
end
|
|
1476
|
+
|
|
1477
|
+
# Benchmarks Iodine's non-cryptographic hash functions.
|
|
1478
|
+
#
|
|
1479
|
+
# Tests risky_hash (64-bit), risky256, risky512, and crc32 against
|
|
1480
|
+
# Ruby stdlib equivalents where available.
|
|
1481
|
+
#
|
|
1482
|
+
# FAIRNESS NOTES:
|
|
1483
|
+
# - risky_hash and String#hash serve the same purpose: seeding hash maps.
|
|
1484
|
+
# Ruby's Hash tables use String#hash (SipHash, seeded per-process).
|
|
1485
|
+
# Iodine's MiniMap uses risky_hash — a faster alternative to SipHash with
|
|
1486
|
+
# proven collision resistance and superior bit distribution.
|
|
1487
|
+
# - crc32 is compared against Zlib.crc32 (same polynomial: ITU-T V.42).
|
|
1488
|
+
# - risky256/risky512 have no stdlib equivalents; shown for reference only.
|
|
1489
|
+
def self.non_crypto_hashing
|
|
1490
|
+
require 'benchmark/ips'
|
|
1491
|
+
require 'zlib'
|
|
1492
|
+
|
|
1493
|
+
puts '=' * 70
|
|
1494
|
+
puts 'Non-Cryptographic Hash Benchmarks'
|
|
1495
|
+
puts '=' * 70
|
|
1496
|
+
puts
|
|
1497
|
+
puts 'NOTE: risky_hash and String#hash both seed hash maps — a direct'
|
|
1498
|
+
puts 'comparison. Ruby Hash uses String#hash (SipHash, per-process seed).'
|
|
1499
|
+
puts 'Iodine MiniMap uses risky_hash: faster than SipHash with proven'
|
|
1500
|
+
puts 'collision resistance and superior bit distribution.'
|
|
1501
|
+
puts
|
|
1502
|
+
|
|
1503
|
+
sizes = {
|
|
1504
|
+
'64B' => 64,
|
|
1505
|
+
'1KB' => 1024,
|
|
1506
|
+
'64KB' => 65_536
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
# Label width — wide enough for 'Iodine risky512 (non-crypto, 64B)'
|
|
1510
|
+
lw = 32
|
|
1511
|
+
|
|
1512
|
+
sizes.each do |size_name, size|
|
|
1513
|
+
data = Iodine::Utils.random(size)
|
|
1514
|
+
|
|
1515
|
+
puts "\n--- Data Size: #{size_name} — all non-crypto hashes ranked together ---\n\n"
|
|
1516
|
+
puts " (risky256/risky512 have no stdlib equivalent; String#hash uses SipHash — same use case as risky_hash)"
|
|
1517
|
+
puts
|
|
1518
|
+
|
|
1519
|
+
# ------------------------------------------------------------------
|
|
1520
|
+
# Unified block: all non-crypto hash algorithms compete side-by-side.
|
|
1521
|
+
# x.compare! prints a ranked table so the fastest algorithm is clear.
|
|
1522
|
+
# ------------------------------------------------------------------
|
|
1523
|
+
::Benchmark.ips do |x|
|
|
1524
|
+
x.report('Iodine risky_hash (64-bit)'.ljust(lw)) do
|
|
1525
|
+
Iodine::Utils.risky_hash(data)
|
|
1526
|
+
end
|
|
1527
|
+
x.report('Iodine crc32 (32-bit)'.ljust(lw)) do
|
|
1528
|
+
Iodine::Utils.crc32(data)
|
|
1529
|
+
end
|
|
1530
|
+
# risky256/risky512 have no stdlib equivalent — included for ranking context
|
|
1531
|
+
x.report('Iodine risky256 (non-crypto, 32B)'.ljust(lw)) do
|
|
1532
|
+
Iodine::Utils.risky256(data)
|
|
1533
|
+
end
|
|
1534
|
+
x.report('Iodine risky512 (non-crypto, 64B)'.ljust(lw)) do
|
|
1535
|
+
Iodine::Utils.risky512(data)
|
|
1536
|
+
end
|
|
1537
|
+
# Zlib.crc32 — direct equivalent for crc32 (same ITU-T V.42 polynomial)
|
|
1538
|
+
x.report('Zlib.crc32'.ljust(lw)) do
|
|
1539
|
+
Zlib.crc32(data)
|
|
1540
|
+
end
|
|
1541
|
+
# String#hash uses SipHash (seeded per-process) — same use case as
|
|
1542
|
+
# risky_hash: both are hash-map hash functions. Direct comparison.
|
|
1543
|
+
x.report('String#hash (SipHash, per-process)'.ljust(lw)) do
|
|
1544
|
+
data.hash
|
|
1545
|
+
end
|
|
1546
|
+
x.compare!
|
|
1547
|
+
end
|
|
1548
|
+
end
|
|
1549
|
+
|
|
1550
|
+
nil
|
|
1551
|
+
end
|
|
1552
|
+
|
|
1553
|
+
# Benchmarks Iodine's X25519+ML-KEM-768 post-quantum hybrid KEM.
|
|
1554
|
+
#
|
|
1555
|
+
# X25519MLKEM768 is a hybrid Key Encapsulation Mechanism that combines:
|
|
1556
|
+
# - Classical X25519 elliptic curve Diffie-Hellman
|
|
1557
|
+
# - ML-KEM-768 (formerly Kyber), a post-quantum lattice-based KEM
|
|
1558
|
+
#
|
|
1559
|
+
# The hybrid construction provides security against both classical and
|
|
1560
|
+
# quantum computer attacks. Even if one algorithm is broken, the other
|
|
1561
|
+
# still provides security.
|
|
1562
|
+
#
|
|
1563
|
+
# Key sizes:
|
|
1564
|
+
# - Secret key: 2432 bytes | Public key: 1216 bytes
|
|
1565
|
+
# - Ciphertext: 1120 bytes | Shared secret: 64 bytes
|
|
1566
|
+
#
|
|
1567
|
+
# There is no Ruby stdlib or OpenSSL equivalent for ML-KEM-768.
|
|
1568
|
+
# X25519.keypair is included for relative cost comparison only.
|
|
1569
|
+
#
|
|
1570
|
+
# FAIRNESS NOTES:
|
|
1571
|
+
# - Keypairs are generated OUTSIDE the encapsulate/decapsulate hot loops.
|
|
1572
|
+
# - X25519.keypair is shown alongside X25519MLKEM768.keypair so readers
|
|
1573
|
+
# can see the overhead of adding ML-KEM-768 to a classical X25519 key.
|
|
1574
|
+
# - No compare! for solo benchmarks (no stdlib equivalent).
|
|
1575
|
+
def self.crypto_pqkem
|
|
1576
|
+
require 'benchmark/ips'
|
|
1577
|
+
|
|
1578
|
+
puts '=' * 70
|
|
1579
|
+
puts 'Post-Quantum Hybrid KEM Benchmarks (X25519 + ML-KEM-768)'
|
|
1580
|
+
puts '=' * 70
|
|
1581
|
+
puts
|
|
1582
|
+
puts 'NOTE: X25519MLKEM768 is a hybrid KEM combining classical X25519 with'
|
|
1583
|
+
puts 'ML-KEM-768 (Kyber). No Ruby stdlib or OpenSSL equivalent exists.'
|
|
1584
|
+
puts 'X25519.keypair is shown for relative cost comparison only.'
|
|
1585
|
+
puts
|
|
1586
|
+
|
|
1587
|
+
# ------------------------------------------------------------------
|
|
1588
|
+
# Key generation — compare X25519MLKEM768 vs plain X25519
|
|
1589
|
+
# ------------------------------------------------------------------
|
|
1590
|
+
puts 'Key Generation (X25519MLKEM768 vs X25519 for relative cost):'
|
|
1591
|
+
::Benchmark.ips do |x|
|
|
1592
|
+
x.report('Iodine X25519MLKEM768.keypair') do
|
|
1593
|
+
Iodine::Base::Crypto::X25519MLKEM768.keypair
|
|
1594
|
+
end
|
|
1595
|
+
|
|
1596
|
+
# X25519 keypair shown for relative cost reference only.
|
|
1597
|
+
# X25519MLKEM768 is expected to be significantly slower due to ML-KEM-768.
|
|
1598
|
+
x.report('Iodine X25519.keypair (classical, for reference)') do
|
|
1599
|
+
Iodine::Base::Crypto::X25519.keypair
|
|
1600
|
+
end
|
|
1601
|
+
|
|
1602
|
+
x.compare!
|
|
1603
|
+
end
|
|
1604
|
+
|
|
1605
|
+
# Generate a keypair outside the hot loop for encapsulate/decapsulate
|
|
1606
|
+
pqkem_sk, pqkem_pk = Iodine::Base::Crypto::X25519MLKEM768.keypair
|
|
1607
|
+
|
|
1608
|
+
# ------------------------------------------------------------------
|
|
1609
|
+
# Encapsulation — sender generates shared secret + data
|
|
1610
|
+
#
|
|
1611
|
+
# No stdlib equivalent; shown for throughput reference only.
|
|
1612
|
+
# ------------------------------------------------------------------
|
|
1613
|
+
puts "\nEncapsulation (sender generates shared secret + 1120-byte data):"
|
|
1614
|
+
puts " [No stdlib equivalent — shown for throughput reference only]"
|
|
1615
|
+
::Benchmark.ips do |x|
|
|
1616
|
+
x.report('Iodine X25519MLKEM768.encapsulate') do
|
|
1617
|
+
Iodine::Base::Crypto::X25519MLKEM768.encapsulate(public_key: pqkem_pk)
|
|
1618
|
+
end
|
|
1619
|
+
# No compare! — no stdlib equivalent
|
|
1620
|
+
end
|
|
1621
|
+
|
|
1622
|
+
# Generate encapsulated data outside the hot loop for decapsulate
|
|
1623
|
+
pqkem_ct, = Iodine::Base::Crypto::X25519MLKEM768.encapsulate(public_key: pqkem_pk)
|
|
1624
|
+
|
|
1625
|
+
# ------------------------------------------------------------------
|
|
1626
|
+
# Decapsulation — recipient recovers shared secret from data
|
|
1627
|
+
#
|
|
1628
|
+
# No stdlib equivalent; shown for throughput reference only.
|
|
1629
|
+
# ------------------------------------------------------------------
|
|
1630
|
+
puts "\nDecapsulation (recipient recovers 64-byte shared secret from data):"
|
|
1631
|
+
puts " [No stdlib equivalent — shown for throughput reference only]"
|
|
1632
|
+
::Benchmark.ips do |x|
|
|
1633
|
+
x.report('Iodine X25519MLKEM768.decapsulate') do
|
|
1634
|
+
Iodine::Base::Crypto::X25519MLKEM768.decapsulate(data: pqkem_ct, secret_key: pqkem_sk)
|
|
1635
|
+
end
|
|
1636
|
+
# No compare! — no stdlib equivalent
|
|
1637
|
+
end
|
|
1638
|
+
|
|
1639
|
+
nil
|
|
1640
|
+
end
|
|
1641
|
+
|
|
1642
|
+
# Runs all crypto benchmarks.
|
|
1643
|
+
#
|
|
1644
|
+
# This is a convenience method that runs all crypto-related benchmarks:
|
|
1645
|
+
# - hmac (keyed MACs)
|
|
1646
|
+
# - random (UUIDs, PRNG and secure random bytes)
|
|
1647
|
+
# - crypto_symmetric (AEAD ciphers)
|
|
1648
|
+
# - crypto_asymmetric (Ed25519, X25519)
|
|
1649
|
+
# - hashing (SHA-2, SHA-3, BLAKE2)
|
|
1650
|
+
# - crypto_ecies (public-key encryption)
|
|
1651
|
+
# - crypto_kdf (HKDF key derivation)
|
|
1652
|
+
# - crypto_totp (TOTP generation/verification)
|
|
1653
|
+
# - compression (Deflate, Gzip, Brotli)
|
|
1654
|
+
# - non_crypto_hashing (risky_hash, crc32, risky256, risky512)
|
|
1655
|
+
# - crypto_pqkem (X25519+ML-KEM-768 post-quantum hybrid KEM)
|
|
1656
|
+
def self.crypto_all
|
|
1657
|
+
puts "\n" + '=' * 70
|
|
1658
|
+
puts 'Running All Crypto Benchmarks'
|
|
1659
|
+
puts '=' * 70 + "\n"
|
|
1660
|
+
|
|
1661
|
+
hmac
|
|
1662
|
+
puts "\n"
|
|
1663
|
+
random
|
|
1664
|
+
puts "\n"
|
|
1665
|
+
crypto_symmetric
|
|
1666
|
+
puts "\n"
|
|
1667
|
+
crypto_asymmetric
|
|
1668
|
+
puts "\n"
|
|
1669
|
+
hashing
|
|
1670
|
+
puts "\n"
|
|
1671
|
+
crypto_ecies
|
|
1672
|
+
puts "\n"
|
|
1673
|
+
crypto_kdf
|
|
1674
|
+
puts "\n"
|
|
1675
|
+
crypto_totp
|
|
1676
|
+
puts "\n"
|
|
1677
|
+
compression
|
|
1678
|
+
puts "\n"
|
|
1679
|
+
non_crypto_hashing
|
|
1680
|
+
puts "\n"
|
|
1681
|
+
crypto_pqkem
|
|
1682
|
+
|
|
1683
|
+
puts "\n" + '=' * 70
|
|
1684
|
+
puts 'All Crypto Benchmarks Complete'
|
|
1685
|
+
puts '=' * 70
|
|
1686
|
+
|
|
1687
|
+
nil
|
|
1688
|
+
end
|
|
1689
|
+
end
|
|
1690
|
+
end
|