epithet 0.1.0 → 1.1.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/CHANGELOG.md +27 -0
- data/README.md +3 -1
- data/SECURITY.md +35 -23
- data/examples/basic.rb +2 -0
- data/lib/epithet/version.rb +4 -2
- data/lib/epithet.rb +130 -60
- metadata +18 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 100d3ac2b640a77419d9d6f45228ed67251285ed6f0bf390b9249573d20db887
|
|
4
|
+
data.tar.gz: 5350064afb6799feec528888c475bcc0d8e403272f13332d00bdcd43b7bcd47c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 77e5c3acde924854471a36ede3d43e010fcf4f2bd7f4a49b75c753e7c80e4c0651355c0c33ec28a9d1c229abf14fd5da61b40c1eae2a23cbd31f8c8ef94cd9a5
|
|
7
|
+
data.tar.gz: 21a8c95376177e30642bed7aefd1b1f87ccd5f0a00d489592c154ed1ecf86cdf076df57f04f98d01ad802b4ea1c62367b5afcc0fdd165501f95fe081ac10efd9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.0 - 2026-07-14
|
|
4
|
+
|
|
5
|
+
- Freeze config strings upon object initialization
|
|
6
|
+
- Merge custom scrypt params
|
|
7
|
+
- Block58 now defaults to a generic s2i that handles any block size
|
|
8
|
+
- Optimised 16-byte unrolled s2i selected via `Block58::build`
|
|
9
|
+
- Recognize unprefixed decodes by payload length
|
|
10
|
+
- Fix github CI warnings
|
|
11
|
+
- Write notes on salt & improve examples
|
|
12
|
+
|
|
13
|
+
## 1.0.0 - 2026-07-14
|
|
14
|
+
|
|
15
|
+
### Breaking changes
|
|
16
|
+
|
|
17
|
+
- Treat out-of-range base58 strings as invalid instead of wrapping them.
|
|
18
|
+
- Reject separators that share codepoints with the encoding alphabet.
|
|
19
|
+
- Drop support for Rubies < 3.3.
|
|
20
|
+
|
|
21
|
+
### Other changes
|
|
22
|
+
|
|
23
|
+
- Code style nitpicks.
|
|
24
|
+
- Support for 32-bit Rubies.
|
|
25
|
+
- Support for Ruby 4.0.
|
|
26
|
+
- Documentation improvements.
|
|
27
|
+
- A custom alphabet may be configured via `Epithet::Config`.
|
|
28
|
+
- Custom alphabets must be strictly ascending byte codepoints.
|
|
29
|
+
|
|
3
30
|
## 0.1.0 - 2025-12-22
|
|
4
31
|
- Initial release.
|
data/README.md
CHANGED
|
@@ -21,12 +21,14 @@ gem install epithet
|
|
|
21
21
|
|
|
22
22
|
## Usage
|
|
23
23
|
|
|
24
|
+
With `EPITHET_PASSPHRASE="example only"`:
|
|
25
|
+
|
|
24
26
|
```ruby
|
|
25
27
|
require 'epithet'
|
|
26
28
|
|
|
27
29
|
def epithet_initialize
|
|
28
30
|
Epithet.configure(
|
|
29
|
-
passphrase: ENV.fetch('EPITHET_PASSPHRASE')
|
|
31
|
+
passphrase: ENV.fetch('EPITHET_PASSPHRASE'),
|
|
30
32
|
salt: 'v1'
|
|
31
33
|
)
|
|
32
34
|
end
|
data/SECURITY.md
CHANGED
|
@@ -2,40 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
## Cryptographic considerations
|
|
4
4
|
|
|
5
|
-
The primary construction is `AES-256-ECB(id(8B) + HMAC-SHA256(id)[0,7])` with the result
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
This library is intended for high-performance obfuscation of integer sequences, deflection
|
|
11
|
-
|
|
12
|
-
it uses standard cryptographic primitives to do so, the design trade-off of
|
|
13
|
-
format means it is not intended to defeat nation-state security services, talented
|
|
5
|
+
The primary construction is `AES-256-ECB(id(8B) + HMAC-SHA256(id)[0,7])` with the result base58
|
|
6
|
+
encoded for transmission and a contextual prefix prepended. Subkeys for AES and HMAC are by default
|
|
7
|
+
derived with HKDF using an internal key generator that takes IKM from a passphrase via scrypt,
|
|
8
|
+
salting generated keys by prefix and purpose.
|
|
9
|
+
|
|
10
|
+
This library is intended for high-performance obfuscation of integer sequences, deflection of casual
|
|
11
|
+
tampering, and conversion to a compact, stable wire parameter format that is hard to guess and hard
|
|
12
|
+
to predict. Although it uses standard cryptographic primitives to do so, the design trade-off of
|
|
13
|
+
the compact format means it is not intended to defeat nation-state security services, talented
|
|
14
14
|
cryptographers, or even a well-resourced enterprise.
|
|
15
15
|
|
|
16
|
-
The identifiers produced are intentionally deterministic i.e. replayable and reusable. For
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
in the usual manner.
|
|
16
|
+
The identifiers produced are intentionally deterministic i.e. replayable and reusable. For privacy,
|
|
17
|
+
confidentiality, and authentication purposes they should therefore be considered equivalent to the
|
|
18
|
+
plaintext integer they represent, and those concerns must still be addressed in the usual manner.
|
|
20
19
|
|
|
21
20
|
The tamper detection is necessarily probabilistic, because the MAC is truncated.
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
(e.g. chacha20) or block ciphers in streaming modes (e.g. aes-256-ctr) must not be used;
|
|
26
|
-
no nonce/IV value is included in construction, making them trivially vulnerable to
|
|
27
|
-
known-plaintext attacks. These, CBC/OCB, and other IV/nonce modes may also be rejected
|
|
28
|
-
by Epithet's guardrails.
|
|
22
|
+
Encodings are canonical, producing exactly one string per id, and Epithet will reject attempts to
|
|
23
|
+
decode a value exceeding the 128-bit block.
|
|
29
24
|
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
If configuring alternative cipher algorithms, note that only 128-bit block ciphers that function
|
|
26
|
+
without IV/nonce requirements are accepted. Streaming ciphers (e.g. chacha20) or block ciphers in
|
|
27
|
+
streaming modes (e.g. aes-256-ctr) must not be used; no nonce/IV value is included in construction,
|
|
28
|
+
making them trivially vulnerable to known-plaintext attacks. These, CBC/OCB, and other IV/nonce
|
|
29
|
+
modes may also be rejected by Epithet's guardrails.
|
|
30
|
+
|
|
31
|
+
If configuring alternative digest algorithms, note that any algorithm may be accepted whenever they
|
|
32
|
+
it produces at least 64 bits of output. HMAC does not rest on collision resistance, so even dated
|
|
33
|
+
digests are not trivially forgeable here, but algorithms other than the defaults step outside the
|
|
34
|
+
supported security profile. If you must stray, stay within the SHA-2 family.
|
|
35
|
+
|
|
36
|
+
A weak, guessable, or disclosed passphrase will compromise the obfuscation and tamper-detection
|
|
37
|
+
properties.
|
|
32
38
|
|
|
33
39
|
Use Epithet at your own risk.
|
|
34
40
|
|
|
41
|
+
## On salt
|
|
42
|
+
|
|
43
|
+
Epithet uses salt in two ways. Firstly, as part of a scrypt operation to turn the configured
|
|
44
|
+
passphrase into initial keying material. Secondly, to supply an additional affordance to separate
|
|
45
|
+
derived subkeys by some application-specific division such as purpose or rotation epoch. Epithet
|
|
46
|
+
does not store or verify passwords; both uses of salt are non-secret configuration and may safely be
|
|
47
|
+
committed to source control.
|
|
48
|
+
|
|
35
49
|
## Vulnerabilities
|
|
36
50
|
|
|
37
51
|
If you think you've found a vulnerability in Epithet that compromises its design or behaviour, please
|
|
38
52
|
[report it via a private advisory](https://github.com/inopinatus/epithet/security/advisories/new).
|
|
39
53
|
Do not open a public issue or pull request.
|
|
40
|
-
|
|
41
|
-
|
data/examples/basic.rb
CHANGED
data/lib/epithet/version.rb
CHANGED
data/lib/epithet.rb
CHANGED
|
@@ -13,6 +13,8 @@ require 'openssl'
|
|
|
13
13
|
# Pseudo-AEAD is via `AES-256-ECB(id(8B) + HMAC-SHA256(id)[0,7])` with the result
|
|
14
14
|
# base58 encoded for transmission and the contextual prefix prepended.
|
|
15
15
|
#
|
|
16
|
+
# Encodings are canonical; a given configuration accepts exactly one string per id.
|
|
17
|
+
#
|
|
16
18
|
# Subkeys for AES and HMAC are by default derived with HKDF using an internal key
|
|
17
19
|
# generator that takes IKM from a passphrase via scrypt. An alternative key generator
|
|
18
20
|
# may be injected via Config objects. Subkeys are salted by prefix and an optional
|
|
@@ -43,14 +45,14 @@ class Epithet
|
|
|
43
45
|
#
|
|
44
46
|
def initialize(prefix, config: Epithet.defaults)
|
|
45
47
|
prefix = String(prefix)
|
|
46
|
-
key_salt = [prefix.bytesize, prefix, config.salt.bytesize, config.salt].pack(
|
|
47
|
-
@block58 = Block58.
|
|
48
|
+
key_salt = [prefix.bytesize, prefix, config.salt.bytesize, config.salt].pack('Q>Z*Q>Z*')
|
|
49
|
+
@block58 = Block58.build(16, alphabet: config.alphabet)
|
|
48
50
|
@prefix_s = "#{prefix}#{config.separator}"
|
|
49
51
|
|
|
50
52
|
cipher_key_len = OpenSSL::Cipher.new(config.cipher).key_len
|
|
51
53
|
digest_key_len = OpenSSL::Digest.new(config.digest).block_length
|
|
52
|
-
cipher_key = config.keygen.generate(
|
|
53
|
-
digest_key = config.keygen.generate(
|
|
54
|
+
cipher_key = config.keygen.generate('epithet:ecb', key_salt, cipher_key_len)
|
|
55
|
+
digest_key = config.keygen.generate('epithet:mac', key_salt, digest_key_len)
|
|
54
56
|
|
|
55
57
|
@encryptor = OpenSSL::Cipher.new(config.cipher).encrypt.tap { |c| c.key = cipher_key; c.padding = 0 }
|
|
56
58
|
@decryptor = OpenSSL::Cipher.new(config.cipher).decrypt.tap { |c| c.key = cipher_key; c.padding = 0 }
|
|
@@ -60,7 +62,7 @@ class Epithet
|
|
|
60
62
|
# Encode a 64-bit unsigned Integer to a prefixed Base58 string.
|
|
61
63
|
# Raises ArgumentError on invalid values.
|
|
62
64
|
def encode(id)
|
|
63
|
-
raise ArgumentError,
|
|
65
|
+
raise ArgumentError, 'not a 64-bit unsigned integer' unless Integer === id && id.bit_length <= 64 && id >= 0
|
|
64
66
|
|
|
65
67
|
e = @encryptor.dup
|
|
66
68
|
h = @hmac.dup
|
|
@@ -72,13 +74,14 @@ class Epithet
|
|
|
72
74
|
@prefix_s + @block58.i2s(ct)
|
|
73
75
|
end
|
|
74
76
|
|
|
75
|
-
# Decode a prefixed or raw Base58 string to an Integer.
|
|
77
|
+
# Decode a prefixed or raw Base58 string to an Integer. Raw inputs are
|
|
78
|
+
# recognised by their exact payload length.
|
|
76
79
|
#
|
|
77
80
|
# Returns the Integer on success, nil if authentication fails.
|
|
78
|
-
# Raises ArgumentError on invalid
|
|
81
|
+
# Raises ArgumentError on invalid wire format (see Block58#valid?).
|
|
79
82
|
def decode(s)
|
|
80
|
-
s = s.delete_prefix(@prefix_s)
|
|
81
|
-
raise ArgumentError,
|
|
83
|
+
s = s.delete_prefix(@prefix_s) unless s.bytesize == @block58.size
|
|
84
|
+
raise ArgumentError, 'unexpected format' unless @block58.valid?(s)
|
|
82
85
|
|
|
83
86
|
d = @decryptor.dup
|
|
84
87
|
h = @hmac.dup
|
|
@@ -102,7 +105,10 @@ class Epithet
|
|
|
102
105
|
# #### Examples
|
|
103
106
|
#
|
|
104
107
|
# # As it might appear in an initializer
|
|
105
|
-
# Epithet.configure(
|
|
108
|
+
# Epithet.configure(
|
|
109
|
+
# passphrase: ENV.fetch('EPITHET_PASSPHRASE'),
|
|
110
|
+
# scrypt: { salt: "#{MyApp.name}/#{MyApp.env}" }
|
|
111
|
+
# )
|
|
106
112
|
#
|
|
107
113
|
# # Retaining already-configured passphrase but updating salt,
|
|
108
114
|
# # and using a custom separator.
|
|
@@ -115,11 +121,13 @@ class Epithet
|
|
|
115
121
|
# #### Options
|
|
116
122
|
#
|
|
117
123
|
# * `:passphrase` - Install new key generator with scrypt-derived key material
|
|
118
|
-
# * `:scrypt` - Params for scrypt
|
|
124
|
+
# * `:scrypt` - Params for scrypt, merged over `Keygen::DEFAULT_SCRYPT_PARAMS`
|
|
119
125
|
# * `:keygen` - Install an existing key generator
|
|
120
126
|
# * `:cipher` - Must be a 128-bit block cipher in ECB mode or equivalent; omit for standard `aes-256-ecb`
|
|
121
127
|
# * `:digest` - Must be >= 64 bits; omit for standard `sha256`
|
|
122
|
-
# * `:separator` -
|
|
128
|
+
# * `:separator` - String inserted between the prefix and the generated param; omit for standard `_`.
|
|
129
|
+
# May be `nil`. Must not share bytes with the alphabet.
|
|
130
|
+
# * `:alphabet` - Alphabet for the wire encoding; must be 58 strictly ascending bytes; omit for `Block58::Alphabet`.
|
|
123
131
|
# * `:salt` - If supplied, stringified form is included in subkey derivation
|
|
124
132
|
#
|
|
125
133
|
# At minimum, one of `passphrase:` or `keygen:` is required.
|
|
@@ -149,20 +157,23 @@ class Epithet
|
|
|
149
157
|
# acct_epithet = Epithet.new('acct', config: cfg)
|
|
150
158
|
#
|
|
151
159
|
def configure(opts) = @defaults = Config === opts ? opts : Config.new(opts)
|
|
152
|
-
def defaults() = @defaults || raise(
|
|
160
|
+
def defaults() = @defaults || raise('no Epithet defaults configured')
|
|
153
161
|
end
|
|
154
162
|
|
|
155
163
|
# Class for passing around preset configs. See Epithet::configure for options.
|
|
156
164
|
class Config
|
|
157
|
-
attr_reader :keygen, :salt, :separator, :cipher, :digest # :nodoc:
|
|
165
|
+
attr_reader :keygen, :salt, :separator, :alphabet, :cipher, :digest # :nodoc:
|
|
166
|
+
|
|
158
167
|
def initialize(opts = {})
|
|
159
168
|
opts = opts.dup
|
|
160
|
-
@separator = String(opts.delete(:separator) { '_' })
|
|
161
|
-
@salt = String(opts.delete(:salt))
|
|
162
|
-
@
|
|
163
|
-
@
|
|
169
|
+
@separator = -String(opts.delete(:separator) { '_' })
|
|
170
|
+
@salt = -String(opts.delete(:salt))
|
|
171
|
+
@alphabet = -String(opts.delete(:alphabet) { Block58::Alphabet })
|
|
172
|
+
@cipher = -(opts.delete(:cipher) || 'aes-256-ecb')
|
|
173
|
+
@digest = -(opts.delete(:digest) || 'sha256')
|
|
164
174
|
|
|
165
175
|
cipher = OpenSSL::Cipher.new(@cipher)
|
|
176
|
+
raise ArgumentError, 'separator intersects alphabet' if @separator.bytes.intersect?(@alphabet.bytes)
|
|
166
177
|
raise ArgumentError, "#{@cipher} not a 128-bit block cipher" if cipher.block_size != 16
|
|
167
178
|
raise ArgumentError, "#{@cipher} requires an IV/nonce" if cipher.iv_len != 0
|
|
168
179
|
raise ArgumentError, "#{@digest} produces < 64-bit digest" if OpenSSL::Digest.new(@digest).digest_length < 8
|
|
@@ -170,8 +181,9 @@ class Epithet
|
|
|
170
181
|
@keygen = opts.delete(:keygen) || Keygen.new(
|
|
171
182
|
passphrase: opts.delete(:passphrase),
|
|
172
183
|
digest: @digest,
|
|
173
|
-
scrypt: opts.delete(:scrypt) ||
|
|
184
|
+
scrypt: opts.delete(:scrypt) || {})
|
|
174
185
|
raise ArgumentError, "unused option(s) #{opts.keys}" unless opts.empty?
|
|
186
|
+
freeze
|
|
175
187
|
end
|
|
176
188
|
end
|
|
177
189
|
|
|
@@ -182,7 +194,7 @@ class Epithet
|
|
|
182
194
|
# ```ruby
|
|
183
195
|
# DEFAULT_SCRYPT_PARAMS = {
|
|
184
196
|
# salt: 'epithet-default',
|
|
185
|
-
# N: 1<<17,
|
|
197
|
+
# N: 1 << 17,
|
|
186
198
|
# r: 8,
|
|
187
199
|
# p: 1,
|
|
188
200
|
# length: 32
|
|
@@ -190,20 +202,22 @@ class Epithet
|
|
|
190
202
|
# ```
|
|
191
203
|
DEFAULT_SCRYPT_PARAMS = {
|
|
192
204
|
salt: 'epithet-default',
|
|
193
|
-
N: 1<<17,
|
|
205
|
+
N: 1 << 17,
|
|
194
206
|
r: 8,
|
|
195
207
|
p: 1,
|
|
196
208
|
length: 32
|
|
197
209
|
}.freeze
|
|
198
210
|
|
|
199
211
|
# Create a new key generator from either high-entropy key material, or a supplied passphrase.
|
|
200
|
-
|
|
212
|
+
# Supplied scrypt params are merged over DEFAULT_SCRYPT_PARAMS.
|
|
213
|
+
def initialize(ikm: nil, passphrase: nil, digest: 'sha256', scrypt: {})
|
|
201
214
|
if (passphrase.nil? && ikm.nil?) || (!passphrase.nil? && !ikm.nil?)
|
|
202
|
-
raise ArgumentError,
|
|
215
|
+
raise ArgumentError, 'keygen requires either ikm or passphrase'
|
|
203
216
|
end
|
|
204
217
|
|
|
205
|
-
@ikm = ikm
|
|
206
|
-
@digest = digest
|
|
218
|
+
@ikm = (ikm ? ikm.b : OpenSSL::KDF.scrypt(passphrase, **DEFAULT_SCRYPT_PARAMS, **scrypt)).freeze
|
|
219
|
+
@digest = -String(digest)
|
|
220
|
+
freeze
|
|
207
221
|
end
|
|
208
222
|
|
|
209
223
|
def inspect
|
|
@@ -217,30 +231,48 @@ class Epithet
|
|
|
217
231
|
end
|
|
218
232
|
|
|
219
233
|
# Fixed-length Base58 codec for a fixed-size block.
|
|
234
|
+
#
|
|
235
|
+
# Obtain codecs via Block58::build, which selects the fastest variant for
|
|
236
|
+
# the block size, an unrolled decoder for 16-byte blocks, or the generic
|
|
237
|
+
# chunked decoder otherwise.
|
|
220
238
|
class Block58
|
|
221
|
-
# `=
|
|
222
|
-
Alphabet =
|
|
239
|
+
# `= '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'`
|
|
240
|
+
Alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
|
241
|
+
|
|
242
|
+
POW58 = Array.new(11) { 58**_1 }.freeze # :nodoc:
|
|
243
|
+
|
|
244
|
+
attr_reader :size
|
|
245
|
+
|
|
246
|
+
# Same as ::new but may select a tuned subclass for performance.
|
|
247
|
+
def self.build(block_size, ...) = (block_size == 16 ? Unrolled16 : self).new(block_size, ...)
|
|
223
248
|
|
|
224
249
|
# Create a codec for a block size in bytes.
|
|
250
|
+
#
|
|
251
|
+
# The alphabet must be 58 distinct bytes in ascending order, so that
|
|
252
|
+
# lexicographic order agrees with numeric order.
|
|
225
253
|
def initialize(block_size, alphabet: Alphabet)
|
|
254
|
+
raise ArgumentError, 'invalid block size' unless Integer === block_size && block_size > 0
|
|
226
255
|
@alphabet = alphabet.b.freeze
|
|
227
|
-
raise ArgumentError,
|
|
256
|
+
raise ArgumentError, 'invalid alphabet length' unless @alphabet.bytesize == 58
|
|
257
|
+
raise ArgumentError, 'alphabet not strictly ascending' unless @alphabet.bytes.each_cons(2).all? { _2 > _1 }
|
|
228
258
|
@size = ((block_size * 8) / Math.log2(58)).ceil(0)
|
|
229
259
|
@charsel = @alphabet.gsub(/[\^\-\\]/, '\\\\\&').freeze
|
|
230
260
|
@blank = @alphabet[0] * @size
|
|
231
261
|
@lut = @alphabet.each_byte.with_index.with_object("\0" * 256) { |(val, idx), lut| lut.setbyte(val, idx) }.freeze
|
|
262
|
+
@max = i2s((1 << (block_size * 8)) - 1).freeze
|
|
232
263
|
end
|
|
233
264
|
|
|
234
265
|
def inspect
|
|
235
266
|
"#<#{self.class}:#{'%#016x' % (object_id << 1)} size=#{@size} alphabet=#{@alphabet}>"
|
|
236
267
|
end
|
|
237
268
|
|
|
238
|
-
# Return true if the string
|
|
269
|
+
# Return true if the string is in range with the right size and alphabet.
|
|
239
270
|
def valid?(s)
|
|
240
|
-
String === s && s.bytesize == @size && s.count(@charsel) == @size
|
|
271
|
+
String === s && s.bytesize == @size && s <= @max && s.count(@charsel) == @size
|
|
241
272
|
end
|
|
242
273
|
|
|
243
274
|
# Encode a non-negative Integer to fixed-length Base58.
|
|
275
|
+
# Truncates if int >= 58**size.
|
|
244
276
|
def i2s(int)
|
|
245
277
|
# Using divmod+setbyte is faster than Integer#digits under YJIT,
|
|
246
278
|
# and about equal in plain MRI.
|
|
@@ -257,42 +289,80 @@ class Epithet
|
|
|
257
289
|
end
|
|
258
290
|
|
|
259
291
|
# Decode a fixed-length Base58 string to an Integer.
|
|
260
|
-
# Assumes the input passes `#valid?`.
|
|
292
|
+
# Assumes the input passes `#valid?`. Wraps at 58**size on the i2s round trip.
|
|
261
293
|
def s2i(str)
|
|
262
|
-
#
|
|
294
|
+
# Chunking intermediate results into 64-bit integers is ~5x faster
|
|
295
|
+
# under YJIT than Horner's scheme
|
|
263
296
|
#
|
|
264
297
|
# str.each_byte.inject(0) { _1 * 58 + @lut[_2] }
|
|
265
298
|
#
|
|
266
|
-
# at computing the inner product
|
|
267
|
-
# intermediate results into 64-bit integers.
|
|
299
|
+
# at computing the inner product.
|
|
268
300
|
lut = @lut
|
|
301
|
+
size = @size
|
|
302
|
+
pow = POW58
|
|
303
|
+
acc = 0
|
|
304
|
+
pos = 0
|
|
305
|
+
while pos < size
|
|
306
|
+
n = size - pos
|
|
307
|
+
n = 10 if n > 10
|
|
308
|
+
chunk = 0
|
|
309
|
+
i = 0
|
|
310
|
+
while i < n
|
|
311
|
+
chunk = (chunk * 58) + lut.getbyte(str.getbyte(pos))
|
|
312
|
+
pos += 1
|
|
313
|
+
i += 1
|
|
314
|
+
end
|
|
315
|
+
acc = (acc * pow[n]) + chunk
|
|
316
|
+
end
|
|
317
|
+
acc
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Specialised decoder for 16-byte blocks (22 digits) with a fully unrolled inner product.
|
|
321
|
+
class Unrolled16 < Block58
|
|
322
|
+
def initialize(...)
|
|
323
|
+
super
|
|
324
|
+
raise ArgumentError, 'unrolled codec requires a 16-byte block' unless @size == 22
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Decode a fixed-length Base58 string to an Integer.
|
|
328
|
+
# Assumes the input passes `#valid?`.
|
|
329
|
+
def s2i(str)
|
|
330
|
+
# rubocop:disable Style/NumericLiterals, Lint/AmbiguousOperatorPrecedence, Layout
|
|
331
|
+
#
|
|
332
|
+
# By unrolling the chunks against literal coefficients, this is ~1.6x
|
|
333
|
+
# faster under YJIT than the generic chunked Block58#s2i, and ~8x
|
|
334
|
+
# faster than Horner's scheme.
|
|
335
|
+
lut = @lut
|
|
269
336
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
337
|
+
acc0 = lut.getbyte(str.getbyte(0)) * 7427658739644928 +
|
|
338
|
+
lut.getbyte(str.getbyte(1)) * 128063081718016 +
|
|
339
|
+
lut.getbyte(str.getbyte(2)) * 2207984167552 +
|
|
340
|
+
lut.getbyte(str.getbyte(3)) * 38068692544 +
|
|
341
|
+
lut.getbyte(str.getbyte(4)) * 656356768 +
|
|
342
|
+
lut.getbyte(str.getbyte(5)) * 11316496 +
|
|
343
|
+
lut.getbyte(str.getbyte(6)) * 195112 +
|
|
344
|
+
lut.getbyte(str.getbyte(7)) * 3364 +
|
|
345
|
+
lut.getbyte(str.getbyte(8)) * 58 +
|
|
346
|
+
lut.getbyte(str.getbyte(9))
|
|
347
|
+
|
|
348
|
+
acc1 = lut.getbyte(str.getbyte(10)) * 7427658739644928 +
|
|
349
|
+
lut.getbyte(str.getbyte(11)) * 128063081718016 +
|
|
350
|
+
lut.getbyte(str.getbyte(12)) * 2207984167552 +
|
|
351
|
+
lut.getbyte(str.getbyte(13)) * 38068692544 +
|
|
352
|
+
lut.getbyte(str.getbyte(14)) * 656356768 +
|
|
353
|
+
lut.getbyte(str.getbyte(15)) * 11316496 +
|
|
354
|
+
lut.getbyte(str.getbyte(16)) * 195112 +
|
|
355
|
+
lut.getbyte(str.getbyte(17)) * 3364 +
|
|
356
|
+
lut.getbyte(str.getbyte(18)) * 58 +
|
|
357
|
+
lut.getbyte(str.getbyte(19))
|
|
358
|
+
|
|
359
|
+
lut.getbyte(str.getbyte(21)) +
|
|
360
|
+
58 * lut.getbyte(str.getbyte(20)) +
|
|
361
|
+
3364 * acc1 +
|
|
362
|
+
1449225352009601191936 * acc0
|
|
363
|
+
|
|
364
|
+
# rubocop:enable Style/NumericLiterals, Lint/AmbiguousOperatorPrecedence, Layout
|
|
365
|
+
end
|
|
296
366
|
end
|
|
297
367
|
end
|
|
298
368
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: epithet
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Josh Goodall
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: minitest
|
|
@@ -52,6 +51,20 @@ dependencies:
|
|
|
52
51
|
- - ">="
|
|
53
52
|
- !ruby/object:Gem::Version
|
|
54
53
|
version: '7'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rubocop
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
55
68
|
description: Epithet generates stable, prefixed, Base58 identifiers from 64-bit integers
|
|
56
69
|
using AES and HMAC.
|
|
57
70
|
email:
|
|
@@ -76,7 +89,6 @@ metadata:
|
|
|
76
89
|
changelog_uri: https://github.com/inopinatus/epithet/blob/main/CHANGELOG.md
|
|
77
90
|
bug_tracker_uri: https://github.com/inopinatus/epithet/issues
|
|
78
91
|
rubygems_mfa_required: 'true'
|
|
79
|
-
post_install_message:
|
|
80
92
|
rdoc_options: []
|
|
81
93
|
require_paths:
|
|
82
94
|
- lib
|
|
@@ -84,15 +96,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
84
96
|
requirements:
|
|
85
97
|
- - ">="
|
|
86
98
|
- !ruby/object:Gem::Version
|
|
87
|
-
version: '3'
|
|
99
|
+
version: '3.3'
|
|
88
100
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
89
101
|
requirements:
|
|
90
102
|
- - ">="
|
|
91
103
|
- !ruby/object:Gem::Version
|
|
92
104
|
version: '0'
|
|
93
105
|
requirements: []
|
|
94
|
-
rubygems_version:
|
|
95
|
-
signing_key:
|
|
106
|
+
rubygems_version: 4.0.10
|
|
96
107
|
specification_version: 4
|
|
97
108
|
summary: External base58 identifiers with reversible, authenticated obfuscation.
|
|
98
109
|
test_files: []
|