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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c8d4c22a2ee0f83df400b54761a8e5c0a03df04390c6c9cdbb486a8c907f1812
4
- data.tar.gz: a8507f28f992f524284262d498b107cfb566f9fefb66c5d97e2f29322d7a9fe4
3
+ metadata.gz: 100d3ac2b640a77419d9d6f45228ed67251285ed6f0bf390b9249573d20db887
4
+ data.tar.gz: 5350064afb6799feec528888c475bcc0d8e403272f13332d00bdcd43b7bcd47c
5
5
  SHA512:
6
- metadata.gz: bc64b08e053da076bab07709bed61f1ff0c41ebcdb59a3963f63b7846b897efa2b8a2de43330b8bb0a3d7b473d377dc676da2ef4331ebcd52d18749809b8b1c3
7
- data.tar.gz: 8495ae0d1ed045619ed752ef55cc240ff5180b6da68bf27f2cc858e6ec1a190eb93e10c5dc1e23665a363ee42448dc1df6d23f3522ab03813f6d2f48da322b44
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') { 'example only' },
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
- base58 encoded for transmission and a contextual prefix prepended. Subkeys for AES and
7
- HMAC are by default derived with HKDF using an internal key generator that takes IKM from
8
- a passphrase via scrypt, salting generated keys by prefix and purpose.
9
-
10
- This library is intended for high-performance obfuscation of integer sequences, deflection
11
- of casual tampering, and conversion to a compact, stable wire parameter format. Although
12
- it uses standard cryptographic primitives to do so, the design trade-off of the compact
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
- privacy, confidentiality, and authentication purposes they should therefore be considered
18
- equivalent to the plaintext integer they represent, and those purposes must still be addressed
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
- If configuring alternative cipher and digest algorithms, note that only 128-bit block
24
- ciphers that function without IV/nonce requirements are accepted. Streaming ciphers
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
- A weak, guessable, or disclosed passphrase will compromise the obfuscation and
31
- tamper-detection properties.
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'epithet'
2
4
 
3
5
  def epithet_initialize
@@ -1,4 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Epithet
2
- # `= "0.1.0"`
3
- VERSION = "0.1.0"
4
+ # `= '1.1.0'`
5
+ VERSION = '1.1.0'
4
6
  end
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("Q>Z*Q>Z*")
47
- @block58 = Block58.new(16)
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("epithet:ecb", key_salt, cipher_key_len)
53
- digest_key = config.keygen.generate("epithet:mac", key_salt, digest_key_len)
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, "not a 64-bit unsigned integer" unless Integer === id && id.size == 8 && id >= 0
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 formats.
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, "unexpected format" unless @block58.valid?(s)
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(passphrase: ENV.fetch('EPITHET_PASSPHRASE'))
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; omit to use `Keygen::DEFAULT_SCRYPT_PARAMS`
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` - Inserted as string between the prefix and the generated param. may be nil; omit for underscore.
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(RuntimeError, "no Epithet defaults configured")
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
- @cipher = opts.delete(:cipher) || 'aes-256-ecb'
163
- @digest = opts.delete(:digest) || 'sha256'
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) || Keygen::DEFAULT_SCRYPT_PARAMS)
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
- def initialize(ikm: nil, passphrase: nil, digest: "sha256", scrypt: DEFAULT_SCRYPT_PARAMS)
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, "keygen requires either ikm or passphrase"
215
+ raise ArgumentError, 'keygen requires either ikm or passphrase'
203
216
  end
204
217
 
205
- @ikm = ikm || OpenSSL::KDF.scrypt(passphrase, **scrypt)
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
- # `= "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"`
222
- Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
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, "invalid alphabet length" unless @alphabet.bytesize == 58
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 has the right size and alphabet.
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
- # By unrolling coefficients, this is ~8x faster than Horner's scheme
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 when using YJIT, by chunking
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
- acc0 = lut.getbyte(str.getbyte(0)) * 7427658739644928 +
271
- lut.getbyte(str.getbyte(1)) * 128063081718016 +
272
- lut.getbyte(str.getbyte(2)) * 2207984167552 +
273
- lut.getbyte(str.getbyte(3)) * 38068692544 +
274
- lut.getbyte(str.getbyte(4)) * 656356768 +
275
- lut.getbyte(str.getbyte(5)) * 11316496 +
276
- lut.getbyte(str.getbyte(6)) * 195112 +
277
- lut.getbyte(str.getbyte(7)) * 3364 +
278
- lut.getbyte(str.getbyte(8)) * 58 +
279
- lut.getbyte(str.getbyte(9))
280
-
281
- acc1 = lut.getbyte(str.getbyte(10)) * 7427658739644928 +
282
- lut.getbyte(str.getbyte(11)) * 128063081718016 +
283
- lut.getbyte(str.getbyte(12)) * 2207984167552 +
284
- lut.getbyte(str.getbyte(13)) * 38068692544 +
285
- lut.getbyte(str.getbyte(14)) * 656356768 +
286
- lut.getbyte(str.getbyte(15)) * 11316496 +
287
- lut.getbyte(str.getbyte(16)) * 195112 +
288
- lut.getbyte(str.getbyte(17)) * 3364 +
289
- lut.getbyte(str.getbyte(18)) * 58 +
290
- lut.getbyte(str.getbyte(19))
291
-
292
- lut.getbyte(str.getbyte(21)) +
293
- 58 * lut.getbyte(str.getbyte(20)) +
294
- 3364 * acc1 +
295
- 1449225352009601191936 * acc0
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: 0.1.0
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: 2025-12-22 00:00:00.000000000 Z
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: 3.0.3.1
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: []