epithet 2.0.0 → 2.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 +10 -0
- data/README.md +7 -3
- data/SECURITY.md +11 -8
- data/examples/basic.rb +3 -2
- data/lib/epithet/block58.rb +142 -0
- data/lib/epithet/config.rb +45 -0
- data/lib/epithet/keygen.rb +69 -0
- data/lib/epithet/scrypt.rb +119 -0
- data/lib/epithet/version.rb +2 -2
- data/lib/epithet.rb +17 -217
- metadata +6 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8065749fc3d0aaf89280bc7238f8dfe30ce749bf9c09c84fbec0406bde779d1b
|
|
4
|
+
data.tar.gz: 8e42aeba4a5f3bae8cec60bac6719a1214f6750de8a8b33ce009af8d7c8dd20c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6946cf232b610670fd5cdd5c1293f1d3c882a4efa36f3e1a9971ce667f9291130372f2106b83cf8bbad16673d1b13fbb5e7b1475989a2b2dc7b201fc44347562
|
|
7
|
+
data.tar.gz: 3437d52226de8303e23db85c11dd803ac1556f267b067696822f864b17d8b1ed1b314ec421074da0766e23ecd2efb926c6cb58dfefc7d7f0d1272e804d3ddcce
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.1.0 - 2026-07-23
|
|
4
|
+
|
|
5
|
+
- Add support for JRuby and TruffleRuby
|
|
6
|
+
- Support multiple scrypt providers (OpenSSL, BouncyCastle, and the `scrypt` gem)
|
|
7
|
+
- Raise `Epithet::ConfigurationError` (< `RuntimeError`) if config missing
|
|
8
|
+
- Unknown cipher/digest names now raise `ArgumentError`
|
|
9
|
+
- Internal reorganisation into separate files
|
|
10
|
+
- Robustness improvements inc. broader CI coverage & concurrency hammer
|
|
11
|
+
- Documentation edits
|
|
12
|
+
|
|
3
13
|
## 2.0.0 - 2026-07-16
|
|
4
14
|
|
|
5
15
|
### Breaking changes
|
data/README.md
CHANGED
|
@@ -45,11 +45,15 @@ user_epithet.decode(param)
|
|
|
45
45
|
# => 42
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
Configuration once at initialisation is recommended, because deriving key material from the passphrase uses scrypt, and is consequently expensive. The `context:` is optional; this parameter is included when deriving the subkey material for obfuscating and tamper resistance, and
|
|
48
|
+
Configuration once at initialisation is recommended, because deriving key material from the passphrase uses scrypt, and is consequently expensive. The `context:` is optional; this parameter is included when deriving the subkey material for obfuscating and tamper resistance, and may be used for separation of purpose or key rotation. The scrypt step is seasoned by a salt, which defaults to a fixed constant; set an application-specific salt, as in the example above, so that two applications inadvertently sharing a passphrase never derive the same keys.
|
|
49
49
|
|
|
50
|
-
Refer to the Epithet rdoc for the full set of configuration options.
|
|
50
|
+
Refer to the [Epithet rdoc](https://inopinatus.github.io/epithet/) for the full set of configuration options.
|
|
51
51
|
|
|
52
|
-
Note that `decode` returns `nil` when authentication fails, and raises `Epithet::FormatError` (an ArgumentError) on invalid formats.
|
|
52
|
+
Note that `Epithet#decode` returns `nil` when authentication fails, and raises `Epithet::FormatError` (an ArgumentError) on invalid formats.
|
|
53
|
+
|
|
54
|
+
## Platform support
|
|
55
|
+
|
|
56
|
+
Epithet supports standard Ruby (aka MRI/CRuby), JRuby, and TruffleRuby. It uses your platform's OpenSSL scrypt where available; on JRuby it falls back to the BouncyCastle provider bundled with jruby-openssl, and LibreSSL builds can fall back to the optional [scrypt gem](https://rubygems.org/gems/scrypt). To use the latter gem provider, your application must bundle and require `scrypt` before Epithet, we won't require it on your behalf. All three providers produce identical key material. To force a provider without a capability check, pass its class in the scrypt parameters, e.g. `scrypt: { provider: Epithet::Scrypt::BouncyCastle }`; omit `provider:` to select one automatically.
|
|
53
57
|
|
|
54
58
|
## Development
|
|
55
59
|
|
data/SECURITY.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Epithet security
|
|
2
2
|
|
|
3
|
+
This library is intended for high-performance obfuscation of integer sequences, deflection of casual
|
|
4
|
+
tampering, and conversion to a compact, stable wire parameter format that is hard to guess and hard
|
|
5
|
+
to predict. Epithet forms a single-block deterministic PRP with an embedded truncated tag, and
|
|
6
|
+
although it uses standard cryptographic primitives to do so, the design trade-off of the compact
|
|
7
|
+
format means it is not intended to defeat nation-state security services, talented cryptographers,
|
|
8
|
+
or even a well-resourced enterprise.
|
|
9
|
+
|
|
3
10
|
## Cryptographic considerations
|
|
4
11
|
|
|
5
12
|
The primary construction is `AES-256-ECB(id(8B) + MSB_64(HMAC-SHA256(id)))` with the result base58
|
|
@@ -7,12 +14,6 @@ encoded for transmission and a contextual prefix prepended. Subkeys for AES and
|
|
|
7
14
|
derived with HKDF using an internal key generator that takes IKM from a passphrase via scrypt,
|
|
8
15
|
salting generated keys by prefix and context and binding them by algorithm.
|
|
9
16
|
|
|
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
|
-
cryptographers, or even a well-resourced enterprise.
|
|
15
|
-
|
|
16
17
|
An epithet should never be used as an authentication token, only as an object identifier. The
|
|
17
18
|
64-bit tag is shorter than the minimum RFC 2104 §5 recommends for message authentication, and the
|
|
18
19
|
tamper detection is necessarily probabilistic because the MAC is truncated. After N independent
|
|
@@ -38,7 +39,9 @@ decode a value exceeding the 128-bit block.
|
|
|
38
39
|
A weak, guessable, or disclosed passphrase will compromise the obfuscation and tamper-detection
|
|
39
40
|
properties.
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
To sum up: neither the design nor implementation of this library has received an independent
|
|
43
|
+
cryptographic review. It leaks equality, offers a 64-bit forgery bound, and supplies no
|
|
44
|
+
authorization or privacy boundary. Use Epithet at your own risk.
|
|
42
45
|
|
|
43
46
|
## On seasoning
|
|
44
47
|
|
|
@@ -48,7 +51,7 @@ Secondly, for the HKDF extract phase to separate derived subkeys by some applica
|
|
|
48
51
|
division such as purpose or rotation epoch.
|
|
49
52
|
|
|
50
53
|
To avoid confusing the two uses, the HKDF salt is not referred to directly in Epithet's public API,
|
|
51
|
-
and is instead derived from the context and prefix parameters
|
|
54
|
+
and is instead derived from the context and prefix parameters.
|
|
52
55
|
|
|
53
56
|
Epithet does not store or verify passwords; the scrypt salt, and the context & prefix parameters
|
|
54
57
|
used in the HKDF salt, are non-secret configuration and may be safely committed to source control.
|
data/examples/basic.rb
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'securerandom'
|
|
3
4
|
require 'epithet'
|
|
4
5
|
|
|
5
6
|
# Using a random passphrase means that epithet identifiers are effectively
|
|
6
7
|
# ephemeral, since decoding is limited to the lifetime of this process.
|
|
7
8
|
def epithet_initialize
|
|
8
9
|
Epithet.configure(
|
|
9
|
-
passphrase: ENV.fetch('EPITHET_PASSPHRASE') {
|
|
10
|
+
passphrase: ENV.fetch('EPITHET_PASSPHRASE') { SecureRandom.random_bytes(32) },
|
|
10
11
|
scrypt: { salt: 'myapp/production' },
|
|
11
12
|
context: 'v1'
|
|
12
13
|
)
|
|
@@ -16,6 +17,6 @@ epithet_initialize
|
|
|
16
17
|
user_epithet = Epithet.new('user')
|
|
17
18
|
|
|
18
19
|
id = Integer(ARGV.shift || 42)
|
|
19
|
-
param = user_epithet.encode(id)
|
|
20
|
+
param = user_epithet.encode(id)
|
|
20
21
|
|
|
21
22
|
puts "User(#{user_epithet.decode(param)}) => #{param}"
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Epithet
|
|
4
|
+
# Fixed-length base58 codec for a fixed-size block.
|
|
5
|
+
#
|
|
6
|
+
# Obtain codecs via Block58::build, which selects the fastest variant for
|
|
7
|
+
# the block size, an unrolled decoder for 16-byte blocks, or the generic
|
|
8
|
+
# chunked decoder otherwise.
|
|
9
|
+
class Block58
|
|
10
|
+
# `= '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'`
|
|
11
|
+
Alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
|
12
|
+
|
|
13
|
+
POW58 = Array.new(11) { 58**it }.freeze # :nodoc:
|
|
14
|
+
|
|
15
|
+
attr_reader :size
|
|
16
|
+
|
|
17
|
+
# Same as ::new but may select a tuned subclass for performance.
|
|
18
|
+
def self.build(block_size, ...) = (block_size == 16 ? Unrolled16 : self).new(block_size, ...)
|
|
19
|
+
|
|
20
|
+
# Create a codec for a block size in bytes.
|
|
21
|
+
#
|
|
22
|
+
# The alphabet must be 58 distinct bytes in ascending order, so that
|
|
23
|
+
# lexicographic order agrees with numeric order.
|
|
24
|
+
def initialize(block_size, alphabet: Alphabet)
|
|
25
|
+
raise ArgumentError, 'invalid block size' unless Integer === block_size && block_size > 0
|
|
26
|
+
@alphabet = alphabet.b.freeze
|
|
27
|
+
raise ArgumentError, 'invalid alphabet length' unless @alphabet.bytesize == 58
|
|
28
|
+
raise ArgumentError, 'alphabet not strictly ascending' unless @alphabet.bytes.each_cons(2).all? { _2 > _1 }
|
|
29
|
+
@size = ((block_size * 8) / Math.log2(58)).ceil
|
|
30
|
+
@charsel = @alphabet.gsub(/[\^\-\\]/, '\\\\\&').freeze
|
|
31
|
+
@blank = (@alphabet[0] * @size).freeze
|
|
32
|
+
@lut = @alphabet.each_byte.with_index.with_object("\0" * 256) { |(val, idx), lut| lut.setbyte(val, idx) }.freeze
|
|
33
|
+
@limit = 1 << (block_size * 8)
|
|
34
|
+
@max = i2s(@limit - 1).freeze
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def inspect
|
|
38
|
+
"#<#{self.class}:#{'%#016x' % (object_id << 1)} size=#{@size} alphabet=#{@alphabet}>"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Return true if the string is in range with the right size and alphabet.
|
|
42
|
+
# The input is read as bytes, whatever its encoding.
|
|
43
|
+
def valid?(s)
|
|
44
|
+
String === s && s.bytesize == @size && (s = s.b) <= @max && s.count(@charsel) == @size
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Encode an acceptable integer to fixed-length base58.
|
|
48
|
+
def i2s(int)
|
|
49
|
+
raise ArgumentError, 'integer out of block range' unless Integer === int && int >= 0 && int < @limit
|
|
50
|
+
|
|
51
|
+
# Using divmod+setbyte is faster than Integer#digits under YJIT,
|
|
52
|
+
# and about equal in plain MRI.
|
|
53
|
+
alphabet = @alphabet
|
|
54
|
+
out = @blank.dup
|
|
55
|
+
idx = @size - 1
|
|
56
|
+
n = int
|
|
57
|
+
while idx >= 0 && n > 0
|
|
58
|
+
n, rem = n.divmod(58)
|
|
59
|
+
out.setbyte(idx, alphabet.getbyte(rem))
|
|
60
|
+
idx -= 1
|
|
61
|
+
end
|
|
62
|
+
out
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Decode a fixed-length base58 string to an integer.
|
|
66
|
+
# Assumes the input passes `#valid?`, behaviour undefined if it doesn't.
|
|
67
|
+
def s2i(str)
|
|
68
|
+
# Chunking intermediate results into 64-bit integers is ~5x faster
|
|
69
|
+
# under YJIT than Horner's scheme
|
|
70
|
+
#
|
|
71
|
+
# str.each_byte.inject(0) { _1 * 58 + @lut[_2] }
|
|
72
|
+
#
|
|
73
|
+
# at computing the inner product.
|
|
74
|
+
lut = @lut
|
|
75
|
+
size = @size
|
|
76
|
+
pow = POW58
|
|
77
|
+
acc = 0
|
|
78
|
+
pos = 0
|
|
79
|
+
while pos < size
|
|
80
|
+
n = size - pos
|
|
81
|
+
n = 10 if n > 10
|
|
82
|
+
chunk = 0
|
|
83
|
+
i = 0
|
|
84
|
+
while i < n
|
|
85
|
+
chunk = (chunk * 58) + lut.getbyte(str.getbyte(pos))
|
|
86
|
+
pos += 1
|
|
87
|
+
i += 1
|
|
88
|
+
end
|
|
89
|
+
acc = (acc * pow[n]) + chunk
|
|
90
|
+
end
|
|
91
|
+
acc
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Specialised decoder for 16-byte blocks (22 digits) with a fully unrolled inner product.
|
|
95
|
+
class Unrolled16 < Block58
|
|
96
|
+
def initialize(...)
|
|
97
|
+
super
|
|
98
|
+
raise ArgumentError, 'unrolled codec requires a 16-byte block' unless @size == 22
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Decode a 22-digit base58 string to an integer.
|
|
102
|
+
# Assumes the input passes `#valid?`, behaviour undefined if it doesn't.
|
|
103
|
+
def s2i(str)
|
|
104
|
+
# rubocop:disable Style/NumericLiterals, Lint/AmbiguousOperatorPrecedence, Layout
|
|
105
|
+
#
|
|
106
|
+
# By unrolling the chunks against literal coefficients, this tested with Ruby 4.0
|
|
107
|
+
# at ~1.5x faster under YJIT than the generic chunked Block58#s2i, and ~6x faster
|
|
108
|
+
# than Horner's scheme.
|
|
109
|
+
lut = @lut
|
|
110
|
+
|
|
111
|
+
acc0 = lut.getbyte(str.getbyte(0)) * 7427658739644928 +
|
|
112
|
+
lut.getbyte(str.getbyte(1)) * 128063081718016 +
|
|
113
|
+
lut.getbyte(str.getbyte(2)) * 2207984167552 +
|
|
114
|
+
lut.getbyte(str.getbyte(3)) * 38068692544 +
|
|
115
|
+
lut.getbyte(str.getbyte(4)) * 656356768 +
|
|
116
|
+
lut.getbyte(str.getbyte(5)) * 11316496 +
|
|
117
|
+
lut.getbyte(str.getbyte(6)) * 195112 +
|
|
118
|
+
lut.getbyte(str.getbyte(7)) * 3364 +
|
|
119
|
+
lut.getbyte(str.getbyte(8)) * 58 +
|
|
120
|
+
lut.getbyte(str.getbyte(9))
|
|
121
|
+
|
|
122
|
+
acc1 = lut.getbyte(str.getbyte(10)) * 7427658739644928 +
|
|
123
|
+
lut.getbyte(str.getbyte(11)) * 128063081718016 +
|
|
124
|
+
lut.getbyte(str.getbyte(12)) * 2207984167552 +
|
|
125
|
+
lut.getbyte(str.getbyte(13)) * 38068692544 +
|
|
126
|
+
lut.getbyte(str.getbyte(14)) * 656356768 +
|
|
127
|
+
lut.getbyte(str.getbyte(15)) * 11316496 +
|
|
128
|
+
lut.getbyte(str.getbyte(16)) * 195112 +
|
|
129
|
+
lut.getbyte(str.getbyte(17)) * 3364 +
|
|
130
|
+
lut.getbyte(str.getbyte(18)) * 58 +
|
|
131
|
+
lut.getbyte(str.getbyte(19))
|
|
132
|
+
|
|
133
|
+
lut.getbyte(str.getbyte(21)) +
|
|
134
|
+
lut.getbyte(str.getbyte(20)) * 58 +
|
|
135
|
+
acc1 * 3364 +
|
|
136
|
+
acc0 * 1449225352009601191936
|
|
137
|
+
|
|
138
|
+
# rubocop:enable Style/NumericLiterals, Lint/AmbiguousOperatorPrecedence, Layout
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
require 'epithet/block58'
|
|
5
|
+
require 'epithet/keygen'
|
|
6
|
+
|
|
7
|
+
class Epithet
|
|
8
|
+
# Class for passing around preset configs. See Epithet::configure for options.
|
|
9
|
+
class Config
|
|
10
|
+
attr_reader :keygen, :context, :separator, :cipher, :digest, :codec # :nodoc:
|
|
11
|
+
|
|
12
|
+
def initialize(opts = {})
|
|
13
|
+
opts = opts.dup
|
|
14
|
+
@separator = -String(opts.delete(:separator) { '_' })
|
|
15
|
+
@context = -String(opts.delete(:context))
|
|
16
|
+
alphabet = String(opts.delete(:alphabet) { Block58::Alphabet })
|
|
17
|
+
@cipher = -(opts.delete(:cipher) || 'aes-256-ecb').downcase
|
|
18
|
+
@digest = -(opts.delete(:digest) || 'sha256').downcase
|
|
19
|
+
keygen, passphrase, scrypt = %i[keygen passphrase scrypt].map { opts.delete it }
|
|
20
|
+
|
|
21
|
+
cipher = probe(OpenSSL::Cipher, @cipher)
|
|
22
|
+
digest = probe(OpenSSL::Digest, @digest)
|
|
23
|
+
|
|
24
|
+
raise ArgumentError, 'separator intersects alphabet' if @separator.bytes.intersect?(alphabet.bytes)
|
|
25
|
+
raise ArgumentError, "#{@cipher} not a 128-bit block cipher" if cipher.block_size != 16
|
|
26
|
+
raise ArgumentError, "#{@cipher} requires an IV/nonce" if cipher.iv_len != 0
|
|
27
|
+
raise ArgumentError, "#{@digest} produces < 64-bit digest" if digest.digest_length < 8
|
|
28
|
+
raise ArgumentError, 'use keygen: or passphrase:, not both' if keygen && (passphrase || scrypt)
|
|
29
|
+
raise ArgumentError, 'one of passphrase: or keygen: is required' unless keygen || passphrase
|
|
30
|
+
raise ArgumentError, "unused option(s) #{opts.keys}" unless opts.empty?
|
|
31
|
+
|
|
32
|
+
@codec = Block58.build(cipher.block_size, alphabet:)
|
|
33
|
+
@keygen = keygen || Keygen.new(passphrase:, digest: @digest, scrypt:)
|
|
34
|
+
freeze
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The openssl gem <4.0 raises bare RuntimeError for unrecognised algorithm
|
|
38
|
+
# names; jruby-openssl raises NotImplementedError.
|
|
39
|
+
def probe(kind, name) # :nodoc:
|
|
40
|
+
kind.new(name)
|
|
41
|
+
rescue OpenSSL::OpenSSLError, RuntimeError, NotImplementedError
|
|
42
|
+
raise ArgumentError, "unknown #{kind.name[/\w+\z/].downcase} #{name}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
require 'epithet/scrypt'
|
|
5
|
+
|
|
6
|
+
class Epithet
|
|
7
|
+
# Key derivation helper
|
|
8
|
+
class Keygen
|
|
9
|
+
# Default parameters for scrypt.
|
|
10
|
+
#
|
|
11
|
+
# ```ruby
|
|
12
|
+
# DEFAULT_SCRYPT_PARAMS = {
|
|
13
|
+
# provider: Epithet::Scrypt,
|
|
14
|
+
# salt: 'epithet-default',
|
|
15
|
+
# N: 1 << 17,
|
|
16
|
+
# r: 8,
|
|
17
|
+
# p: 1,
|
|
18
|
+
# length: 32
|
|
19
|
+
# }.freeze
|
|
20
|
+
# ```
|
|
21
|
+
DEFAULT_SCRYPT_PARAMS = {
|
|
22
|
+
provider: Epithet::Scrypt,
|
|
23
|
+
salt: 'epithet-default',
|
|
24
|
+
N: 1 << 17,
|
|
25
|
+
r: 8,
|
|
26
|
+
p: 1,
|
|
27
|
+
length: 32
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
# Create a new key generator from a supplied passphrase, or from high-entropy initial key
|
|
31
|
+
# material if already prepared. The passphrase will be hashed with scrypt. Supplied scrypt
|
|
32
|
+
# params, if any, are merged over DEFAULT_SCRYPT_PARAMS, so this works:
|
|
33
|
+
#
|
|
34
|
+
# Epithet::Keygen.new(
|
|
35
|
+
# passphrase: ENV.fetch('EPITHET_PASSPHRASE'),
|
|
36
|
+
# scrypt: { salt: "#{MyApp.name}/#{MyApp.env}" }
|
|
37
|
+
# )
|
|
38
|
+
#
|
|
39
|
+
# A scrypt provider will be chosen by `Epithet::Scrypt`. To override automatic selection
|
|
40
|
+
# and use a specific scrypt provider class, pass it as `provider` in the scrypt parameters:
|
|
41
|
+
#
|
|
42
|
+
# kg = Epithet::Keygen.new(passphrase: 'pw', scrypt: { provider: Epithet::Scrypt::OpenSSL })
|
|
43
|
+
#
|
|
44
|
+
# but this should be unnecessary in the common case.
|
|
45
|
+
def initialize(ikm: nil, passphrase: nil, digest: 'sha256', scrypt: {})
|
|
46
|
+
raise ArgumentError, 'keygen requires either ikm or passphrase' unless passphrase.nil? ^ ikm.nil?
|
|
47
|
+
|
|
48
|
+
@ikm = (ikm&.b || build_scrypt(Hash(scrypt)).ikm(passphrase)).freeze
|
|
49
|
+
@digest = -String(digest)
|
|
50
|
+
freeze
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def inspect
|
|
54
|
+
"#<#{self.class}:#{'%#016x' % (object_id << 1)} digest=#{@digest}>"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Derive a key via HKDF.
|
|
58
|
+
def generate(info, salt, length)
|
|
59
|
+
OpenSSL::KDF.hkdf(@ikm, hash: @digest, info:, salt:, length:)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def build_scrypt(opts)
|
|
65
|
+
params = DEFAULT_SCRYPT_PARAMS.merge(opts)
|
|
66
|
+
params.delete(:provider).new(**params)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
require 'monitor'
|
|
5
|
+
|
|
6
|
+
class Epithet
|
|
7
|
+
# Provider registry & classes for the scrypt implementations.
|
|
8
|
+
#
|
|
9
|
+
# A scrypt provider is any class that can be instantiated with the scrypt
|
|
10
|
+
# parameters `salt`, `N`, `r`, `p`, and `length`, has a predicate singleton
|
|
11
|
+
# method `auto?` indicating willingness to operate, and an instance method
|
|
12
|
+
# `ikm(passphrase)` that derives the key material. Automatic selection picks
|
|
13
|
+
# the most recently registered willing provider.
|
|
14
|
+
#
|
|
15
|
+
# Of the builtin providers, we prefer `OpenSSL::KDF.scrypt` when the platform
|
|
16
|
+
# supplies it, then JRuby's BouncyCastle implementation, and finally an
|
|
17
|
+
# already-loaded [`scrypt`](https://rubygems.org/gems/scrypt) gem. If none of
|
|
18
|
+
# these are available, selection falls through to a base class which raises
|
|
19
|
+
# `NotImplementedError` at the point of use.
|
|
20
|
+
#
|
|
21
|
+
# Epithet deliberately does not require the optional `scrypt` gem itself; an
|
|
22
|
+
# application relying on the `SCryptGem` provider should bundle and require
|
|
23
|
+
# `scrypt` before configuring Epithet.
|
|
24
|
+
#
|
|
25
|
+
# You may register a custom scrypt provider with the necessary signature, even
|
|
26
|
+
# after Epithet has loaded:
|
|
27
|
+
#
|
|
28
|
+
# class MyProvider < Epithet::Scrypt::Base
|
|
29
|
+
# def ikm(passphrase)
|
|
30
|
+
# #...
|
|
31
|
+
# end
|
|
32
|
+
# end
|
|
33
|
+
# Epithet::Scrypt.register(MyProvider)
|
|
34
|
+
#
|
|
35
|
+
# and this will be unconditionally preferred unless you also define a selective
|
|
36
|
+
# `auto?` method.
|
|
37
|
+
#
|
|
38
|
+
# Your `ikm` method should return `length` bytes of key material.
|
|
39
|
+
#
|
|
40
|
+
# Using this mechanism as a hook to deviate from the scrypt algorithm is not
|
|
41
|
+
# recommended; the better move would be to substitute a variant key
|
|
42
|
+
# generator in the `Epithet::Config` parameters, or supply IKM to an
|
|
43
|
+
# `Epithet::Keygen`.
|
|
44
|
+
module Scrypt
|
|
45
|
+
extend MonitorMixin
|
|
46
|
+
|
|
47
|
+
class << self
|
|
48
|
+
# The registered providers, in automatic-selection order.
|
|
49
|
+
def providers = synchronize { @providers }
|
|
50
|
+
|
|
51
|
+
# Registers `klass` ahead of the existing providers.
|
|
52
|
+
def register(klass) = synchronize { @providers = [klass, *@providers].freeze }
|
|
53
|
+
|
|
54
|
+
# Returns the first registered provider willing to operate.
|
|
55
|
+
def auto = providers.detect(&:auto?)
|
|
56
|
+
|
|
57
|
+
# Instantiates the automatically selected provider.
|
|
58
|
+
def new(...) = auto.new(...)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# :section: Providers
|
|
62
|
+
#
|
|
63
|
+
# ### `Base` class
|
|
64
|
+
#
|
|
65
|
+
# Defines the common `salt`, `N`, `r`, `p`, and `length` parameters. It is
|
|
66
|
+
# the last-resort provider: `ikm(passphrase)` raises `NotImplementedError`
|
|
67
|
+
# when no scrypt implementation is available.
|
|
68
|
+
#
|
|
69
|
+
# ### `OpenSSL` class
|
|
70
|
+
#
|
|
71
|
+
# The default, preferred provider. It uses `OpenSSL::KDF.scrypt` when the
|
|
72
|
+
# platform's OpenSSL supplies it.
|
|
73
|
+
#
|
|
74
|
+
# ### `BouncyCastle` class
|
|
75
|
+
#
|
|
76
|
+
# The JRuby fallback. JRuby's `openssl` is a BouncyCastle wrapper, but does
|
|
77
|
+
# not expose BouncyCastle's scrypt, so this provider invokes it directly.
|
|
78
|
+
#
|
|
79
|
+
# ### `SCryptGem` class
|
|
80
|
+
#
|
|
81
|
+
# A fallback for LibreSSL and other OpenSSLs that lack scrypt. It is
|
|
82
|
+
# available when the application has already loaded the optional
|
|
83
|
+
# [`scrypt`](https://rubygems.org/gems/scrypt) gem.
|
|
84
|
+
Base = Data.define(:salt, :N, :r, :p, :length) do # :nodoc: # rubocop:disable Naming/MethodName
|
|
85
|
+
def self.auto? = true
|
|
86
|
+
def ikm(passphrase) = raise NotImplementedError, 'no scrypt available'
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
class SCryptGem < Base # :nodoc:
|
|
90
|
+
def self.auto? = defined? ::SCrypt::Engine
|
|
91
|
+
|
|
92
|
+
def ikm(passphrase)
|
|
93
|
+
::SCrypt::Engine.scrypt(passphrase, salt, self.N, r, p, length)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
class BouncyCastle < Base # :nodoc:
|
|
98
|
+
def self.auto? = RUBY_ENGINE == 'jruby'
|
|
99
|
+
|
|
100
|
+
def ikm(passphrase)
|
|
101
|
+
String.from_java_bytes(
|
|
102
|
+
Java::OrgBouncycastleCryptoGenerators::SCrypt.generate(
|
|
103
|
+
passphrase.to_java_bytes, salt.to_java_bytes, self.N, r, p, length
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
class OpenSSL < Base # :nodoc:
|
|
110
|
+
def self.auto? = ::OpenSSL::KDF.respond_to? :scrypt
|
|
111
|
+
|
|
112
|
+
def ikm(passphrase)
|
|
113
|
+
::OpenSSL::KDF.scrypt(passphrase, **to_h)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
@providers = [OpenSSL, BouncyCastle, SCryptGem, Base].freeze
|
|
118
|
+
end
|
|
119
|
+
end
|
data/lib/epithet/version.rb
CHANGED
data/lib/epithet.rb
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
require 'epithet/version'
|
|
4
4
|
require 'openssl'
|
|
5
|
+
require 'epithet/scrypt'
|
|
6
|
+
require 'epithet/keygen'
|
|
7
|
+
require 'epithet/block58'
|
|
8
|
+
require 'epithet/config'
|
|
5
9
|
|
|
6
10
|
#
|
|
7
11
|
# Epithet, a tool for external identifiers.
|
|
@@ -35,6 +39,9 @@ class Epithet
|
|
|
35
39
|
# Raised by #decode when the input is not valid wire format.
|
|
36
40
|
FormatError = Class.new(ArgumentError)
|
|
37
41
|
|
|
42
|
+
# Raised if no defaults are configured.
|
|
43
|
+
ConfigurationError = Class.new(RuntimeError)
|
|
44
|
+
|
|
38
45
|
# Create an encoder/decoder.
|
|
39
46
|
#
|
|
40
47
|
# Setup could be moderately expensive due to key derivation; you are recommended to cache
|
|
@@ -45,14 +52,15 @@ class Epithet
|
|
|
45
52
|
#
|
|
46
53
|
# * `config` is optional and intended for cases where you need finer control than global defaults.
|
|
47
54
|
#
|
|
48
|
-
#
|
|
55
|
+
# Mixing incompatible character encodings across prefix/separator/payload may raise
|
|
56
|
+
# Encoding::CompatibilityError or Epithet::FormatError. Don't expect UTF-16LE to work.
|
|
49
57
|
#
|
|
50
58
|
# The simplest typical invocation is `Epithet.new('prefix')`.
|
|
51
59
|
#
|
|
52
60
|
def initialize(prefix, config: Epithet.defaults)
|
|
53
|
-
prefix = String(prefix)
|
|
54
|
-
@prefix = prefix.empty? ? prefix : prefix + config.separator
|
|
55
|
-
@wire_prefix = @prefix.b
|
|
61
|
+
prefix = -String(prefix)
|
|
62
|
+
@prefix = prefix.empty? ? prefix : -(prefix + config.separator)
|
|
63
|
+
@wire_prefix = @prefix.b.freeze
|
|
56
64
|
key_salt = [prefix.bytesize, prefix, config.context.bytesize, config.context].pack('Q>Z*Q>Z*')
|
|
57
65
|
@codec = config.codec
|
|
58
66
|
|
|
@@ -130,7 +138,7 @@ class Epithet
|
|
|
130
138
|
# #### Options
|
|
131
139
|
#
|
|
132
140
|
# * `:passphrase` - Install new key generator with scrypt-derived key material
|
|
133
|
-
# * `:scrypt` -
|
|
141
|
+
# * `:scrypt` - Override scrypt parameters (cost, salt, provider etc); see Epithet::Scrypt
|
|
134
142
|
# * `:keygen` - Use an existing key generator
|
|
135
143
|
# * `:cipher` - Must be a 128-bit block cipher in ECB mode or equivalent; omit for standard `aes-256-ecb`
|
|
136
144
|
# * `:digest` - Must be >= 64 bits digest; omit for standard `sha256`
|
|
@@ -145,6 +153,7 @@ class Epithet
|
|
|
145
153
|
#
|
|
146
154
|
# If passing an existing key generator, the object must respond to `generate(info, salt, length)`
|
|
147
155
|
# and return a byte string suitable for use with OpenSSL cryptographic primitives.
|
|
156
|
+
# Configuration sharing means an alternative keygen must support concurrent `generate` calls.
|
|
148
157
|
#
|
|
149
158
|
# See [`SECURITY.md`](SECURITY.md) for discussion of ciphers & digests.
|
|
150
159
|
#
|
|
@@ -154,8 +163,8 @@ class Epithet
|
|
|
154
163
|
#
|
|
155
164
|
# cfg = Epithet::Config.new(
|
|
156
165
|
# keygen: my_key_gen,
|
|
157
|
-
# cipher: '
|
|
158
|
-
# digest: '
|
|
166
|
+
# cipher: 'camellia-256-ecb',
|
|
167
|
+
# digest: 'sha224'
|
|
159
168
|
# )
|
|
160
169
|
#
|
|
161
170
|
# and either install this as default with
|
|
@@ -167,215 +176,6 @@ class Epithet
|
|
|
167
176
|
# acct_epithet = Epithet.new('acct', config: cfg)
|
|
168
177
|
#
|
|
169
178
|
def configure(opts) = @defaults = Config === opts ? opts : Config.new(opts)
|
|
170
|
-
def defaults() = @defaults || raise('no Epithet defaults configured')
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
# Class for passing around preset configs. See Epithet::configure for options.
|
|
174
|
-
class Config
|
|
175
|
-
attr_reader :keygen, :context, :separator, :cipher, :digest, :codec # :nodoc:
|
|
176
|
-
|
|
177
|
-
def initialize(opts = {})
|
|
178
|
-
opts = opts.dup
|
|
179
|
-
@separator = -String(opts.delete(:separator) { '_' })
|
|
180
|
-
@context = -String(opts.delete(:context))
|
|
181
|
-
alphabet = String(opts.delete(:alphabet) { Block58::Alphabet })
|
|
182
|
-
@cipher = -(opts.delete(:cipher) || 'aes-256-ecb').downcase
|
|
183
|
-
@digest = -(opts.delete(:digest) || 'sha256').downcase
|
|
184
|
-
keygen, passphrase, scrypt = %i[keygen passphrase scrypt].map { opts.delete it }
|
|
185
|
-
|
|
186
|
-
cipher = OpenSSL::Cipher.new(@cipher)
|
|
187
|
-
raise ArgumentError, 'separator intersects alphabet' if @separator.bytes.intersect?(alphabet.bytes)
|
|
188
|
-
raise ArgumentError, "#{@cipher} not a 128-bit block cipher" if cipher.block_size != 16
|
|
189
|
-
raise ArgumentError, "#{@cipher} requires an IV/nonce" if cipher.iv_len != 0
|
|
190
|
-
raise ArgumentError, "#{@digest} produces < 64-bit digest" if OpenSSL::Digest.new(@digest).digest_length < 8
|
|
191
|
-
raise ArgumentError, 'use keygen: or passphrase:, not both' if keygen && (passphrase || scrypt)
|
|
192
|
-
raise ArgumentError, 'one of passphrase: or keygen: is required' unless keygen || passphrase
|
|
193
|
-
raise ArgumentError, "unused option(s) #{opts.keys}" unless opts.empty?
|
|
194
|
-
|
|
195
|
-
@codec = Block58.build(cipher.block_size, alphabet:)
|
|
196
|
-
@keygen = keygen || Keygen.new(passphrase:, digest: @digest, scrypt:)
|
|
197
|
-
freeze
|
|
198
|
-
end
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
# Key derivation helper
|
|
202
|
-
class Keygen
|
|
203
|
-
# Default parameters for scrypt.
|
|
204
|
-
#
|
|
205
|
-
# ```ruby
|
|
206
|
-
# DEFAULT_SCRYPT_PARAMS = {
|
|
207
|
-
# salt: 'epithet-default',
|
|
208
|
-
# N: 1 << 17,
|
|
209
|
-
# r: 8,
|
|
210
|
-
# p: 1
|
|
211
|
-
# }.freeze
|
|
212
|
-
# ```
|
|
213
|
-
DEFAULT_SCRYPT_PARAMS = {
|
|
214
|
-
salt: 'epithet-default',
|
|
215
|
-
N: 1 << 17,
|
|
216
|
-
r: 8,
|
|
217
|
-
p: 1
|
|
218
|
-
}.freeze
|
|
219
|
-
|
|
220
|
-
# Create a new key generator from either high-entropy key material, or a supplied passphrase.
|
|
221
|
-
# Supplied scrypt params, if any, are merged over DEFAULT_SCRYPT_PARAMS.
|
|
222
|
-
def initialize(ikm: nil, passphrase: nil, digest: 'sha256', scrypt: {})
|
|
223
|
-
if (passphrase.nil? && ikm.nil?) || (!passphrase.nil? && !ikm.nil?)
|
|
224
|
-
raise ArgumentError, 'keygen requires either ikm or passphrase'
|
|
225
|
-
end
|
|
226
|
-
raise ArgumentError, 'scrypt length is not configurable' if scrypt&.key?(:length)
|
|
227
|
-
|
|
228
|
-
@ikm = (ikm&.b || OpenSSL::KDF.scrypt(passphrase, **DEFAULT_SCRYPT_PARAMS, **scrypt, length: 32)).freeze
|
|
229
|
-
@digest = -String(digest)
|
|
230
|
-
freeze
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
def inspect
|
|
234
|
-
"#<#{self.class}:#{'%#016x' % (object_id << 1)} digest=#{@digest}>"
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
# Derive a key via HKDF.
|
|
238
|
-
def generate(info, salt, length)
|
|
239
|
-
OpenSSL::KDF.hkdf(@ikm, hash: @digest, info:, salt:, length:)
|
|
240
|
-
end
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
# Fixed-length base58 codec for a fixed-size block.
|
|
244
|
-
#
|
|
245
|
-
# Obtain codecs via Block58::build, which selects the fastest variant for
|
|
246
|
-
# the block size, an unrolled decoder for 16-byte blocks, or the generic
|
|
247
|
-
# chunked decoder otherwise.
|
|
248
|
-
class Block58
|
|
249
|
-
# `= '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'`
|
|
250
|
-
Alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
|
251
|
-
|
|
252
|
-
POW58 = Array.new(11) { 58**it }.freeze # :nodoc:
|
|
253
|
-
|
|
254
|
-
attr_reader :size
|
|
255
|
-
|
|
256
|
-
# Same as ::new but may select a tuned subclass for performance.
|
|
257
|
-
def self.build(block_size, ...) = (block_size == 16 ? Unrolled16 : self).new(block_size, ...)
|
|
258
|
-
|
|
259
|
-
# Create a codec for a block size in bytes.
|
|
260
|
-
#
|
|
261
|
-
# The alphabet must be 58 distinct bytes in ascending order, so that
|
|
262
|
-
# lexicographic order agrees with numeric order.
|
|
263
|
-
def initialize(block_size, alphabet: Alphabet)
|
|
264
|
-
raise ArgumentError, 'invalid block size' unless Integer === block_size && block_size > 0
|
|
265
|
-
@alphabet = alphabet.b.freeze
|
|
266
|
-
raise ArgumentError, 'invalid alphabet length' unless @alphabet.bytesize == 58
|
|
267
|
-
raise ArgumentError, 'alphabet not strictly ascending' unless @alphabet.bytes.each_cons(2).all? { _2 > _1 }
|
|
268
|
-
@size = ((block_size * 8) / Math.log2(58)).ceil
|
|
269
|
-
@charsel = @alphabet.gsub(/[\^\-\\]/, '\\\\\&').freeze
|
|
270
|
-
@blank = @alphabet[0] * @size
|
|
271
|
-
@lut = @alphabet.each_byte.with_index.with_object("\0" * 256) { |(val, idx), lut| lut.setbyte(val, idx) }.freeze
|
|
272
|
-
@limit = 1 << (block_size * 8)
|
|
273
|
-
@max = i2s(@limit - 1).freeze
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
def inspect
|
|
277
|
-
"#<#{self.class}:#{'%#016x' % (object_id << 1)} size=#{@size} alphabet=#{@alphabet}>"
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
# Return true if the string is in range with the right size and alphabet.
|
|
281
|
-
# The input is read as bytes, whatever its encoding.
|
|
282
|
-
def valid?(s)
|
|
283
|
-
String === s && s.bytesize == @size && (s = s.b) <= @max && s.count(@charsel) == @size
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
# Encode an acceptable integer to fixed-length base58.
|
|
287
|
-
def i2s(int)
|
|
288
|
-
raise ArgumentError, 'integer out of block range' unless Integer === int && int >= 0 && int < @limit
|
|
289
|
-
|
|
290
|
-
# Using divmod+setbyte is faster than Integer#digits under YJIT,
|
|
291
|
-
# and about equal in plain MRI.
|
|
292
|
-
alphabet = @alphabet
|
|
293
|
-
out = @blank.dup
|
|
294
|
-
idx = @size - 1
|
|
295
|
-
n = int
|
|
296
|
-
while idx >= 0 && n > 0
|
|
297
|
-
n, rem = n.divmod(58)
|
|
298
|
-
out.setbyte(idx, alphabet.getbyte(rem))
|
|
299
|
-
idx -= 1
|
|
300
|
-
end
|
|
301
|
-
out
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
# Decode a fixed-length base58 string to an integer.
|
|
305
|
-
# Assumes the input passes `#valid?`, behaviour undefined if it doesn't.
|
|
306
|
-
def s2i(str)
|
|
307
|
-
# Chunking intermediate results into 64-bit integers is ~5x faster
|
|
308
|
-
# under YJIT than Horner's scheme
|
|
309
|
-
#
|
|
310
|
-
# str.each_byte.inject(0) { _1 * 58 + @lut[_2] }
|
|
311
|
-
#
|
|
312
|
-
# at computing the inner product.
|
|
313
|
-
lut = @lut
|
|
314
|
-
size = @size
|
|
315
|
-
pow = POW58
|
|
316
|
-
acc = 0
|
|
317
|
-
pos = 0
|
|
318
|
-
while pos < size
|
|
319
|
-
n = size - pos
|
|
320
|
-
n = 10 if n > 10
|
|
321
|
-
chunk = 0
|
|
322
|
-
i = 0
|
|
323
|
-
while i < n
|
|
324
|
-
chunk = (chunk * 58) + lut.getbyte(str.getbyte(pos))
|
|
325
|
-
pos += 1
|
|
326
|
-
i += 1
|
|
327
|
-
end
|
|
328
|
-
acc = (acc * pow[n]) + chunk
|
|
329
|
-
end
|
|
330
|
-
acc
|
|
331
|
-
end
|
|
332
|
-
|
|
333
|
-
# Specialised decoder for 16-byte blocks (22 digits) with a fully unrolled inner product.
|
|
334
|
-
class Unrolled16 < Block58
|
|
335
|
-
def initialize(...)
|
|
336
|
-
super
|
|
337
|
-
raise ArgumentError, 'unrolled codec requires a 16-byte block' unless @size == 22
|
|
338
|
-
end
|
|
339
|
-
|
|
340
|
-
# Decode a 22-digit base58 string to an integer.
|
|
341
|
-
# Assumes the input passes `#valid?`, behaviour undefined if it doesn't.
|
|
342
|
-
def s2i(str)
|
|
343
|
-
# rubocop:disable Style/NumericLiterals, Lint/AmbiguousOperatorPrecedence, Layout
|
|
344
|
-
#
|
|
345
|
-
# By unrolling the chunks against literal coefficients, this tested with Ruby 4.0
|
|
346
|
-
# at ~1.5x faster under YJIT than the generic chunked Block58#s2i, and ~6x faster
|
|
347
|
-
# than Horner's scheme.
|
|
348
|
-
lut = @lut
|
|
349
|
-
|
|
350
|
-
acc0 = lut.getbyte(str.getbyte(0)) * 7427658739644928 +
|
|
351
|
-
lut.getbyte(str.getbyte(1)) * 128063081718016 +
|
|
352
|
-
lut.getbyte(str.getbyte(2)) * 2207984167552 +
|
|
353
|
-
lut.getbyte(str.getbyte(3)) * 38068692544 +
|
|
354
|
-
lut.getbyte(str.getbyte(4)) * 656356768 +
|
|
355
|
-
lut.getbyte(str.getbyte(5)) * 11316496 +
|
|
356
|
-
lut.getbyte(str.getbyte(6)) * 195112 +
|
|
357
|
-
lut.getbyte(str.getbyte(7)) * 3364 +
|
|
358
|
-
lut.getbyte(str.getbyte(8)) * 58 +
|
|
359
|
-
lut.getbyte(str.getbyte(9))
|
|
360
|
-
|
|
361
|
-
acc1 = lut.getbyte(str.getbyte(10)) * 7427658739644928 +
|
|
362
|
-
lut.getbyte(str.getbyte(11)) * 128063081718016 +
|
|
363
|
-
lut.getbyte(str.getbyte(12)) * 2207984167552 +
|
|
364
|
-
lut.getbyte(str.getbyte(13)) * 38068692544 +
|
|
365
|
-
lut.getbyte(str.getbyte(14)) * 656356768 +
|
|
366
|
-
lut.getbyte(str.getbyte(15)) * 11316496 +
|
|
367
|
-
lut.getbyte(str.getbyte(16)) * 195112 +
|
|
368
|
-
lut.getbyte(str.getbyte(17)) * 3364 +
|
|
369
|
-
lut.getbyte(str.getbyte(18)) * 58 +
|
|
370
|
-
lut.getbyte(str.getbyte(19))
|
|
371
|
-
|
|
372
|
-
lut.getbyte(str.getbyte(21)) +
|
|
373
|
-
lut.getbyte(str.getbyte(20)) * 58 +
|
|
374
|
-
acc1 * 3364 +
|
|
375
|
-
acc0 * 1449225352009601191936
|
|
376
|
-
|
|
377
|
-
# rubocop:enable Style/NumericLiterals, Lint/AmbiguousOperatorPrecedence, Layout
|
|
378
|
-
end
|
|
379
|
-
end
|
|
179
|
+
def defaults() = @defaults || raise(ConfigurationError, 'no Epithet defaults configured')
|
|
380
180
|
end
|
|
381
181
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: epithet
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Josh Goodall
|
|
@@ -37,20 +37,6 @@ dependencies:
|
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '0'
|
|
40
|
-
- !ruby/object:Gem::Dependency
|
|
41
|
-
name: rdoc
|
|
42
|
-
requirement: !ruby/object:Gem::Requirement
|
|
43
|
-
requirements:
|
|
44
|
-
- - ">="
|
|
45
|
-
- !ruby/object:Gem::Version
|
|
46
|
-
version: '7'
|
|
47
|
-
type: :development
|
|
48
|
-
prerelease: false
|
|
49
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
-
requirements:
|
|
51
|
-
- - ">="
|
|
52
|
-
- !ruby/object:Gem::Version
|
|
53
|
-
version: '7'
|
|
54
40
|
- !ruby/object:Gem::Dependency
|
|
55
41
|
name: rubocop
|
|
56
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -79,6 +65,10 @@ files:
|
|
|
79
65
|
- SECURITY.md
|
|
80
66
|
- examples/basic.rb
|
|
81
67
|
- lib/epithet.rb
|
|
68
|
+
- lib/epithet/block58.rb
|
|
69
|
+
- lib/epithet/config.rb
|
|
70
|
+
- lib/epithet/keygen.rb
|
|
71
|
+
- lib/epithet/scrypt.rb
|
|
82
72
|
- lib/epithet/version.rb
|
|
83
73
|
homepage: https://github.com/inopinatus/epithet
|
|
84
74
|
licenses:
|
|
@@ -103,7 +93,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
103
93
|
- !ruby/object:Gem::Version
|
|
104
94
|
version: '0'
|
|
105
95
|
requirements: []
|
|
106
|
-
rubygems_version: 4.0.
|
|
96
|
+
rubygems_version: 4.0.16
|
|
107
97
|
specification_version: 4
|
|
108
98
|
summary: External base58 identifiers with reversible, tamper-evident obfuscation.
|
|
109
99
|
test_files: []
|