encoded_token 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|