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.
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EncodedToken
4
+ module Encoder
5
+ ##
6
+ # EncodedToken::Encode::LegacyEncoder
7
+ #
8
+ # This module represents the encoder used by EncodedToken::Encryptor
9
+ # to encode legacy tokens.
10
+ #
11
+ module LegacyEncoder
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
+ # Encodes the given ID into a legacy token.
28
+ #
29
+ # @param [Integer, String, *.to_s] id
30
+ # the ID to encode.
31
+ #
32
+ # @return [String]
33
+ # the encoded legacy token.
34
+ #
35
+ # @raise [ArgumentError]
36
+ # if the ID is invalid.
37
+ #
38
+ def encode(id)
39
+ assert_valid_id!(id)
40
+ generate_token(id)
41
+ end
42
+
43
+ # = Private Singleton Methods
44
+ # ======================================================================
45
+ #
46
+ private
47
+
48
+ def assert_valid_id!(id)
49
+ sid = id.to_s
50
+ fail if sid.empty? || sid.size > 255
51
+ fail unless valid_hex_text?(sid)
52
+ fail unless valid_integer?(sid) || valid_uuid_format?(sid)
53
+
54
+ true
55
+ rescue StandardError
56
+ fail_with(:invalid_id)
57
+ end
58
+
59
+
60
+ # rubocop:disable Metrics/MethodLength
61
+ #
62
+ def generate_token(id, user_key = nil)
63
+ encryptor = EncodedToken::Encryptor.new(Encoder.encoding_cipher, user_key)
64
+ padding_size = encryptor.padding
65
+ sid = id.to_s
66
+ hex_sid_size = hex_id_size(sid)
67
+
68
+ token = [
69
+ encryptor.key,
70
+ encryptor.encrypt(hex_sid_size),
71
+ random_characters(padding_size),
72
+ encryptor.encrypt(sid)
73
+ ].join
74
+
75
+ pad_to_min_token_size(token)
76
+ end
77
+ # rubocop:enable Metrics/MethodLength
78
+
79
+
80
+ def hex_id_size(sid)
81
+ sid.size.to_s(16).rjust(2, '0')
82
+ end
83
+
84
+
85
+ def pad_to_min_token_size(token)
86
+ return token if token.size >= __min_token_size
87
+
88
+ padding = random_characters(__min_token_size)
89
+ (token + padding).slice(0, __min_token_size)
90
+ end
91
+
92
+ end # class << self
93
+
94
+ end # module LegacyEncoder
95
+ end # module encoder
96
+ end # module
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EncodedToken
4
+ module Encoder
5
+ ##
6
+ # EncodedToken::Encode::Utf8Decoder
7
+ #
8
+ # This module represents the decoder used by EncodedToken::Encryptor
9
+ # to decode UTF-8 tokens.
10
+ #
11
+ module Utf8Decoder
12
+
13
+ # ======================================================================
14
+ # = Macros
15
+ # ======================================================================
16
+
17
+ extend EncodedToken::Utils
18
+ extend EncodedToken::ErrorMessages
19
+
20
+
21
+ # ======================================================================
22
+ # = Singleton Methods
23
+ # ======================================================================
24
+
25
+ class << self
26
+
27
+ ##
28
+ # Decodes the given UTF-8 token.
29
+ #
30
+ # @param [String] token
31
+ # the token to decode.
32
+ #
33
+ # @return [String, nil]
34
+ # the decoded value, or nil if unsuccessful.
35
+ #
36
+ # @raise [ArgumentError]
37
+ # if the token is invalid.
38
+ #
39
+ def decode(token)
40
+ assert_valid_token!(token)
41
+ parse_token(token)
42
+ end
43
+
44
+
45
+ # ======================================================================
46
+ # = Private Singleton Methods
47
+ # ======================================================================
48
+ #
49
+ private
50
+
51
+ def assert_valid_token!(token)
52
+ fail_with(:non_string_token) unless token.is_a?(String)
53
+ fail_with(:invalid_token) unless token[-1] == '_'
54
+ fail_with(:invalid_token_characters) unless valid_token_text?(token[0..-2])
55
+ end
56
+
57
+ # rubocop:disable Metrics
58
+ #
59
+ def parse_token(original_token)
60
+ Encoder.decoding_ciphers.each do |cipher|
61
+ begin
62
+ token = original_token[0..-2]
63
+ key = token.slice!(0)
64
+ encryptor = EncodedToken::Encryptor.new(cipher, key) rescue next
65
+
66
+ hex_token = encryptor.decrypt(token)
67
+ padding_size = encryptor.padding
68
+ hex_token.slice!(0, padding_size)
69
+
70
+ data_size = hex_token.slice!(0, 3).to_i(16)
71
+ hex_data = hex_token.slice!(0, data_size)
72
+ crc_checksum = hex_token.slice!(0, 2).to_i(16)
73
+
74
+ next unless crc_checksum == __crc8_checksum(hex_data)
75
+
76
+ [hex_data].pack("H*").force_encoding("UTF-8")
77
+ rescue
78
+ next
79
+ end.tap { |result| return result if result }
80
+ end # each cipher
81
+ nil
82
+ end
83
+ # rubocop:enable Metrics
84
+
85
+ end # class << self
86
+
87
+ end # module Utf8Decoder
88
+ end # module encoder
89
+ end # module
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EncodedToken
4
+ module Encoder
5
+ ##
6
+ # EncodedToken::Encode::Utf8Encoder
7
+ #
8
+ # This module represents a encoder used by EncodedToken::Encryptor
9
+ # to encode UTF-8 tokens.
10
+ #
11
+ module Utf8Encoder
12
+
13
+ # ======================================================================
14
+ # = Mcros
15
+ # ======================================================================
16
+
17
+ extend EncodedToken::Utils
18
+ extend EncodedToken::ErrorMessages
19
+
20
+
21
+ # ======================================================================
22
+ # = Singleton methods
23
+ # ======================================================================
24
+
25
+ class << self
26
+
27
+ ##
28
+ # Encodes the given input into a UTF-8 token.
29
+ #
30
+ # @param [Integer, String, *.to_s] input
31
+ # the input to encode.
32
+ #
33
+ # @return [String]
34
+ # the encoded UTF-8 token.
35
+ #
36
+ # @raise [ArgumentError]
37
+ # if the input is invalid.
38
+ #
39
+ def encode(input)
40
+ assert_valid_input!(input)
41
+ generate_token(input)
42
+ end
43
+
44
+
45
+ # = Private Singleton Methods
46
+ # ======================================================================
47
+ #
48
+ private
49
+
50
+ def assert_valid_input!(input)
51
+ utf8 = input.to_s.force_encoding("UTF-8")
52
+ fail if utf8.empty? || utf8.size > __max_input_size
53
+
54
+ true
55
+ rescue StandardError
56
+ fail_with(:invalid_utf8_input)
57
+ end
58
+
59
+ # rubocop:disable Metrics
60
+ #
61
+ def generate_token(input, user_key = nil)
62
+ encryptor = EncodedToken::Encryptor.new(Encoder.encoding_cipher, user_key)
63
+ padding_size = encryptor.padding
64
+
65
+ token = [
66
+ random_hex(padding_size),
67
+ hex_input_size(input),
68
+ hex_input(input),
69
+ crc_checksum(input),
70
+ random_hex(padding_size),
71
+ random_hex(rand(20).to_i)
72
+ ].join
73
+
74
+ token = pad_to_min_token_size(token)
75
+
76
+ [encryptor.key, encryptor.encrypt(token), '_'].join
77
+ end
78
+ # rubocop:enable Metrics
79
+
80
+ def hex_input(input)
81
+ input.to_s.force_encoding("UTF-8").unpack1('H*')
82
+ end
83
+
84
+ def hex_input_size(input)
85
+ hex_input(input).size.to_s(16).rjust(3, '0')
86
+ end
87
+
88
+ def crc_checksum(input)
89
+ __crc8_checksum(hex_input(input)).to_s(16).rjust(2, '0')
90
+ end
91
+
92
+ def pad_to_min_token_size(token)
93
+ return token if token.size >= __min_token_size
94
+
95
+ padding = random_hex(__min_token_size)
96
+ (token + padding).slice(0, __min_token_size)
97
+ end
98
+
99
+ end # class << self
100
+
101
+ end # class Utf8Encoder
102
+ end # module encoder
103
+ end # module
@@ -1,196 +1,183 @@
1
1
  # frozen_string_literal: true
