encoded_token 1.0.2 → 2.0.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/lib/encoded_token/cipher.rb +283 -0
- data/lib/encoded_token/configuration.rb +289 -0
- data/lib/encoded_token/encoder/legacy_decoder.rb +86 -0
- data/lib/encoded_token/encoder/legacy_encoder.rb +96 -0
- data/lib/encoded_token/encoder/utf8_decoder.rb +89 -0
- data/lib/encoded_token/encoder/utf8_encoder.rb +103 -0
- data/lib/encoded_token/encoder.rb +158 -171
- data/lib/encoded_token/encryptor.rb +134 -0
- data/lib/encoded_token/error_messages.rb +101 -0
- data/lib/encoded_token/utils.rb +131 -0
- data/lib/encoded_token/version.rb +11 -13
- data/lib/encoded_token.rb +120 -22
- metadata +17 -14
- data/lib/encoded_token/base.rb +0 -280
- data/lib/encoded_token/decoder.rb +0 -178
data/lib/encoded_token/base.rb
DELETED
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
##
|
|
4
|
-
# EncodedToken::Base
|
|
5
|
-
#
|
|
6
|
-
# The core configuration settings used for encoding and decoding, along
|
|
7
|
-
# with the methods for initialization and verification.
|
|
8
|
-
#
|
|
9
|
-
# Encoding is achived though a variation of Alberti's cipher for
|
|
10
|
-
# multiple Ciphertext Alphabets.
|
|
11
|
-
# (https://en.wikipedia.org/wiki/Alberti_cipher
|
|
12
|
-
#
|
|
13
|
-
class EncodedToken
|
|
14
|
-
module Base
|
|
15
|
-
|
|
16
|
-
# ======================================================================
|
|
17
|
-
# Configuration
|
|
18
|
-
# ======================================================================
|
|
19
|
-
|
|
20
|
-
HEX_NUMS = ('0'..'9').to_a
|
|
21
|
-
HEX_CHARS = ('a'..'f').to_a + ('A'..'F').to_a
|
|
22
|
-
SPECIAL_CHARS = ['-']
|
|
23
|
-
HEX_TEXT = (HEX_NUMS + HEX_CHARS + SPECIAL_CHARS).join # :nodoc:
|
|
24
|
-
|
|
25
|
-
CIPHER_CHARS = ('0'..'9').to_a + ('a'..'z').to_a + ('A'..'Z').to_a
|
|
26
|
-
CIPHER_TEXT = CIPHER_CHARS.join # :nodoc:
|
|
27
|
-
CIPHER_COUNT = 16 # :nodoc:
|
|
28
|
-
TARGET_SIZE = 55 # :nodoc:
|
|
29
|
-
|
|
30
|
-
private_constant :HEX_NUMS, :HEX_CHARS, :SPECIAL_CHARS, :CIPHER_CHARS
|
|
31
|
-
|
|
32
|
-
@@seed = nil
|
|
33
|
-
@@ciphers = nil
|
|
34
|
-
@@keylist = nil
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
# ======================================================================
|
|
39
|
-
# Public Methods
|
|
40
|
-
# ======================================================================
|
|
41
|
-
|
|
42
|
-
##
|
|
43
|
-
# Sets the seed to be used in generating a random encoding
|
|
44
|
-
#
|
|
45
|
-
# [returns:]
|
|
46
|
-
# - true on success
|
|
47
|
-
#
|
|
48
|
-
# [on error:]
|
|
49
|
-
# - raises an exception
|
|
50
|
-
#
|
|
51
|
-
def seed=(new_seed)
|
|
52
|
-
if @@seed
|
|
53
|
-
fail_with_seed_already_set
|
|
54
|
-
else
|
|
55
|
-
@@seed = parse_seed(new_seed)
|
|
56
|
-
generate_ciphers
|
|
57
|
-
return true
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
# ======================================================================
|
|
64
|
-
# Class Private Methods
|
|
65
|
-
# ======================================================================
|
|
66
|
-
private
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
# parse the new seed to ensure it is an integer
|
|
70
|
-
#
|
|
71
|
-
# return the Integer seed on success, otherwise raises an error
|
|
72
|
-
#
|
|
73
|
-
def parse_seed(new_seed)
|
|
74
|
-
if valid_integer?(new_seed)
|
|
75
|
-
return new_seed.to_i.abs
|
|
76
|
-
else
|
|
77
|
-
fail_with_invalid_seed_argument
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
# Generate a set of ciphers
|
|
84
|
-
#
|
|
85
|
-
# returns - a Hash of ciphers with a CIPHER_CHARS character for each key
|
|
86
|
-
#
|
|
87
|
-
def generate_ciphers
|
|
88
|
-
ciphers = {}
|
|
89
|
-
random = Random.new(__seed)
|
|
90
|
-
keys = CIPHER_CHARS.sample(__cipher_count, random: random).sort_by(&:downcase)
|
|
91
|
-
@@keylist = keys
|
|
92
|
-
|
|
93
|
-
# for each key, add a hash of the padding chareacter count
|
|
94
|
-
# and a cipher string to be used for encryption, using a different seed each time
|
|
95
|
-
keys.each_with_index do |key, idx|
|
|
96
|
-
ciphers[key] = {
|
|
97
|
-
padding: random.rand(0..10),
|
|
98
|
-
cipher_text: CIPHER_CHARS.sample(HEX_TEXT.size, random: random).join
|
|
99
|
-
}
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
@@ciphers = ciphers
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
# return the next cypher key after the given key, looping to the first when required
|
|
108
|
-
def rotate_cipher_key(key)
|
|
109
|
-
idx = __keylist.index(key) + 1
|
|
110
|
-
__keylist[idx] || __keylist.first
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
# Validity
|
|
116
|
-
# ======================================================================
|
|
117
|
-
|
|
118
|
-
# checks if the seed had been set
|
|
119
|
-
# - returns true if the seed is set
|
|
120
|
-
# - set the seed if missing and a valid ENV['ENCODED_TOKEN_SEED'] is present
|
|
121
|
-
#
|
|
122
|
-
def assert_valid_seed!
|
|
123
|
-
case
|
|
124
|
-
when !!@@seed
|
|
125
|
-
true
|
|
126
|
-
|
|
127
|
-
when !!ENV['ENCODED_TOKEN_SEED']
|
|
128
|
-
assert_valid_env!
|
|
129
|
-
self.seed = ENV['ENCODED_TOKEN_SEED'].to_i
|
|
130
|
-
|
|
131
|
-
else
|
|
132
|
-
fail RuntimeError, "Encryption seed must be set before using EncodedToken."\
|
|
133
|
-
" Set the seed with EncodedToken.seed=(xxx)."
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
# check ENV['ENCODED_TOKEN_SEED'] is a string integer
|
|
140
|
-
def assert_valid_env!
|
|
141
|
-
begin
|
|
142
|
-
if valid_integer?(ENV['ENCODED_TOKEN_SEED'])
|
|
143
|
-
return true
|
|
144
|
-
else
|
|
145
|
-
fail
|
|
146
|
-
end
|
|
147
|
-
rescue
|
|
148
|
-
fail RuntimeError, "ENV['ENCODED_TOKEN_SEED'] must be a string encoded Integer."
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
# Return true if the given String only contains hex text
|
|
155
|
-
def valid_hex_text?(val)
|
|
156
|
-
(val.chars - __hex_text.chars).empty?
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
# returns true if the given id is is an integer, else false
|
|
162
|
-
#
|
|
163
|
-
# id - and Inetger or String
|
|
164
|
-
#
|
|
165
|
-
def valid_integer?(id)
|
|
166
|
-
sid = id.to_s
|
|
167
|
-
sid.to_i.to_s == sid
|
|
168
|
-
rescue
|
|
169
|
-
false
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
# Return true if the given String only contains cipher text text
|
|
175
|
-
def valid_token_text?(val)
|
|
176
|
-
(val.chars - __cipher_text.chars).empty?
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
# returns true if the given id is a UUID, else false
|
|
182
|
-
#
|
|
183
|
-
# id - String uuid
|
|
184
|
-
#
|
|
185
|
-
def valid_uuid_format?(id)
|
|
186
|
-
uuid_regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
|
|
187
|
-
uuid_regex.match?(id.downcase)
|
|
188
|
-
rescue
|
|
189
|
-
false
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
# Configuration Attributes
|
|
195
|
-
# ======================================================================
|
|
196
|
-
|
|
197
|
-
# return the number of ciphers
|
|
198
|
-
def __cipher_count
|
|
199
|
-
CIPHER_COUNT
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
# return the cipher keylist
|
|
205
|
-
def __keylist
|
|
206
|
-
@@keylist
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
# return the constant cypher text
|
|
212
|
-
def __cipher_text
|
|
213
|
-
CIPHER_TEXT
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
# return the base ciphers hash
|
|
219
|
-
def __ciphers
|
|
220
|
-
@@ciphers
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
# return the constant hex text
|
|
226
|
-
def __hex_text
|
|
227
|
-
HEX_TEXT
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
# return seed
|
|
233
|
-
def __seed
|
|
234
|
-
@@seed
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
# return the target size
|
|
240
|
-
def __target_size
|
|
241
|
-
TARGET_SIZE
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
# Error Messages
|
|
247
|
-
# ======================================================================
|
|
248
|
-
|
|
249
|
-
# error: invalid ID supplied
|
|
250
|
-
def fail_with_invalid_id_argument
|
|
251
|
-
fail_with ArgumentError,
|
|
252
|
-
":id must be an Integer, a String integer or a String UUID."
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
# error: Seed is already set
|
|
258
|
-
def fail_with_seed_already_set
|
|
259
|
-
fail_with ArgumentError,
|
|
260
|
-
"EncodedToken seed has alreay been set to #{@@seed}."
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
# error: invalid Seed supplied
|
|
266
|
-
def fail_with_invalid_seed_argument
|
|
267
|
-
fail_with ArgumentError,
|
|
268
|
-
":seed must be an Integer, preferably with at least 5 digits."
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
# default error message header
|
|
274
|
-
def fail_with(error_klass, message)
|
|
275
|
-
fail error_klass, "\n\nERROR :=> EncodedToken: #{message}\n\n"
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
end #module
|
|
280
|
-
end #class
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
##
|
|
4
|
-
# EncodedToken::Encoder
|
|
5
|
-
#
|
|
6
|
-
# The methods required to decode a token.
|
|
7
|
-
#
|
|
8
|
-
class EncodedToken # :nodoc:
|
|
9
|
-
module Decoder
|
|
10
|
-
|
|
11
|
-
# ======================================================================
|
|
12
|
-
# Public Methods
|
|
13
|
-
# ======================================================================
|
|
14
|
-
|
|
15
|
-
##
|
|
16
|
-
# Decode a previously encoded token to return the original ID
|
|
17
|
-
#
|
|
18
|
-
# [args:]
|
|
19
|
-
# - *token* [String]
|
|
20
|
-
#
|
|
21
|
-
# [returns:]
|
|
22
|
-
# - a String with the original ID
|
|
23
|
-
#
|
|
24
|
-
# [on error:]
|
|
25
|
-
# - an invalid String token returns +nil+
|
|
26
|
-
# - otherwise an exception will be raised
|
|
27
|
-
#
|
|
28
|
-
# *examples:*
|
|
29
|
-
#
|
|
30
|
-
# EncodedToken.decode!("KY3bnaRGmyy6yJS3imWr1dcWtzDYvZjpIAYyCUo5PEKPFvQgtTTed")
|
|
31
|
-
# #=> "12345"
|
|
32
|
-
#
|
|
33
|
-
# EncodedToken.decode!("3gDwO7r4UJYeBYDBLU94MqjZQm0SToSE29ACDNcw0xf4QusZKxQHJ")
|
|
34
|
-
# #=> "12345"
|
|
35
|
-
#
|
|
36
|
-
# EncodedToken.decode!("pAi1SmpKgFAchh76EoLbYLeXVQmLwmMlH2v1zDVeufioKGr0709Qw")
|
|
37
|
-
# #=> "468a5eeb-0cda-4c99-8dba-6a96c33003e0"
|
|
38
|
-
#
|
|
39
|
-
# EncodedToken.decode!("abcdefghijklmnopqrstuvwxyz")
|
|
40
|
-
# #=> nil
|
|
41
|
-
#
|
|
42
|
-
# EncodedToken.decode!(:test)
|
|
43
|
-
# #=> Token is not a string. (RuntimeError)
|
|
44
|
-
#
|
|
45
|
-
def decode!(token)
|
|
46
|
-
assert_valid_seed!
|
|
47
|
-
|
|
48
|
-
token = sanitize_token(token)
|
|
49
|
-
id = parse_token(token)
|
|
50
|
-
|
|
51
|
-
# is it a UUID or numeric ID
|
|
52
|
-
if valid_integer?(id) || valid_uuid_format?(id)
|
|
53
|
-
return id
|
|
54
|
-
else
|
|
55
|
-
return nil
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
##
|
|
62
|
-
# Decode a previously encoded token to return the original ID
|
|
63
|
-
#
|
|
64
|
-
# [args:]
|
|
65
|
-
# - *token* [String]
|
|
66
|
-
#
|
|
67
|
-
# [returns:]
|
|
68
|
-
# - a String with the original ID
|
|
69
|
-
#
|
|
70
|
-
# [on error:]
|
|
71
|
-
# - returns +nil+
|
|
72
|
-
#
|
|
73
|
-
# *examples:*
|
|
74
|
-
#
|
|
75
|
-
# EncodedToken.decode("KY3bnaRGmyy6yJS3imWr1dcWtzDYvZjpIAYyCUo5PEKPFvQgtTTed")
|
|
76
|
-
# #=> "12345"
|
|
77
|
-
#
|
|
78
|
-
# EncodedToken.decode("3gDwO7r4UJYeBYDBLU94MqjZQm0SToSE29ACDNcw0xf4QusZKxQHJ")
|
|
79
|
-
# #=> "12345"
|
|
80
|
-
#
|
|
81
|
-
# EncodedToken.decode("pAi1SmpKgFAchh76EoLbYLeXVQmLwmMlH2v1zDVeufioKGr0709Qw")
|
|
82
|
-
# #=> "4ef2091f-023b-4af6-9e9f-f46465f897ba"
|
|
83
|
-
#
|
|
84
|
-
# EncodedToken.decode("abcdefghijklmnopqrstuvwxyz")
|
|
85
|
-
# #=> nil
|
|
86
|
-
#
|
|
87
|
-
# EncodedToken.decode(:test)
|
|
88
|
-
# #=> nil
|
|
89
|
-
#
|
|
90
|
-
def decode(id)
|
|
91
|
-
decode!(id)
|
|
92
|
-
rescue
|
|
93
|
-
nil
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
# ======================================================================
|
|
98
|
-
# Private Methods
|
|
99
|
-
# ======================================================================
|
|
100
|
-
#
|
|
101
|
-
private
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
# ensures the given token is valid to decode
|
|
105
|
-
#
|
|
106
|
-
# token [String] - a properly encoded String
|
|
107
|
-
#
|
|
108
|
-
# returns - a String duplicate of the given token
|
|
109
|
-
#
|
|
110
|
-
# on error: - a RuntimeError is raised
|
|
111
|
-
#
|
|
112
|
-
# NOTE: - we return a duplicate so the original is not changed later
|
|
113
|
-
# in the process when shifting segments
|
|
114
|
-
#
|
|
115
|
-
def sanitize_token(token)
|
|
116
|
-
fail 'Token is not a string.' unless token.is_a?(String)
|
|
117
|
-
fail 'Invalid token characters' unless valid_token_text?(token)
|
|
118
|
-
fail 'Invalid token cipher.' unless __keylist.include?(token[0])
|
|
119
|
-
token.dup
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
# Parses the token to retrieve the original ID
|
|
125
|
-
#
|
|
126
|
-
# token [String] - the encoded token
|
|
127
|
-
#
|
|
128
|
-
# returns [String] - the original ID
|
|
129
|
-
#
|
|
130
|
-
def parse_token(token)
|
|
131
|
-
key = token[0]
|
|
132
|
-
id_size = decrypt_size(token[1,2], key)
|
|
133
|
-
padding = __ciphers[key][:padding]
|
|
134
|
-
enc_id = token[padding + 3, id_size]
|
|
135
|
-
|
|
136
|
-
return decrypt(enc_id, key)
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
# returns the Integer size of the id
|
|
142
|
-
#
|
|
143
|
-
# enc_size - the encrypted ID
|
|
144
|
-
# key - the cipher key to use
|
|
145
|
-
#
|
|
146
|
-
def decrypt_size(enc_size, key)
|
|
147
|
-
decrypt(enc_size, key).hex
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
# decrypt the id using the cipher text from the given key.
|
|
153
|
-
# - rotate the cipher every character
|
|
154
|
-
#
|
|
155
|
-
# enc_id - encoded String ID
|
|
156
|
-
# key - base cipher key to use
|
|
157
|
-
#
|
|
158
|
-
# on error - rasies an exception (invalid cipher chars, etc)
|
|
159
|
-
#
|
|
160
|
-
def decrypt(enc_id, key)
|
|
161
|
-
id = ""
|
|
162
|
-
enc_key = key
|
|
163
|
-
|
|
164
|
-
enc_id.each_char do |char|
|
|
165
|
-
enc_key = rotate_cipher_key(enc_key)
|
|
166
|
-
cipher_text = __ciphers[enc_key][:cipher_text]
|
|
167
|
-
|
|
168
|
-
id += __hex_text[cipher_text.index(char)]
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
return id
|
|
172
|
-
rescue
|
|
173
|
-
fail 'Invalid token characters'
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
end #module
|
|
177
|
-
end #class
|
|
178
|
-
|