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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 94f6aff78bcde0cd3cd00402ebf39c4ae1967529448e3d3f821329ad2e469e52
4
- data.tar.gz: 34a07e6096e29a16c13adc2e59add378174d8b43796959d76b0e46e38028ebd8
3
+ metadata.gz: 50fba0475e4de9c89299be7eec8a694dd602561b001d42c77d36cb16d654cfd6
4
+ data.tar.gz: 443f35ddbdc837ae6dd851ae426342fb4a3c305f51cda9fdb2dfa0dc1652d25b
5
5
  SHA512:
6
- metadata.gz: bffcc7452c6b6edbb1c4e82da354a925c5804583579e537344260c7b1d0f8145a9fd171adc676982a0dc808cf52f3195cb8b6036c9847b2893438d48a864d462
7
- data.tar.gz: 054e5e5e05415062b40404a5ef0bd231d393840c30169375f6de1257b652b242315b234a14715dad933c3bca584a8ddfeecba070461c28539116e5b239427e2a
6
+ metadata.gz: 1dfad2067b675873e7c2c786180e9641c7172202a5ff3d282b7cb111756e5e2eea1e59a43d94cb29c1b701e1260ddd773c43d82e0fc0601e9128753707b64b16
7
+ data.tar.gz: ae1f469da08ca88d1d1490877a735d12b1e12ac2e754993ae069ff730afa778cd6abd2639651008ca038be5f5171cfe23e706071a4c751ad11f03455090ddaa4
@@ -0,0 +1,283 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EncodedToken
4
+ ##
5
+ # EncodedToken::Cipher
6
+ #
7
+ # This class represents a cipher used by EncodedToken::Encryptor
8
+ # for encryption.
9
+ #
10
+ class Cipher
11
+
12
+ # ======================================================================
13
+ # = Macros
14
+ # ======================================================================
15
+
16
+ include EncodedToken::ErrorMessages
17
+
18
+ # ======================================================================
19
+ # = Constants
20
+ # ======================================================================
21
+
22
+ CIPHER_COUNT = 16
23
+ CIPHER_CHARS = (('0'..'9').to_a + ('a'..'z').to_a + ('A'..'Z').to_a).freeze
24
+ CIPHER_TEXT = CIPHER_CHARS.join.freeze
25
+ CIPHER_INDEX_MAP = CIPHER_TEXT.each_char.with_index.to_h.freeze
26
+ HEX_CHARS = (('a'..'f').to_a + ('A'..'F').to_a).freeze
27
+ HEX_NUMS = ('0'..'9').to_a.freeze
28
+ SPECIAL_CHARS = ['-'].freeze
29
+ HEX_TEXT = (HEX_NUMS + HEX_CHARS + SPECIAL_CHARS).join.freeze
30
+ HEX_INDEX_MAP = HEX_TEXT.each_char.with_index.to_h.freeze
31
+ MAX_CIPHER_COUNT = 60
32
+
33
+ # ======================================================================
34
+ # = Instance Methods
35
+ # ======================================================================
36
+
37
+ ##
38
+ # Initializes a new Cipher instance.
39
+ #
40
+ # @param [Integer] seed
41
+ # the seed for randomization.
42
+ # @param [Date, Time, nil] expiry_date
43
+ # the date when this cipher expires.
44
+ # @param [Integer] cipher_count
45
+ # the number of keys to generate.
46
+ #
47
+ def initialize(seed, expiry_date = nil, cipher_count = EncodedToken.configuration.cipher_count)
48
+ @seed = seed
49
+ @expiry_date = expiry_date
50
+ @cipher_count = cipher_count
51
+
52
+ build_cipher!
53
+ end
54
+
55
+ ##
56
+ # Returns the seed used for this cipher.
57
+ #
58
+ # @return [Integer]
59
+ #
60
+ def seed
61
+ @seed
62
+ end
63
+
64
+ ##
65
+ # Returns a duplicate of the root key.
66
+ #
67
+ # @return [String]
68
+ #
69
+ def root_key
70
+ @root_key.dup
71
+ end
72
+
73
+ ##
74
+ # Sets the root key and resets the rotation.
75
+ #
76
+ # @param [String] new_key
77
+ # the new root key.
78
+ #
79
+ # @return [String]
80
+ # the new root key.
81
+ #
82
+ # @raise [ArgumentError]
83
+ # if the key is invalid.
84
+ #
85
+ def root_key=(new_key)
86
+ fail_with(:invalid_root_key) unless valid_key?(new_key)
87
+ @root_key = new_key
88
+ reset!
89
+ end
90
+
91
+ ##
92
+ # Resets the key rotation to the root key.
93
+ #
94
+ # @return [nil]
95
+ #
96
+ def reset!
97
+ @rotation_key = root_key
98
+ @rotation_keys = keys.dup
99
+
100
+ rotate_keys!(target_key: root_key)
101
+ nil
102
+ end
103
+
104
+ ##
105
+ # Rotates the current key.
106
+ #
107
+ # @param [String, nil] target_key
108
+ # the key to rotate to.
109
+ #
110
+ # @return [String]
111
+ # the new current rotation key.
112
+ #
113
+ # @raise [ArgumentError]
114
+ # if the target key is invalid.
115
+ #
116
+ def rotate_keys!(target_key: nil)
117
+ if target_key
118
+ # noinspection RubyMismatchedArgumentType
119
+ unless valid_key?(target_key)
120
+ fail_with(:invalid_root_key)
121
+ end
122
+ rotation_keys.rotate! until rotation_keys.first == target_key
123
+ else
124
+ rotation_keys.rotate!
125
+ end
126
+
127
+ @rotation_key = rotation_keys.first
128
+ end
129
+
130
+ ##
131
+ # Checks if the given key is valid for this cipher.
132
+ #
133
+ # @param [String] key
134
+ #
135
+ # @return [Boolean]
136
+ #
137
+ def valid_key?(key)
138
+ keys.include?(key)
139
+ end
140
+
141
+ ##
142
+ # Returns the cipher text for the current rotation key.
143
+ #
144
+ # @return [String]
145
+ #
146
+ def text
147
+ key_texts[@rotation_key]
148
+ end
149
+
150
+ ##
151
+ # Returns the index map for the current rotation key.
152
+ #
153
+ # @return [Hash<String, Integer>]
154
+ #
155
+ def text_map
156
+ key_maps[@rotation_key]
157
+ end
158
+
159
+ ##
160
+ # Returns the padding size for the root key.
161
+ #
162
+ # @return [Integer]
163
+ #
164
+ def padding
165
+ @key_paddings[@root_key]
166
+ end
167
+
168
+ ##
169
+ # Returns the hex text used for encryption mapping.
170
+ #
171
+ # @return [String]
172
+ #
173
+ def hex_text
174
+ HEX_TEXT
175
+ end
176
+
177
+ ##
178
+ # Returns the index map for the hex text.
179
+ #
180
+ # @return [Hash<String, Integer>]
181
+ #
182
+ def hex_map
183
+ HEX_INDEX_MAP
184
+ end
185
+
186
+ ##
187
+ # Checks if the cipher has expired.
188
+ #
189
+ # @return [Boolean]
190
+ #
191
+ def expired?
192
+ @expiry_date ? Time.now > @expiry_date : false
193
+ end
194
+
195
+ ##
196
+ # Returns the number of keys in the cipher.
197
+ #
198
+ # @return [Integer]
199
+ #
200
+ def cipher_count
201
+ @cipher_count
202
+ end
203
+
204
+
205
+ ##
206
+ # Returns the current rotation key.
207
+ #
208
+ # @return [String]
209
+ #
210
+ def rotation_key
211
+ @rotation_key
212
+ end
213
+
214
+ ##
215
+ # Returns the map of keys to their respective cipher texts.
216
+ #
217
+ # @return [Hash<String, String>]
218
+ #
219
+ def key_texts
220
+ @key_texts ||= {}
221
+ end
222
+
223
+ ##
224
+ # Returns the map of keys to their respective index maps.
225
+ #
226
+ # @return [Hash<String, Hash>]
227
+ #
228
+ def key_maps
229
+ @key_maps ||= {}
230
+ end
231
+
232
+ ##
233
+ # Returns the map of keys to their respective padding sizes.
234
+ #
235
+ # @return [Hash<String, Integer>]
236
+ #
237
+ def key_paddings
238
+ @key_paddings ||= {}
239
+ end
240
+
241
+ ##
242
+ # Returns the list of keys.
243
+ #
244
+ # @return [Array<String>]
245
+ #
246
+ def keys
247
+ @keys ||= []
248
+ end
249
+
250
+ ##
251
+ # Returns the list of keys in their current rotation order.
252
+ #
253
+ # @return [Array<String>]
254
+ #
255
+ def rotation_keys
256
+ @rotation_keys ||= []
257
+ end
258
+
259
+ # = Private Instance Methods
260
+ # ======================================================================
261
+ #
262
+ private
263
+
264
+ def build_cipher!
265
+ return if keys.any?
266
+
267
+ randomizer = Random.new(seed)
268
+ @keys = CIPHER_CHARS.sample(cipher_count, random: randomizer)
269
+ @root_key = keys.sample(random: randomizer)
270
+ @rotation_key = @root_key
271
+ @rotation_keys = keys.dup
272
+
273
+ rotate_keys! target_key: @rotation_key
274
+
275
+ keys.each do |key|
276
+ key_paddings[key] = randomizer.rand(0..10)
277
+ key_texts[key] = CIPHER_CHARS.sample(HEX_TEXT.size, random: randomizer).join
278
+ key_maps[key] = key_texts[key].each_char.with_index.to_h
279
+ end
280
+ end
281
+
282
+ end # CipherPack
283
+ end
@@ -0,0 +1,289 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EncodedToken
4
+ ##
5
+ # EncodedToken::Configuration
6
+ #
7
+ # This class manages the single configuration instance for EncodedToken.
8
+ #
9
+ class Configuration
10
+
11
+ # ======================================================================
12
+ # = Macros
13
+ # ======================================================================
14
+
15
+ include EncodedToken::ErrorMessages
16
+ include EncodedToken::Utils
17
+
18
+ # ======================================================================
19
+ # = Constants
20
+ # ======================================================================
21
+
22
+ DEFAULT_CIPHER_COUNT = 16
23
+ DEFAULT_MIN_TOKEN_SIZE = 55
24
+ DEFAULT_MAX_INPUT_SIZE = 1000
25
+
26
+ # ======================================================================
27
+ # = Class methods
28
+ # ======================================================================
29
+
30
+ private_class_method :new
31
+
32
+ class << self
33
+
34
+ ##
35
+ # Returns the singleton instance of the Configuration.
36
+ #
37
+ # @return [Configuration]
38
+ #
39
+ def instance
40
+ @instance ||= new
41
+ end
42
+
43
+ ##
44
+ # Checks if the configuration is locked.
45
+ #
46
+ # @return [Boolean]
47
+ #
48
+ def locked?
49
+ instance.locked?
50
+ end
51
+
52
+ ##
53
+ # Checks if the configuration is unlocked.
54
+ #
55
+ # @return [Boolean]
56
+ #
57
+ def unlocked?
58
+ instance.unlocked?
59
+ end
60
+
61
+ ##
62
+ # Locks the configuration to prevent further changes.
63
+ #
64
+ # @return [true]
65
+ # the lock status
66
+ #
67
+ def lock!
68
+ instance.lock!
69
+ end
70
+
71
+ ##
72
+ # Unlocks the configuration.
73
+ #
74
+ # @return [false]
75
+ # the lock status
76
+ #
77
+ def unlock!
78
+ instance.unlock!
79
+ end
80
+
81
+ end # class << self
82
+
83
+ # ======================================================================
84
+ # = Instance Methods
85
+ # ======================================================================
86
+
87
+ def initialize
88
+ @cipher_count = DEFAULT_CIPHER_COUNT
89
+ @expiring_seeds = {}
90
+ @locked = false
91
+ @max_input_size = DEFAULT_MAX_INPUT_SIZE
92
+ @min_token_size = DEFAULT_MIN_TOKEN_SIZE
93
+ end
94
+
95
+ ##
96
+ # Checks if the configuration is locked.
97
+ #
98
+ # @return [Boolean]
99
+ #
100
+ def locked?
101
+ !!@locked
102
+ end
103
+
104
+ ##
105
+ # Checks if the configuration is unlocked.
106
+ #
107
+ # @return [Boolean]
108
+ #
109
+ def unlocked?
110
+ !locked?
111
+ end
112
+
113
+ ##
114
+ # Locks the configuration.
115
+ #
116
+ # @return [true] the lock status
117
+ #
118
+ def lock!
119
+ @locked = true
120
+ end
121
+
122
+ ##
123
+ # Unlocks the configuration.
124
+ #
125
+ # @return [false] the lock status
126
+ #
127
+ def unlock!
128
+ @locked = false
129
+ end
130
+
131
+ ##
132
+ # Adds an expiring seed to the configuration.
133
+ #
134
+ # @param [Integer, String] expiring_seed
135
+ # the seed for the expiring cipher.
136
+ # @param [Date, Time] expiry_date
137
+ # the date when the seed expires.
138
+ # @param [Integer] cipher_count
139
+ # the number of keys for the expiring cipher.
140
+ #
141
+ # @return [Hash]
142
+ # the details of the added expiring seed.
143
+ #
144
+ # @raise [ArgumentError]
145
+ # if the arguments are invalid.
146
+ #
147
+ def add_expiring_seed(expiring_seed, expiry_date, cipher_count = DEFAULT_CIPHER_COUNT)
148
+ assert_configuration_can_change!
149
+
150
+ key = Integer(expiring_seed).abs rescue nil
151
+ c_count = Integer(cipher_count).abs rescue nil
152
+
153
+ if key && valid_date?(expiry_date) && valid_cipher_count?(c_count)
154
+ @expiring_seeds[key] = {expires_on: expiry_date, cipher_count: c_count}
155
+ else
156
+ fail_with(:invalid_expiry_argument)
157
+ end
158
+ end
159
+
160
+ ##
161
+ # Returns the default cipher count.
162
+ #
163
+ # @return [Integer]
164
+ #
165
+ def cipher_count
166
+ @cipher_count
167
+ end
168
+
169
+ ##
170
+ # Sets the default cipher count.
171
+ #
172
+ # @param [Integer] new_count
173
+ # the new cipher count.
174
+ #
175
+ # @raise [ArgumentError, RuntimeError]
176
+ # if the count is invalid or configuration is locked.
177
+ #
178
+ def cipher_count=(new_count)
179
+ assert_configuration_can_change!
180
+
181
+ value = Integer(new_count).abs rescue nil
182
+ fail_with(:invalid_cipher_count) unless valid_cipher_count?(value)
183
+
184
+ @cipher_count = value
185
+ end
186
+
187
+ ##
188
+ # Returns the map of expiring seeds.
189
+ #
190
+ # @return [Hash<Integer, Hash>]
191
+ #
192
+ def expiring_seeds
193
+ @expiring_seeds || {}
194
+ end
195
+
196
+ ##
197
+ # Returns the maximum input size.
198
+ #
199
+ # @return [Integer]
200
+ #
201
+ def max_input_size
202
+ @max_input_size
203
+ end
204
+
205
+ ##
206
+ # Sets the maximum input size.
207
+ #
208
+ # @param [Integer] max_size
209
+ #
210
+ # @raise [ArgumentError, RuntimeError]
211
+ # if the size is invalid or configuration is locked.
212
+ #
213
+ def max_input_size=(max_size)
214
+ assert_configuration_can_change!
215
+
216
+ value = Integer(max_size).abs rescue nil
217
+ fail_with(:invalid_max_input_size) if value.nil? || !value.positive?
218
+
219
+ @max_input_size = value
220
+ end
221
+
222
+ ##
223
+ # Returns the minimum token size.
224
+ #
225
+ # @return [Integer]
226
+ #
227
+ def min_token_size
228
+ @min_token_size
229
+ end
230
+
231
+ ##
232
+ # Sets the minimum token size.
233
+ #
234
+ # @param [Integer] min_size
235
+ #
236
+ # @raise [ArgumentError, RuntimeError]
237
+ # if the size is invalid or configuration is locked.
238
+ #
239
+ def min_token_size=(min_size)
240
+ assert_configuration_can_change!
241
+
242
+ value = Integer(min_size).abs rescue nil
243
+ fail_with(:invalid_min_token_size) if value.nil? || !value.positive?
244
+
245
+ @min_token_size = value
246
+ end
247
+
248
+ ##
249
+ # Returns the master seed.
250
+ #
251
+ # @return [Integer]
252
+ #
253
+ def seed
254
+ @seed
255
+ end
256
+
257
+ ##
258
+ # Sets the master seed.
259
+ #
260
+ # @param [Integer] new_seed
261
+ #
262
+ # @raise [ArgumentError, RuntimeError]
263
+ # if the seed is invalid or configuration is locked.
264
+ #
265
+ def seed=(new_seed)
266
+ assert_configuration_can_change!
267
+
268
+ value = Integer(new_seed).abs rescue nil
269
+ fail_with(:invalid_seed) if value.nil?
270
+
271
+ @seed = value
272
+ end
273
+
274
+ # = Private Instance Methods
275
+ # ======================================================================
276
+ #
277
+ private
278
+
279
+ def assert_configuration_can_change!
280
+ fail_with(:no_config_change_allowed) if locked?
281
+ end
282
+
283
+ def valid_cipher_count?(count)
284
+ return true if count.is_a?(Integer) && count >= 8 && count <= Cipher::MAX_CIPHER_COUNT
285
+ fail_with(:invalid_expiry_argument)
286
+ end
287
+
288
+ end # class configuration
289
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EncodedToken
4
+ module Encoder
5
+ ##
6
+ # EncodedToken::Encode::LegacyDecoder
7
+ #
8
+ # This module represents a decoder used by EncodedToken::Encryptor
9
+ # to decode legacy tokens.
10
+ #
11
+ module LegacyDecoder
12
+
13
+ # ======================================================================
14
+ # = Macros
15
+ # ======================================================================
16
+
17
+ extend EncodedToken::Utils
18
+ extend EncodedToken::ErrorMessages
19
+
20
+ # ======================================================================
21
+ # = Singleton Methods
22
+ # ======================================================================
23
+
24
+ class << self
25
+
26
+ ##
27
+ # Decodes the given legacy token.
28
+ #
29
+ # @param [String] token
30
+ # the token to decode.
31
+ #
32
+ # @return [String, nil]
33
+ # the decoded value, or nil if unsuccessful.
34
+ #
35
+ # @raise [ArgumentError]
36
+ # if the token is invalid.
37
+ #
38
+ def decode(token)
39
+ assert_valid_token!(token)
40
+ parse_token(token)
41
+ end
42
+
43
+ # = Private Singleton Methods
44
+ # ======================================================================
45
+ #
46
+ private
47
+
48
+ def assert_valid_token!(token)
49
+ fail_with(:non_string_token) unless token.is_a?(String)
50
+ fail_with(:invalid_token_characters) unless valid_token_text?(token)
51
+ end
52
+
53
+ # rubocop:disable Metrics/MethodLength
54
+ #
55
+ def parse_token(original_token)
56
+ Encoder.decoding_ciphers.each do |cipher|
57
+ begin
58
+ token = original_token.dup
59
+ key = token.slice!(0)
60
+ encryptor = EncodedToken::Encryptor.new(cipher, key) rescue next
61
+
62
+ # get the id size * padding
63
+ id_size = encryptor.decrypt(token.slice!(0, 2)).to_i(16)
64
+ padding = encryptor.padding
65
+
66
+ # remove the padding, get and decrypt the id
67
+ token.slice!(0, padding)
68
+ enc_id = token.slice!(0, id_size)
69
+ id = encryptor.decrypt(enc_id)
70
+
71
+ # next cipher if it's a UUID or numeric ID
72
+ return id if valid_integer?(id) || valid_uuid_format?(id)
73
+
74
+ rescue
75
+ next
76
+ end
77
+ end
78
+
79
+ nil
80
+ end
81
+ # rubocop:enable Metrics/MethodLength
82
+
83
+ end # class << self
84
+ end # module LegacyDecoder
85
+ end # module encoder
86
+ end # module