2
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:
3
+ module EncodedToken
4
+ ##
5
+ # EncodedToken::Encoder
6
+ #
7
+ # This module handles the encoding and decoding of tokens.
8
+ #
9
9
  module Encoder
10
10
 
11
- # Public Methods
11
+ # ======================================================================
12
+ # = Macros
12
13
  # ======================================================================
13
14
 
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
-
15
+ extend EncodedToken::ErrorMessages
83
16
 
84
17
 
85
18
  # ======================================================================
86
- # Class Private Methods
19
+ # = Singleton Methods
87
20
  # ======================================================================
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
21
 
143
- # generate right padding
144
- count = (__target_size - token.size).clamp(0, __target_size)
145
- token += random_characters(count)
22
+ class << self
146
23
 
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')
24
+ ##
25
+ # Checks if the ciphers have been built.
26
+ #
27
+ # @return [Boolean]
28
+ # true if the encoding cipher is set, false otherwise.
29
+ #
30
+ def ciphers_built?
31
+ !!encoding_cipher
32
+ end
161
33
 
162
- encrypt(hex_size, key)
163
- end
34
+ ##
35
+ # Returns a duplicate of the current encoding cipher.
36
+ #
37
+ # @return [Cipher]
38
+ # the encoding cipher instance.
39
+ #
40
+ def encoding_cipher
41
+ @encoding_cipher.dup
42
+ end
164
43
 
