baseh 1.1.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +117 -27
- data/lib/baseh/baseh.rb +302 -25
- data/lib/baseh/basen.rb +7 -0
- data/lib/baseh/checksum.rb +13 -6
- data/lib/baseh/feistel.rb +27 -14
- data/lib/baseh/profile.rb +143 -12
- data/lib/baseh/profiles.rb +97 -30
- data/lib/baseh/version.rb +1 -1
- data/lib/baseh.rb +89 -11
- metadata +6 -4
- data/lib/baseh/zero.rb +0 -71
data/lib/baseh/checksum.rb
CHANGED
|
@@ -10,9 +10,16 @@ module Baseh
|
|
|
10
10
|
module_function
|
|
11
11
|
|
|
12
12
|
# Returns the checksum value in [0, modulus).
|
|
13
|
-
|
|
13
|
+
# Spec 22: expandable generations may pass a shorter effective checksum
|
|
14
|
+
# length; the modulus is then S^length instead of the profile default.
|
|
15
|
+
def checksum_value(prepared, body, body_index = nil, length = prepared.checksum_length)
|
|
14
16
|
body_index ||= BaseN.alphabet_index(prepared.body_alphabet)
|
|
15
|
-
modulus =
|
|
17
|
+
modulus =
|
|
18
|
+
if length == prepared.checksum_length
|
|
19
|
+
prepared.checksum_modulus
|
|
20
|
+
else
|
|
21
|
+
[prepared.checksum_alphabet.length, 1].max**length
|
|
22
|
+
end
|
|
16
23
|
|
|
17
24
|
state = INITIAL_STATE
|
|
18
25
|
prepared.profile_id.each_byte do |byte|
|
|
@@ -33,12 +40,12 @@ module Baseh
|
|
|
33
40
|
end
|
|
34
41
|
|
|
35
42
|
# Expected checksum string for a normalized body.
|
|
36
|
-
def calculate_checksum(prepared, body, body_index = nil)
|
|
37
|
-
return "" if
|
|
43
|
+
def calculate_checksum(prepared, body, body_index = nil, length = prepared.checksum_length)
|
|
44
|
+
return "" if length.zero?
|
|
38
45
|
|
|
39
46
|
body_index ||= BaseN.alphabet_index(prepared.body_alphabet)
|
|
40
|
-
value = checksum_value(prepared, body, body_index)
|
|
41
|
-
BaseN.encode_base_n(value, prepared.checksum_alphabet,
|
|
47
|
+
value = checksum_value(prepared, body, body_index, length)
|
|
48
|
+
BaseN.encode_base_n(value, prepared.checksum_alphabet, length)
|
|
42
49
|
end
|
|
43
50
|
end
|
|
44
51
|
end
|
data/lib/baseh/feistel.rb
CHANGED
|
@@ -37,13 +37,17 @@ module Baseh
|
|
|
37
37
|
bytes.pack("C*")
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
# Normative round message, spec 7.3 step 4.
|
|
41
|
-
|
|
40
|
+
# Normative round message, spec 7.3 step 4. In expandable mode the
|
|
41
|
+
# generation's total code length L is mixed in as ASCII decimal plus a
|
|
42
|
+
# 0x00 terminator after the profileId field (spec 7.3/19.4); fixed-mode
|
|
43
|
+
# messages stay byte-for-byte unchanged.
|
|
44
|
+
def round_message(profile_id, round, right, wr, length = nil)
|
|
42
45
|
"".b
|
|
43
46
|
.concat(TAG)
|
|
44
47
|
.concat(0.chr(Encoding::BINARY))
|
|
45
48
|
.concat(profile_id)
|
|
46
49
|
.concat(0.chr(Encoding::BINARY))
|
|
50
|
+
.concat(length.nil? ? "".b : "#{length}\x00".b)
|
|
47
51
|
.concat(round.chr(Encoding::BINARY))
|
|
48
52
|
.concat(to_be(right, (wr + 7) / 8))
|
|
49
53
|
end
|
|
@@ -52,12 +56,12 @@ module Baseh
|
|
|
52
56
|
OpenSSL::HMAC.digest(OpenSSL::Digest.new("sha256"), key_bytes, message)
|
|
53
57
|
end
|
|
54
58
|
|
|
55
|
-
def run_rounds(left, right, profile_id, key_bytes, rounds, w0, w1)
|
|
59
|
+
def run_rounds(left, right, profile_id, key_bytes, rounds, w0, w1, length)
|
|
56
60
|
rounds.times do |i|
|
|
57
61
|
even = i.even?
|
|
58
62
|
wr = even ? w1 : w0
|
|
59
63
|
wl = even ? w0 : w1
|
|
60
|
-
f = low_bits(hmac(key_bytes, round_message(profile_id, i, right, wr)), wl)
|
|
64
|
+
f = low_bits(hmac(key_bytes, round_message(profile_id, i, right, wr, length)), wl)
|
|
61
65
|
new_left = right
|
|
62
66
|
new_right = left ^ f
|
|
63
67
|
left = new_left
|
|
@@ -66,12 +70,12 @@ module Baseh
|
|
|
66
70
|
[left, right]
|
|
67
71
|
end
|
|
68
72
|
|
|
69
|
-
def run_inverse(left, right, profile_id, key_bytes, rounds, w0, w1)
|
|
73
|
+
def run_inverse(left, right, profile_id, key_bytes, rounds, w0, w1, length)
|
|
70
74
|
(rounds - 1).downto(0) do |i|
|
|
71
75
|
even = i.even?
|
|
72
76
|
wr = even ? w1 : w0
|
|
73
77
|
wl = even ? w0 : w1
|
|
74
|
-
f = low_bits(hmac(key_bytes, round_message(profile_id, i, left, wr)), wl)
|
|
78
|
+
f = low_bits(hmac(key_bytes, round_message(profile_id, i, left, wr, length)), wl)
|
|
75
79
|
prev_right = left
|
|
76
80
|
prev_left = right ^ f
|
|
77
81
|
left = prev_left
|
|
@@ -80,21 +84,23 @@ module Baseh
|
|
|
80
84
|
[left, right]
|
|
81
85
|
end
|
|
82
86
|
|
|
83
|
-
# Forward permutation with cycle walking.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
# Forward permutation with cycle walking. length is expandable mode only
|
|
88
|
+
# (spec 7.3/19.4): the generation's total code length mixed into the round
|
|
89
|
+
# message; omit it in fixed mode.
|
|
90
|
+
def permute(value, capacity, profile_id:, key_bytes:, rounds:, length: nil)
|
|
91
|
+
walk(value, capacity, profile_id, key_bytes, rounds, length) do |left, right, w0, w1|
|
|
92
|
+
run_rounds(left, right, profile_id, key_bytes, rounds, w0, w1, length)
|
|
87
93
|
end
|
|
88
94
|
end
|
|
89
95
|
|
|
90
96
|
# Inverse permutation with cycle walking.
|
|
91
|
-
def inverse_permute(value, capacity, profile_id:, key_bytes:, rounds:)
|
|
92
|
-
walk(value, capacity, profile_id, key_bytes, rounds) do |left, right, w0, w1|
|
|
93
|
-
run_inverse(left, right, profile_id, key_bytes, rounds, w0, w1)
|
|
97
|
+
def inverse_permute(value, capacity, profile_id:, key_bytes:, rounds:, length: nil)
|
|
98
|
+
walk(value, capacity, profile_id, key_bytes, rounds, length) do |left, right, w0, w1|
|
|
99
|
+
run_inverse(left, right, profile_id, key_bytes, rounds, w0, w1, length)
|
|
94
100
|
end
|
|
95
101
|
end
|
|
96
102
|
|
|
97
|
-
def walk(value, capacity, profile_id, key_bytes, rounds)
|
|
103
|
+
def walk(value, capacity, profile_id, key_bytes, rounds, length)
|
|
98
104
|
bits = bit_length(capacity)
|
|
99
105
|
w1 = bits / 2
|
|
100
106
|
w0 = bits - w1
|
|
@@ -114,5 +120,12 @@ module Baseh
|
|
|
114
120
|
safe_for_customer: false
|
|
115
121
|
)
|
|
116
122
|
end
|
|
123
|
+
|
|
124
|
+
# Only permute and inverse_permute are public; the round machinery is
|
|
125
|
+
# internal to the spec 7.3 permutation.
|
|
126
|
+
class << self
|
|
127
|
+
private :bit_length, :low_bits, :to_be, :round_message, :hmac,
|
|
128
|
+
:run_rounds, :run_inverse, :walk
|
|
129
|
+
end
|
|
117
130
|
end
|
|
118
131
|
end
|
data/lib/baseh/profile.rb
CHANGED
|
@@ -11,27 +11,111 @@ module Baseh
|
|
|
11
11
|
# key_bytes:, rounds: } or { enabled: false },
|
|
12
12
|
# profanity: { mode: "none" | "no-vowels" | "blocklist",
|
|
13
13
|
# words: [...], extra_words: [...] } (optional, spec 18)
|
|
14
|
+
# max_repetition: 0 (off) or an integer >= 3 (optional, spec 21)
|
|
15
|
+
# short_checksum_length:, short_checksum_until: (expandable only,
|
|
16
|
+
# spec 22; 0 or absent disables)
|
|
14
17
|
module Profile
|
|
15
18
|
ASCII_ONLY = /\A[\x20-\x7e]*\z/.freeze
|
|
16
19
|
|
|
17
20
|
# Immutable, fully validated profile with pre-computed derived values.
|
|
18
21
|
class Prepared
|
|
19
|
-
attr_reader :profile_id, :body_alphabet, :body_length,
|
|
20
|
-
:
|
|
21
|
-
:
|
|
22
|
-
:
|
|
22
|
+
attr_reader :profile_id, :mode, :body_alphabet, :body_length,
|
|
23
|
+
:min_length, :checksum_alphabet, :checksum_length,
|
|
24
|
+
:short_checksum_length, :short_checksum_until,
|
|
25
|
+
:case_sensitive, :separator, :separator_min_length,
|
|
26
|
+
:grouping, :aliases, :permutation,
|
|
27
|
+
:capacity, :checksum_modulus, :profanity_mode, :blocklist,
|
|
28
|
+
:max_repetition
|
|
23
29
|
|
|
24
30
|
def initialize(profile)
|
|
25
31
|
validate_type!(profile)
|
|
26
32
|
@profile_id = validate_profile_id!(profile[:profile_id])
|
|
27
33
|
@case_sensitive = profile[:case_sensitive] == true
|
|
28
34
|
|
|
35
|
+
# Spec 2.2/19.9. A persisted or frozen profile declares its mode;
|
|
36
|
+
# profiles built before the mode field existed are fixed, so the
|
|
37
|
+
# frozen vectors keep matching byte for byte.
|
|
38
|
+
@mode = (profile[:mode] || "fixed").to_s
|
|
39
|
+
unless %w[fixed expandable].include?(@mode)
|
|
40
|
+
self.class.fail_profile!("mode must be fixed or expandable")
|
|
41
|
+
end
|
|
42
|
+
|
|
29
43
|
@body_alphabet = validate_body_alphabet!(profile[:body_alphabet])
|
|
30
|
-
|
|
44
|
+
# Spec 19.2: in expandable mode the zero ban strips 0 and O from the
|
|
45
|
+
# body alphabet silently, before any other validation, exactly like
|
|
46
|
+
# the no-vowels strip of section 18.1.
|
|
47
|
+
if @mode == "expandable"
|
|
48
|
+
@body_alphabet = @body_alphabet.delete("0O").freeze
|
|
49
|
+
end
|
|
50
|
+
@body_length =
|
|
51
|
+
if @mode == "fixed"
|
|
52
|
+
validate_integer!(profile[:body_length], 1, 32, "bodyLength")
|
|
53
|
+
end
|
|
31
54
|
@checksum_length = validate_integer!(profile[:checksum_length], 0, 8, "checksumLength")
|
|
32
|
-
@
|
|
33
|
-
|
|
34
|
-
|
|
55
|
+
@min_length = (profile[:min_length] || 4)
|
|
56
|
+
@separator_min_length = (profile[:separator_min_length] || 0)
|
|
57
|
+
if @mode == "fixed"
|
|
58
|
+
unless @separator_min_length.is_a?(Integer) && @separator_min_length.zero?
|
|
59
|
+
self.class.fail_profile!("separatorMinLength must be 0 in fixed mode")
|
|
60
|
+
end
|
|
61
|
+
else
|
|
62
|
+
unless @min_length.is_a?(Integer) && @min_length >= 1
|
|
63
|
+
self.class.fail_profile!("minLength must be an integer of at least 1")
|
|
64
|
+
end
|
|
65
|
+
unless @min_length > @checksum_length
|
|
66
|
+
self.class.fail_profile!("minLength must be greater than checksumLength")
|
|
67
|
+
end
|
|
68
|
+
unless @separator_min_length.is_a?(Integer) && @separator_min_length >= 0
|
|
69
|
+
self.class.fail_profile!("separatorMinLength must be an integer of at least 0")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
# Spec 22. The short checksum is expandable-only; 0 or absent turns
|
|
73
|
+
# it off.
|
|
74
|
+
@short_checksum_length = profile[:short_checksum_length] || 0
|
|
75
|
+
@short_checksum_until = profile[:short_checksum_until] || 0
|
|
76
|
+
# Spec 22. The window field is the switch: shortChecksumUntil of 0
|
|
77
|
+
# turns the feature off (the codebase convention, like
|
|
78
|
+
# maxRepetition), and the length field without a window is
|
|
79
|
+
# INVALID_PROFILE. With a window set, a shortChecksumLength of 0 is
|
|
80
|
+
# legal: the window's generations carry no checksum symbols at all.
|
|
81
|
+
if @mode == "fixed"
|
|
82
|
+
unless @short_checksum_length == 0 && @short_checksum_until == 0
|
|
83
|
+
self.class.fail_profile!(
|
|
84
|
+
"shortChecksumLength and shortChecksumUntil are expandable-mode only"
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
elsif @short_checksum_until != 0
|
|
88
|
+
unless @short_checksum_until.is_a?(Integer) && @short_checksum_until >= @min_length
|
|
89
|
+
self.class.fail_profile!("shortChecksumUntil must be an integer of at least minLength")
|
|
90
|
+
end
|
|
91
|
+
# Beyond 8 the window would swallow nearly every practical code,
|
|
92
|
+
# and long codes genuinely want two checksum symbols.
|
|
93
|
+
if @short_checksum_until > 8
|
|
94
|
+
self.class.fail_profile!("shortChecksumUntil must be at most 8")
|
|
95
|
+
end
|
|
96
|
+
unless @short_checksum_length.is_a?(Integer) && @short_checksum_length >= 0 &&
|
|
97
|
+
@short_checksum_length < @checksum_length
|
|
98
|
+
self.class.fail_profile!(
|
|
99
|
+
"shortChecksumLength must be an integer from 0 through checksumLength - 1"
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
unless @min_length > @short_checksum_length
|
|
103
|
+
self.class.fail_profile!("minLength must be greater than shortChecksumLength")
|
|
104
|
+
end
|
|
105
|
+
elsif @short_checksum_length != 0
|
|
106
|
+
self.class.fail_profile!("shortChecksumLength requires shortChecksumUntil")
|
|
107
|
+
end
|
|
108
|
+
# Spec 19.3: in expandable mode the checksum alphabet is derived from
|
|
109
|
+
# the body alphabet after every body strip; the configured
|
|
110
|
+
# checksumAlphabet is not consulted.
|
|
111
|
+
@checksum_alphabet =
|
|
112
|
+
if @mode == "expandable"
|
|
113
|
+
"".freeze
|
|
114
|
+
else
|
|
115
|
+
validate_checksum_alphabet!(
|
|
116
|
+
profile[:checksum_alphabet], @checksum_length
|
|
117
|
+
)
|
|
118
|
+
end
|
|
35
119
|
|
|
36
120
|
# Spec 18: validation happens before the vowel strip so malformed
|
|
37
121
|
# alphabets are reported as such, then no-vowels strips and
|
|
@@ -41,10 +125,20 @@ module Baseh
|
|
|
41
125
|
@body_alphabet = Profanity.strip_vowels(@body_alphabet)
|
|
42
126
|
@checksum_alphabet = Profanity.strip_vowels(@checksum_alphabet)
|
|
43
127
|
validate_stripped!(@body_alphabet, "body")
|
|
44
|
-
|
|
128
|
+
if @mode == "fixed" && @checksum_length.positive?
|
|
129
|
+
validate_stripped!(@checksum_alphabet, "checksum")
|
|
130
|
+
end
|
|
45
131
|
@body_alphabet.freeze
|
|
46
132
|
@checksum_alphabet.freeze
|
|
47
133
|
end
|
|
134
|
+
if @mode == "expandable"
|
|
135
|
+
# Spec 19.3: the checksum alphabet is derived, "0" followed by the
|
|
136
|
+
# body alphabet in order, after every body strip (zero ban,
|
|
137
|
+
# no-vowels) so all downstream rules see the final alphabets. The
|
|
138
|
+
# configured checksumAlphabet is not consulted.
|
|
139
|
+
@checksum_alphabet = ("0" + @body_alphabet).freeze
|
|
140
|
+
validate_stripped!(@body_alphabet, "body")
|
|
141
|
+
end
|
|
48
142
|
@blocklist =
|
|
49
143
|
if @profanity_mode == "blocklist"
|
|
50
144
|
Profanity.effective_blocklist(profile[:profanity]).freeze
|
|
@@ -52,6 +146,14 @@ module Baseh
|
|
|
52
146
|
[].freeze
|
|
53
147
|
end
|
|
54
148
|
|
|
149
|
+
# Spec 21: 0 disables the filter; an active filter needs a floor of
|
|
150
|
+
# 3 — banning pairs (2) would destroy roughly 9% of every generation.
|
|
151
|
+
@max_repetition = profile[:max_repetition] || 0
|
|
152
|
+
unless @max_repetition.is_a?(Integer) && @max_repetition >= 0 &&
|
|
153
|
+
(@max_repetition.zero? || @max_repetition >= 3)
|
|
154
|
+
self.class.fail_profile!("maxRepetition must be 0 (off) or an integer of at least 3")
|
|
155
|
+
end
|
|
156
|
+
|
|
55
157
|
@separator = validate_separator!(
|
|
56
158
|
profile[:separator].to_s, @body_alphabet, @checksum_alphabet
|
|
57
159
|
)
|
|
@@ -63,11 +165,28 @@ module Baseh
|
|
|
63
165
|
)
|
|
64
166
|
@permutation = validate_permutation!(profile[:permutation] || { enabled: false })
|
|
65
167
|
|
|
66
|
-
|
|
168
|
+
# Capacity is the fixed-mode A^bodyLength; meaningless in expandable
|
|
169
|
+
# mode, where Baseh#capacity refuses per spec 12.3.
|
|
170
|
+
@capacity = @body_alphabet.length**(@body_length || 0)
|
|
67
171
|
@checksum_modulus = [@checksum_alphabet.length, 1].max**@checksum_length
|
|
68
172
|
freeze
|
|
69
173
|
end
|
|
70
174
|
|
|
175
|
+
# Spec 22. The checksum length that applies to a generation of the
|
|
176
|
+
# given total length: short_checksum_length at or below
|
|
177
|
+
# short_checksum_until, checksum_length above it (and always in fixed
|
|
178
|
+
# mode). The feature is on exactly when short_checksum_until is
|
|
179
|
+
# non-zero; a short_checksum_length of 0 then means the window's
|
|
180
|
+
# generations carry no checksum symbols at all.
|
|
181
|
+
def effective_checksum_length(length)
|
|
182
|
+
if @mode == "expandable" && @short_checksum_until.positive? &&
|
|
183
|
+
length <= @short_checksum_until
|
|
184
|
+
@short_checksum_length
|
|
185
|
+
else
|
|
186
|
+
@checksum_length
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
71
190
|
private
|
|
72
191
|
|
|
73
192
|
def self.fail_profile!(reason)
|
|
@@ -189,7 +308,13 @@ module Baseh
|
|
|
189
308
|
end
|
|
190
309
|
s_norm = case_sensitive ? src : src.upcase
|
|
191
310
|
t_norm = case_sensitive ? tgt : tgt.upcase
|
|
192
|
-
|
|
311
|
+
# Spec 3.2: an alias must never map two distinct canonical symbols
|
|
312
|
+
# into one value. Fixed mode rejects a canonical alias source
|
|
313
|
+
# outright. In expandable mode the frozen tier (spec 17.1) carries
|
|
314
|
+
# aliases whose sources are canonical body symbols (T, N, W stay in
|
|
315
|
+
# the body alphabet); the canonical symbol wins at normalization,
|
|
316
|
+
# making those entries inert instead of destructive.
|
|
317
|
+
if @mode == "fixed" && canonical.include?(s_norm)
|
|
193
318
|
self.class.fail_profile!("alias source #{src.inspect} is already a canonical symbol")
|
|
194
319
|
end
|
|
195
320
|
unless canonical.include?(t_norm)
|
|
@@ -222,7 +347,13 @@ module Baseh
|
|
|
222
347
|
unless grouping.all? { |g| g.is_a?(Integer) && g >= 1 }
|
|
223
348
|
self.class.fail_profile!("group sizes must be positive integers")
|
|
224
349
|
end
|
|
225
|
-
|
|
350
|
+
if @mode == "expandable"
|
|
351
|
+
# Spec 2.2/19.5: the split is derived from the total length; a
|
|
352
|
+
# configurable pattern no longer exists in expandable mode.
|
|
353
|
+
unless grouping.empty?
|
|
354
|
+
self.class.fail_profile!("grouping must be empty in expandable mode")
|
|
355
|
+
end
|
|
356
|
+
elsif grouping.sum != body_length + checksum_length
|
|
226
357
|
self.class.fail_profile!("group sizes must sum to bodyLength + checksumLength")
|
|
227
358
|
end
|
|
228
359
|
end
|
data/lib/baseh/profiles.rb
CHANGED
|
@@ -6,18 +6,27 @@ module Baseh
|
|
|
6
6
|
# visual ones exactly as the web tools derive them, so the tool capacities
|
|
7
7
|
# match.
|
|
8
8
|
#
|
|
9
|
-
# Minimum 36 symbols, no checksum 2,176,782,336 ids
|
|
10
|
-
# Light 31 symbols,
|
|
11
|
-
# Medium 28 symbols,
|
|
12
|
-
# Heavy 26 symbols,
|
|
9
|
+
# Minimum 36 symbols, no checksum, XXX-XXX 2,176,782,336 ids
|
|
10
|
+
# Light 31 symbols, 2 checksums, XXXX-XXXX 887,503,681 ids
|
|
11
|
+
# Medium 28 symbols, 2 checksums, XXXX-XXXX 481,890,304 ids (default)
|
|
12
|
+
# Heavy 26 symbols, 2 checksums, XXXX-XXXX 308,915,776 ids
|
|
13
13
|
#
|
|
14
|
-
# All four keep the typed O/I/L aliases where possible
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
14
|
+
# All four keep the typed O/I/L aliases where possible, use a hyphen
|
|
15
|
+
# delimiter at the midpoint and run the default profanity blocklist. Every
|
|
16
|
+
# tier permutes with the frozen published key (FROZEN_KEY_BYTES below): the
|
|
17
|
+
# key is public, so the permutation obscures sequence but is not secrecy.
|
|
18
|
+
# The -p variants are identical but permute with caller-supplied key
|
|
19
|
+
# material instead.
|
|
18
20
|
module Profiles
|
|
19
21
|
OIL_ALIASES = { "O" => "0", "I" => "1", "L" => "1" }.freeze
|
|
20
22
|
|
|
23
|
+
# The frozen published permutation key. Public by design: it makes issued
|
|
24
|
+
# codes look non-sequential but offers no secrecy, since anyone can read
|
|
25
|
+
# it here. Never swap it on a live namespace; codes only decode with the
|
|
26
|
+
# key they were issued under. Use the -p variants to supply private key
|
|
27
|
+
# material.
|
|
28
|
+
FROZEN_KEY_BYTES = "baseh-frozen-key-v1".b.freeze
|
|
29
|
+
|
|
21
30
|
# Tier shapes shared by the plain and (-p) keyed helpers. The values are
|
|
22
31
|
# thawed on every build, so each helper returns a fresh mutable profile.
|
|
23
32
|
TIERS = {
|
|
@@ -34,18 +43,18 @@ module Baseh
|
|
|
34
43
|
profile_id: "baseh-light",
|
|
35
44
|
body_alphabet: "0123456789ABCEFGHJKMNPQRSUVWXYZ",
|
|
36
45
|
checksum_alphabet: "234679ACEFGHJKMNPQRUVWXY",
|
|
37
|
-
checksum_length:
|
|
38
|
-
separator: "",
|
|
39
|
-
grouping: [],
|
|
46
|
+
checksum_length: 2,
|
|
47
|
+
separator: "-",
|
|
48
|
+
grouping: [4, 4],
|
|
40
49
|
aliases: { **OIL_ALIASES, "D" => "B", "T" => "P" }
|
|
41
50
|
},
|
|
42
51
|
medium: {
|
|
43
52
|
profile_id: "baseh-medium",
|
|
44
53
|
body_alphabet: "0123456789ACDEFGHJKMPQRUVXYZ",
|
|
45
54
|
checksum_alphabet: "234679ACDEFGHJKMPQRUVXY",
|
|
46
|
-
checksum_length:
|
|
47
|
-
separator: "",
|
|
48
|
-
grouping: [],
|
|
55
|
+
checksum_length: 2,
|
|
56
|
+
separator: "-",
|
|
57
|
+
grouping: [4, 4],
|
|
49
58
|
# B and S are dropped for looking like 8 and 5; since they can never
|
|
50
59
|
# be issued, a typed B is always an 8 and a typed S always a 5.
|
|
51
60
|
aliases: { **OIL_ALIASES, "B" => "8", "S" => "5", "T" => "P",
|
|
@@ -55,9 +64,9 @@ module Baseh
|
|
|
55
64
|
profile_id: "baseh-heavy",
|
|
56
65
|
body_alphabet: "0123456789ABCEFHJKMPQRVXYZ",
|
|
57
66
|
checksum_alphabet: "234679ACEFHJKMPQRUVXY",
|
|
58
|
-
checksum_length:
|
|
59
|
-
separator: "",
|
|
60
|
-
grouping: [],
|
|
67
|
+
checksum_length: 2,
|
|
68
|
+
separator: "-",
|
|
69
|
+
grouping: [4, 4],
|
|
61
70
|
aliases: { **OIL_ALIASES, "D" => "B", "T" => "P", "N" => "M",
|
|
62
71
|
"W" => "V", "S" => "F", "G" => "C" }
|
|
63
72
|
}
|
|
@@ -65,46 +74,80 @@ module Baseh
|
|
|
65
74
|
|
|
66
75
|
module_function
|
|
67
76
|
|
|
77
|
+
# Permutation every plain tier applies, built from the frozen published
|
|
78
|
+
# key.
|
|
79
|
+
def frozen_permutation
|
|
80
|
+
keyed_permutation(FROZEN_KEY_BYTES, "frozen", 8)
|
|
81
|
+
end
|
|
82
|
+
|
|
68
83
|
# Alphanumeric, no safety strips, no checksum, hyphen-delimited XXX-XXX.
|
|
69
84
|
def baseh_minimum_v1
|
|
70
|
-
tier(:minimum,
|
|
85
|
+
tier(:minimum, frozen_permutation, false)
|
|
71
86
|
end
|
|
72
87
|
|
|
73
|
-
# baseh-minimum with
|
|
88
|
+
# baseh-minimum permuted with caller-supplied key material.
|
|
74
89
|
def baseh_minimum_p_v1(key_bytes:, key_id: "default", rounds: 8)
|
|
75
90
|
tier(:minimum, keyed_permutation(key_bytes, key_id, rounds), true)
|
|
76
91
|
end
|
|
77
92
|
|
|
78
|
-
# Visual light plus spoken light,
|
|
93
|
+
# Visual light plus spoken light, two checksum symbols, hyphen-delimited.
|
|
79
94
|
def baseh_light_v1
|
|
80
|
-
tier(:light,
|
|
95
|
+
tier(:light, frozen_permutation, false)
|
|
81
96
|
end
|
|
82
97
|
|
|
83
|
-
# baseh-light with
|
|
98
|
+
# baseh-light permuted with caller-supplied key material.
|
|
84
99
|
def baseh_light_p_v1(key_bytes:, key_id: "default", rounds: 8)
|
|
85
100
|
tier(:light, keyed_permutation(key_bytes, key_id, rounds), true)
|
|
86
101
|
end
|
|
87
102
|
|
|
88
|
-
# Visual medium plus spoken medium,
|
|
103
|
+
# Visual medium plus spoken medium, two checksum symbols, hyphen-delimited. The default.
|
|
89
104
|
def baseh_medium_v1
|
|
90
|
-
tier(:medium,
|
|
105
|
+
tier(:medium, frozen_permutation, false)
|
|
91
106
|
end
|
|
92
107
|
|
|
93
|
-
# baseh-medium with
|
|
108
|
+
# baseh-medium permuted with caller-supplied key material.
|
|
94
109
|
def baseh_medium_p_v1(key_bytes:, key_id: "default", rounds: 8)
|
|
95
110
|
tier(:medium, keyed_permutation(key_bytes, key_id, rounds), true)
|
|
96
111
|
end
|
|
97
112
|
|
|
98
|
-
# Conservative alphabet plus spoken heavy,
|
|
113
|
+
# Conservative alphabet plus spoken heavy, two checksum symbols, hyphen-delimited.
|
|
99
114
|
def baseh_heavy_v1
|
|
100
|
-
tier(:heavy,
|
|
115
|
+
tier(:heavy, frozen_permutation, false)
|
|
101
116
|
end
|
|
102
117
|
|
|
103
|
-
# baseh-heavy with
|
|
118
|
+
# baseh-heavy permuted with caller-supplied key material.
|
|
104
119
|
def baseh_heavy_p_v1(key_bytes:, key_id: "default", rounds: 8)
|
|
105
120
|
tier(:heavy, keyed_permutation(key_bytes, key_id, rounds), true)
|
|
106
121
|
end
|
|
107
122
|
|
|
123
|
+
# Spec 17.1. The expandable recommended default carries the same safety
|
|
124
|
+
# posture as baseh-medium-v1: the medium visual strips (O, I, L, B, S) and
|
|
125
|
+
# the medium spoken strips (T, N, W), so an issued code never emits a
|
|
126
|
+
# visual or spoken confusable. The zero ban of section 19.2 then removes 0
|
|
127
|
+
# (and O, already gone), leaving a 27-symbol body. The checksum alphabet
|
|
128
|
+
# derives as "0" plus the body (28 symbols).
|
|
129
|
+
EXPANDABLE_BODY = "123456789ACDEFGHJKMPQRUVXYZ"
|
|
130
|
+
|
|
131
|
+
# The frozen expandable tier; the recommended starting point for new
|
|
132
|
+
# namespaces. Four characters while the namespace is small, gaining one
|
|
133
|
+
# symbol automatically as issuance climbs past each generation's
|
|
134
|
+
# capacity. The body alphabet is the medium safety set (the full
|
|
135
|
+
# alphanumeric set minus the visual look-alikes O, I, L, B, S and the
|
|
136
|
+
# spoken-confusable T, N, W), with the zero ban of spec 19.2 removing 0
|
|
137
|
+
# (27 symbols); the checksum alphabet derives as "0" plus the body (28
|
|
138
|
+
# symbols). The short checksum of spec 22 is on: one checksum symbol
|
|
139
|
+
# through five characters (27^3 = 19,683 ids at length 4, 27^4 = 531,441
|
|
140
|
+
# at length 5), two from six characters up. The hyphen appears from six
|
|
141
|
+
# characters up, split by the balanced grouping rule of spec 19.5.
|
|
142
|
+
def baseh_expandable_v1
|
|
143
|
+
expandable_tier(frozen_permutation, false)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# baseh-expandable permuted with caller-supplied key material.
|
|
147
|
+
def baseh_expandable_p_v1(key_bytes:, key_id: "default", rounds: 8)
|
|
148
|
+
expandable_tier(keyed_permutation(key_bytes, key_id, rounds), true)
|
|
149
|
+
end
|
|
150
|
+
|
|
108
151
|
# feistel-v1 permutation block for the keyed (p) helpers.
|
|
109
152
|
def keyed_permutation(key_bytes, key_id, rounds)
|
|
110
153
|
{
|
|
@@ -131,9 +174,33 @@ module Baseh
|
|
|
131
174
|
grouping: shape[:grouping].dup,
|
|
132
175
|
aliases: shape[:aliases].dup,
|
|
133
176
|
permutation: permutation,
|
|
134
|
-
profanity: { mode: "blocklist" }
|
|
177
|
+
profanity: { mode: "blocklist" },
|
|
178
|
+
max_repetition: 4
|
|
179
|
+
}
|
|
180
|
+
end
|
|
181
|
+
# Builds a fresh mutable profile for the expandable tier, plain or (-p)
|
|
182
|
+
# keyed. Every call returns new strings, arrays and hashes.
|
|
183
|
+
def expandable_tier(permutation, p_suffix)
|
|
184
|
+
{
|
|
185
|
+
profile_id: "baseh-expandable" + (p_suffix ? "-p" : "") + "-v1",
|
|
186
|
+
mode: "expandable",
|
|
187
|
+
body_alphabet: EXPANDABLE_BODY.dup,
|
|
188
|
+
min_length: 4,
|
|
189
|
+
checksum_alphabet: ("0" + EXPANDABLE_BODY).dup,
|
|
190
|
+
checksum_length: 2,
|
|
191
|
+
short_checksum_length: 1,
|
|
192
|
+
short_checksum_until: 5,
|
|
193
|
+
case_sensitive: false,
|
|
194
|
+
separator: "-",
|
|
195
|
+
separator_min_length: 6,
|
|
196
|
+
grouping: [],
|
|
197
|
+
aliases: { **OIL_ALIASES, "B" => "8", "S" => "5", "T" => "P",
|
|
198
|
+
"N" => "M", "W" => "V" },
|
|
199
|
+
permutation: permutation,
|
|
200
|
+
profanity: { mode: "blocklist" },
|
|
201
|
+
max_repetition: 4
|
|
135
202
|
}
|
|
136
203
|
end
|
|
137
|
-
private_class_method :keyed_permutation, :tier
|
|
204
|
+
private_class_method :frozen_permutation, :keyed_permutation, :tier, :expandable_tier
|
|
138
205
|
end
|
|
139
206
|
end
|
data/lib/baseh/version.rb
CHANGED