encoded_token 1.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 +7 -0
- data/lib/encoded_token/base.rb +280 -0
- data/lib/encoded_token/decoder.rb +178 -0
- data/lib/encoded_token/encoder.rb +196 -0
- data/lib/encoded_token/version.rb +33 -0
- data/lib/encoded_token.rb +48 -0
- metadata +53 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 94f6aff78bcde0cd3cd00402ebf39c4ae1967529448e3d3f821329ad2e469e52
|
|
4
|
+
data.tar.gz: 34a07e6096e29a16c13adc2e59add378174d8b43796959d76b0e46e38028ebd8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: bffcc7452c6b6edbb1c4e82da354a925c5804583579e537344260c7b1d0f8145a9fd171adc676982a0dc808cf52f3195cb8b6036c9847b2893438d48a864d462
|
|
7
|
+
data.tar.gz: 054e5e5e05415062b40404a5ef0bd231d393840c30169375f6de1257b652b242315b234a14715dad933c3bca584a8ddfeecba070461c28539116e5b239427e2a
|
|
@@ -0,0 +1,280 @@
|
|
|
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
|
|
@@ -0,0 +1,178 @@
|
|
|
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
|
+
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
##
|
|
4
|
+
# EncodedToken::Encoder
|
|
5
|
+
#
|
|
6
|
+
# The methods required to encode a token with an Integer ID or String UUID.
|
|
7
|
+
#
|
|
8
|
+
class EncodedToken # :nodoc:
|
|
9
|
+
module Encoder
|
|
10
|
+
|
|
11
|
+
# Public Methods
|
|
12
|
+
# ======================================================================
|
|
13
|
+
|
|
14
|
+
##
|
|
15
|
+
# Generates a Secure Token from the given ID
|
|
16
|
+
#
|
|
17
|
+
# [args:]
|
|
18
|
+
# - *id* [Integer, String] - the ID or UUID to encode
|
|
19
|
+
# - eg. 12345, "12345", "468a5eeb-0cda-4c99-8dba-6a96c33003e0"
|
|
20
|
+
#
|
|
21
|
+
# [returns:]
|
|
22
|
+
# - a web-safe, variable length String of alphanumeric characters
|
|
23
|
+
#
|
|
24
|
+
# [on error:]
|
|
25
|
+
# - raises an exception based on the error
|
|
26
|
+
#
|
|
27
|
+
# *examples:*
|
|
28
|
+
#
|
|
29
|
+
# EncodedToken.encode!(12345)
|
|
30
|
+
# # => "KY3bnaRGmyy6yJS3imWr1dcWtzDYvZjpIAYyCUo5PEKPFvQgtTTed"
|
|
31
|
+
#
|
|
32
|
+
# EncodedToken.encode!("12345")
|
|
33
|
+
# # => "3gDwO7r4UJYeBYDBLU94MqjZQm0SToSE29ACDNcw0xf4QusZKxQHJ"
|
|
34
|
+
#
|
|
35
|
+
# EncodedToken.encode!("468a5eeb-0cda-4c99-8dba-6a96c33003e0")
|
|
36
|
+
# # => "pAi1SmpKgFAchh76EoLbYLeXVQmLwmMlH2v1zDVeufioKGr0709Qw"
|
|
37
|
+
#
|
|
38
|
+
# EncodedToken.encode!(:test)
|
|
39
|
+
# # => EncodedToken: :id must be an Integer, a String integer or a String UUID. (RuntimeError)
|
|
40
|
+
#
|
|
41
|
+
def encode!(id)
|
|
42
|
+
assert_valid_seed!
|
|
43
|
+
assert_valid_id!(id)
|
|
44
|
+
generate_token(id)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
##
|
|
50
|
+
# Generates a Secure Token from the given ID
|
|
51
|
+
#
|
|
52
|
+
# [args:]
|
|
53
|
+
# - *id* [Integer, String] - the ID or UUID to encode
|
|
54
|
+
# - eg. 12345, "12345", "468a5eeb-0cda-4c99-8dba-6a96c33003e0"
|
|
55
|
+
#
|
|
56
|
+
# [returns:]
|
|
57
|
+
# - a web-safe, variable length String of alphanumeric characters
|
|
58
|
+
#
|
|
59
|
+
# [on error:]
|
|
60
|
+
# - raises an ArgumentError
|
|
61
|
+
#
|
|
62
|
+
# *examples:*
|
|
63
|
+
#
|
|
64
|
+
# EncodedToken.encode(12345)
|
|
65
|
+
# # => "KY3bnaRGmyy6yJS3imWr1dcWtzDYvZjpIAYyCUo5PEKPFvQgtTTed"
|
|
66
|
+
#
|
|
67
|
+
# EncodedToken.encode("12345")
|
|
68
|
+
# # => "3gDwO7r4UJYeBYDBLU94MqjZQm0SToSE29ACDNcw0xf4QusZKxQHJ"
|
|
69
|
+
#
|
|
70
|
+
# EncodedToken.encode("468a5eeb-0cda-4c99-8dba-6a96c33003e0")
|
|
71
|
+
# # => "pAi1SmpKgFAchh76EoLbYLeXVQmLwmMlH2v1zDVeufioKGr0709Qw"
|
|
72
|
+
#
|
|
73
|
+
# EncodedToken.encode(:test)
|
|
74
|
+
# # => EncodedToken: :id must be an Integer, a String integer or a String UUID. (RuntimeError)
|
|
75
|
+
#
|
|
76
|
+
#
|
|
77
|
+
def encode(id)
|
|
78
|
+
encode!(id)
|
|
79
|
+
rescue ArgumentError
|
|
80
|
+
fail_with_invalid_id_argument
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ======================================================================
|
|
86
|
+
# Class Private Methods
|
|
87
|
+
# ======================================================================
|
|
88
|
+
#
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ensures the given ID is valid to encode
|
|
94
|
+
#
|
|
95
|
+
# id - an Integer, numerical String integer or UUID
|
|
96
|
+
# - max size of 255 characters
|
|
97
|
+
# - contain only hex charatacters + '-'
|
|
98
|
+
#
|
|
99
|
+
# returns - true if valid
|
|
100
|
+
#
|
|
101
|
+
# on error: - an ArgumentError is raised
|
|
102
|
+
#
|
|
103
|
+
def assert_valid_id!(id)
|
|
104
|
+
sid = id.to_s
|
|
105
|
+
|
|
106
|
+
fail if sid.size < 1
|
|
107
|
+
fail if sid.size > 255
|
|
108
|
+
fail unless valid_hex_text?(sid)
|
|
109
|
+
fail unless valid_integer?(id) || valid_uuid_format?(id)
|
|
110
|
+
|
|
111
|
+
return true
|
|
112
|
+
|
|
113
|
+
rescue
|
|
114
|
+
fail_with_invalid_id_argument
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# generates the token
|
|
120
|
+
#
|
|
121
|
+
# id - Integer, String integer or String UUID
|
|
122
|
+
#
|
|
123
|
+
# returns - an alphanumeric String token
|
|
124
|
+
#
|
|
125
|
+
# Note - token comprises [key, id_size, left_padding, enc_id, right_padding]
|
|
126
|
+
#
|
|
127
|
+
def generate_token(id)
|
|
128
|
+
# stringify the id
|
|
129
|
+
sid = id.to_s
|
|
130
|
+
|
|
131
|
+
# select a random cipher key
|
|
132
|
+
token = key = __keylist.sample
|
|
133
|
+
|
|
134
|
+
# encrypt the id size
|
|
135
|
+
token += encrypt_size(sid, key)
|
|
136
|
+
|
|
137
|
+
# generate the left padding
|
|
138
|
+
token += random_characters(__ciphers[key][:padding])
|
|
139
|
+
|
|
140
|
+
# encrypt the id
|
|
141
|
+
token += encrypt(sid, key)
|
|
142
|
+
|
|
143
|
+
# generate right padding
|
|
144
|
+
count = (__target_size - token.size).clamp(0, __target_size)
|
|
145
|
+
token += random_characters(count)
|
|
146
|
+
|
|
147
|
+
# return the new token
|
|
148
|
+
return token
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# return the encrypted size of the id
|
|
154
|
+
#
|
|
155
|
+
# returns a 2-character String
|
|
156
|
+
#
|
|
157
|
+
# note - we convert to hex to allow for strings up to 255 chars
|
|
158
|
+
#
|
|
159
|
+
def encrypt_size(id, key)
|
|
160
|
+
hex_size = id.size.to_s(16).rjust(2, '0')
|
|
161
|
+
|
|
162
|
+
encrypt(hex_size, key)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# encrypt the id using the cipher text from the given key.
|
|
168
|
+
# - rotate the cipher every character to avoid sequential valuies like id: 1000
|
|
169
|
+
#
|
|
170
|
+
def encrypt(id, key)
|
|
171
|
+
enc_id = []
|
|
172
|
+
encipher_key = key
|
|
173
|
+
|
|
174
|
+
id.to_s.each_char do |char|
|
|
175
|
+
encipher_key = rotate_cipher_key(encipher_key)
|
|
176
|
+
cipher_text = __ciphers[encipher_key][:cipher_text]
|
|
177
|
+
|
|
178
|
+
enc_id << cipher_text[__hex_text.index(char)]
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
return enc_id.join
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# generate a String of alphanumeric characters ot the given size
|
|
187
|
+
#
|
|
188
|
+
def random_characters(size)
|
|
189
|
+
SecureRandom.alphanumeric(size)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
end #module
|
|
193
|
+
end #class
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
##
|
|
5
|
+
# EncodedToken version details
|
|
6
|
+
#
|
|
7
|
+
class EncodedToken # :nodoc:
|
|
8
|
+
|
|
9
|
+
##
|
|
10
|
+
# The EncodedToken gem version.
|
|
11
|
+
#
|
|
12
|
+
# [returns:]
|
|
13
|
+
# - the version of the currently loaded EncodedToken as a <tt>Gem::Version</tt>
|
|
14
|
+
#
|
|
15
|
+
def self.gem_version
|
|
16
|
+
Gem::Version.new VERSION::STRING
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
module VERSION # :nodoc: all
|
|
22
|
+
|
|
23
|
+
MAJOR = 1
|
|
24
|
+
MINOR = 0
|
|
25
|
+
TINY = 2
|
|
26
|
+
# MICRO = ''
|
|
27
|
+
|
|
28
|
+
STRING = [MAJOR, MINOR, TINY].compact.join(".")
|
|
29
|
+
# STRING = [MAJOR, MINOR, TINY, MICRO].compact.join(".")
|
|
30
|
+
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
##
|
|
4
|
+
# = EncodedToken
|
|
5
|
+
#
|
|
6
|
+
# Encodes a UUID or numeric ID to produce a Secure Token,
|
|
7
|
+
# then decodes the Secure Token to return the origianl ID.
|
|
8
|
+
#
|
|
9
|
+
# - The given ID is encoded using a substitution cipher, then padded
|
|
10
|
+
# with alphanumeric characters to a random length.
|
|
11
|
+
#
|
|
12
|
+
# - Multiple substituion ciphers are used to improve security.
|
|
13
|
+
#
|
|
14
|
+
# *examples:*
|
|
15
|
+
#
|
|
16
|
+
# EncodedToken.encode(12345)
|
|
17
|
+
# # => "b4ex6AEB62jlBGpVAGNou8iRmD7pnHGHafQlAHB7w0J"
|
|
18
|
+
#
|
|
19
|
+
# EncodedToken.decode("b4ex6AEB62jlBGpVAGNou8iRmD7pnHGHafQlAHB7w0J")
|
|
20
|
+
# # => "12345"
|
|
21
|
+
#
|
|
22
|
+
class EncodedToken
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ======================================================================
|
|
26
|
+
# Macros
|
|
27
|
+
# ======================================================================
|
|
28
|
+
|
|
29
|
+
require "securerandom"
|
|
30
|
+
require_relative "encoded_token/base.rb"
|
|
31
|
+
require_relative "encoded_token/encoder.rb"
|
|
32
|
+
require_relative "encoded_token/decoder.rb"
|
|
33
|
+
|
|
34
|
+
extend EncodedToken::Base
|
|
35
|
+
extend EncodedToken::Encoder
|
|
36
|
+
extend EncodedToken::Decoder
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ======================================================================
|
|
40
|
+
# Public Instance Methods
|
|
41
|
+
# ======================================================================
|
|
42
|
+
|
|
43
|
+
# This is an abstract class, so ensure no instantiation
|
|
44
|
+
def initialize # :nodoc:
|
|
45
|
+
raise NotImplementedError.new("SecureToken is an abstract class and cannot be instantiated.")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
end #class
|
metadata
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: encoded_token
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.2
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- CodeMeister
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2022-10-05 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Stop hitting the DB with every secure-token submission. Encoded Tokens
|
|
14
|
+
have the ID, or UUID, encoded within the token itself - increasing both security
|
|
15
|
+
and performance. Coded in plain Ruby, EncodedToken is framework agnostic.
|
|
16
|
+
email: encoded_token@codemeister.dev
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- lib/encoded_token.rb
|
|
22
|
+
- lib/encoded_token/base.rb
|
|
23
|
+
- lib/encoded_token/decoder.rb
|
|
24
|
+
- lib/encoded_token/encoder.rb
|
|
25
|
+
- lib/encoded_token/version.rb
|
|
26
|
+
homepage: https://github.com/Rubology/encoded_token
|
|
27
|
+
licenses:
|
|
28
|
+
- MIT
|
|
29
|
+
metadata:
|
|
30
|
+
homepage_uri: https://github.com/Rubology/encoded_token
|
|
31
|
+
source_code_uri: https://github.com/Rubology/encoded_token
|
|
32
|
+
changelog_uri: https://github.com/Rubology/encoded_token/blob/master/CHANGELOG.md
|
|
33
|
+
post_install_message:
|
|
34
|
+
rdoc_options: []
|
|
35
|
+
require_paths:
|
|
36
|
+
- lib
|
|
37
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
38
|
+
requirements:
|
|
39
|
+
- - ">="
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: 2.5.0
|
|
42
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
requirements: []
|
|
48
|
+
rubygems_version: 3.3.22
|
|
49
|
+
signing_key:
|
|
50
|
+
specification_version: 4
|
|
51
|
+
summary: A better, more secure and efficient way to manage secure-tokens - by encoding
|
|
52
|
+
the ID, or UUID, within the token itself.
|
|
53
|
+
test_files: []
|