44
+ ##
45
+ # Returns a list of duplicates of the current decoding ciphers.
46
+ #
47
+ # @return [Array<Cipher>]
48
+ # an array of decoding cipher instances.
49
+ #
50
+ def decoding_ciphers
51
+ @decoding_ciphers ||= []
52
+ @decoding_ciphers.map{ |cipher| cipher.dup }
53
+ end
165
54
 
55
+ ##
56
+ # Returns the minimum token size from configuration.
57
+ #
58
+ # @return [Integer]
59
+ # the minimum size for an encoded token.
60
+ #
61
+ def min_token_size
62
+ EncodedToken.configuration.min_token_size
63
+ end
166
64
 
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
65
+ ##
66
+ # Returns the maximum input size from configuration.
67
+ #
68
+ # @return [Integer]
69
+ # the maximum size allowed for input.
70
+ #
71
+ def max_input_size
72
+ EncodedToken.configuration.max_input_size
73
+ end
173
74
 
174
- id.to_s.each_char do |char|
175
- encipher_key = rotate_cipher_key(encipher_key)
176
- cipher_text = __ciphers[encipher_key][:cipher_text]
75
+ ##
76
+ # Encodes a given input to an encoded token.
77
+ #
78
+ # @param [Integer, String, *.to_s] input
79
+ # any object that responds to '.to_s'
80
+ # @param [:utf8, :legacy] encoder
81
+ # the encoder to use.
82
+ #
83
+ # @return [String] if successful
84
+ # @return [nil] if unsuccessful
85
+ #
86
+ def encode(input, encoder = :utf8)
87
+ encode!(input, encoder)
88
+ rescue StandardError
89
+ nil
90
+ end
177
91
 
178
- enc_id << cipher_text[__hex_text.index(char)]
92
+ ##
93
+ # Encodes a given input to an encoded token, raising an exception
94
+ # if encountered.
95
+ #
96
+ # @param [Integer, String, *.to_s] input
97
+ # any object that responds to '.to_s'
98
+ # @param [:utf8, :legacy] encoder
99
+ # the encoder to use.
100
+ #
101
+ # @return [String] the encoded token
102
+ #
103
+ # @raise [ArgumentError, RuntimeError]
104
+ # if the encoding fails or if the seed is not set.
105
+ #
106
+ def encode!(input, encoder = :utf8)
107
+ fail_with(:seed_not_set) unless encoding_cipher
108
+
109
+ case encoder
110
+ when :utf8
111
+ Encoder::Utf8Encoder.encode(input)
112
+ when :legacy
113
+ Encoder::LegacyEncoder.encode(input)
114
+ else
115
+ fail_with(:invalid_encoder)
116
+ end
179
117
  end
180
118
 
181
- return enc_id.join
182
- end
119
+ ##
120
+ # Decodes a given token to the original string.
121
+ #
122
+ # @param [String] token
123
+ # an encoded-token string
124
+ #
125
+ # @return [String]
126
+ # the decoded value if successful
127
+ # @return [nil]
128
+ # if unsuccessful
129
+ #
130
+ def decode(token)
131
+ decode!(token)
132
+ rescue
133
+ nil
134
+ end
183
135
 
136
+ ##
137
+ # Decodes a given token to the original string, raising an exception
138
+ # if encountered.
139
+ #
140
+ # @param [String] token
141
+ # an encoded-token string
142
+ #
143
+ # @return [String]
144
+ # the decoded value
145
+ #
146
+ # @raise [ArgumentError, RuntimeError]
147
+ # if the decoding fails, if the token is not a string, or if the seed is not set.
148
+ #
149
+ def decode!(token)
150
+ fail_with(:seed_not_set) unless encoding_cipher
151
+ fail_with(:non_string_token) unless token.is_a?(String)
152
+
153
+ if token[-1] == '_'
154
+ Utf8Decoder.decode(token)
155
+ else
156
+ LegacyDecoder.decode(token)
157
+ end
158
+ end
184
159
 
160
+ ##
161
+ # Builds the encoding and decoding ciphers based on the current configuration.
162
+ #
163
+ # @return [void]
164
+ #
165
+ # @raise [RuntimeError]
166
+ # if the seed is not set in the configuration.
167
+ #
168
+ def build_ciphers!
169
+ default_seed = EncodedToken.configuration.seed || fail_with(:seed_not_set)
170
+
171
+ @encoding_cipher = Cipher.new(default_seed)
172
+ @decoding_ciphers = [@encoding_cipher]
173
+
174
+ EncodedToken.configuration.expiring_seeds.each do |exp_seed, details|
175
+ cipher = Cipher.new(exp_seed, details[:expires_on], details[:cipher_count])
176
+ @decoding_ciphers << cipher unless cipher.expired?
177
+ end
178
+ end
185
179
 
186
- # generate a String of alphanumeric characters ot the given size
187
- #
188
- def random_characters(size)
189
- SecureRandom.alphanumeric(size)
190
- end
180
+ end # class << self
191
181
 
192
182
  end #module
193
- end #class
194
-
195
-
196
-
183
+ end #EncodedToken