jsobfu 0.1.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,144 @@
1
+ require_relative 'ecma_tight'
2
+
3
+ class JSObfu::Obfuscator < JSObfu::ECMANoWhitespaceVisitor
4
+
5
+ # @return [JSObfu::Scope] the scope maintained while walking the ast
6
+ attr_reader :scope
7
+
8
+ # Note: At a high level #renames is not that useful, because var shadowing can
9
+ # cause multiple variables in different contexts to be mapped separately.
10
+ # - joev
11
+
12
+ # @return [Hash] of original var/fn names to our new random neames
13
+ attr_reader :renames
14
+
15
+ # @param opts [Hash] the options hash
16
+ # @option opts [JSObfu::Scope] :scope the optional scope to save vars to
17
+ def initialize(opts={})
18
+ @scope = opts.fetch(:scope, JSObfu::Scope.new)
19
+ @renames = {}
20
+ super()
21
+ end
22
+
23
+ # Maintains a stack of closures that we have visited. This method is called
24
+ # everytime we visit a nested function.
25
+ #
26
+ # Javascript is functionally-scoped, so a function(){} creates its own
27
+ # unique closure. When resolving variables, Javascript looks "up" the
28
+ # closure stack, ending up as a property lookup in the global scope
29
+ # (available as `window` in all browsers)
30
+ #
31
+ # This is changed in newer ES versions, where a `let` keyword has been
32
+ # introduced, which has regular C-style block scoping. We'll ignore this
33
+ # feature since it is not yet widely used.
34
+ def visit_SourceElementsNode(o)
35
+ scope.push!
36
+
37
+ hoister = JSObfu::Hoister.new(parent_scope: scope)
38
+ o.value.each { |x| hoister.accept(x) }
39
+
40
+ hoister.scope.keys.each do |key|
41
+ rename_var(key)
42
+ end
43
+
44
+ ret = super
45
+ ret = hoister.scope_declaration + ret
46
+
47
+ scope.pop!
48
+
49
+ ret
50
+ end
51
+
52
+ def visit_FunctionDeclNode(o)
53
+ o.value = if o.value and o.value.length > 0
54
+ JSObfu::Utils::random_var_encoding(scope.rename_var(o.value))
55
+ else
56
+ if rand(3) != 0
57
+ JSObfu::Utils::random_var_encoding(scope.random_var_name)
58
+ end
59
+ end
60
+
61
+ super
62
+ end
63
+
64
+ def visit_FunctionExprNode(o)
65
+ if o.value != 'function'
66
+ o.value = JSObfu::Utils::random_var_encoding(rename_var(o.value))
67
+ end
68
+
69
+ super
70
+ end
71
+
72
+ # Called whenever a variable is declared.
73
+ def visit_VarDeclNode(o)
74
+ o.name = JSObfu::Utils::random_var_encoding(rename_var(o.name))
75
+
76
+ super
77
+ end
78
+
79
+ # Called whenever a variable is referred to (not declared).
80
+ #
81
+ # If the variable was never added to scope, it is assumed to be a global
82
+ # object (like "document"), and hence will not be obfuscated.
83
+ #
84
+ def visit_ResolveNode(o)
85
+ new_val = rename_var(o.value, :generate => false)
86
+
87
+ if new_val
88
+ o.value = JSObfu::Utils::random_var_encoding(new_val)
89
+ super
90
+ else
91
+ # A global is used, at least obfuscate the lookup
92
+ "window[#{JSObfu::Utils::transform_string(o.value, scope, :quotes => false)}]"
93
+ end
94
+ end
95
+
96
+ # Called on a dot lookup, like X.Y
97
+ def visit_DotAccessorNode(o)
98
+ obf_str = JSObfu::Utils::transform_string(o.accessor, scope, :quotes => false)
99
+ "#{o.value.accept(self)}[(#{obf_str})]"
100
+ end
101
+
102
+ # Called when a parameter is declared. "Shadowed" parameters in the original
103
+ # source are preserved - the randomized name is "shadowed" from the outer scope.
104
+ def visit_ParameterNode(o)
105
+ o.value = JSObfu::Utils::random_var_encoding(rename_var(o.value))
106
+
107
+ super
108
+ end
109
+
110
+ # A property node in an object "{}"
111
+ def visit_PropertyNode(o)
112
+ # if it is a non-alphanumeric property, obfuscate the string's bytes
113
+ if o.name =~ /^[a-zA-Z_][a-zA-Z0-9_]*$/
114
+ o.instance_variable_set :@name, '"'+JSObfu::Utils::random_string_encoding(o.name)+'"'
115
+ end
116
+
117
+ super
118
+ end
119
+
120
+ def visit_NumberNode(o)
121
+ o.value = JSObfu::Utils::transform_number(o.value)
122
+ super
123
+ end
124
+
125
+ def visit_StringNode(o)
126
+ o.value = JSObfu::Utils::transform_string(o.value, scope)
127
+ super
128
+ end
129
+
130
+ def visit_TryNode(o)
131
+ if o.catch_block
132
+ o.instance_variable_set :@catch_var, rename_var(o.catch_var)
133
+ end
134
+ super
135
+ end
136
+
137
+ protected
138
+
139
+ # Assigns the var +var_name+ a new obfuscated name
140
+ def rename_var(var_name, opts={})
141
+ @renames[var_name] = scope.rename_var(var_name, opts)
142
+ end
143
+
144
+ end
@@ -0,0 +1,148 @@
1
+ require_relative 'utils'
2
+ require 'set'
3
+
4
+ # A single Javascript scope, used as a key-value store
5
+ # to maintain uniqueness of members in generated closures.
6
+ # For speed this class is implemented as a subclass of Hash.
7
+ class JSObfu::Scope < Hash
8
+
9
+ # these keywords should never be used as a random var name
10
+ # source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Reserved_Words
11
+ RESERVED_KEYWORDS = %w(
12
+ break case catch continue debugger default delete do else finally
13
+ for function if in instanceof new return switch this throw try
14
+ typeof var void while with class enum export extends import super
15
+ implements interface let package private protected public static yield
16
+ const let
17
+ )
18
+
19
+ # these vars should not be shadowed as they in the exploit code,
20
+ # and generating them would cause problems.
21
+ BUILTIN_VARS = %w(
22
+ String window unescape location chrome document navigator location
23
+ frames ActiveXObject XMLHttpRequest Function eval Object Math CSS
24
+ parent opener event frameElement Error TypeError setTimeout setInterval
25
+ top arguments Array Date
26
+ )
27
+
28
+ # @return [JSObfu::Scope] parent that spawned this scope
29
+ attr_accessor :parent
30
+
31
+ # @return [Hash] mapping old var names to random ones
32
+ attr_accessor :renames
33
+
34
+ # @param [Hash] opts the options hash
35
+ # @option opts [Rex::Exploitation::JSObfu::Scope] :parent an optional parent scope,
36
+ # sometimes necessary to prevent needless var shadowing
37
+ # @option opts [Integer] :min_len minimum length of the var names
38
+ def initialize(opts={})
39
+ @parent = opts[:parent]
40
+ @first_char_set = opts[:first_char_set] || [*'A'..'Z']+[*'a'..'z']+['_', '$']
41
+ @char_set = opts[:first_char_set] || @first_char_set + [*'0'..'9']
42
+ @min_len = opts[:min_len] || 1
43
+ @renames = {}
44
+ end
45
+
46
+ # Generates a unique, "safe" random variable
47
+ # @return [String] a unique random var name that is not a reserved keyword
48
+ def random_var_name
49
+ len = @min_len
50
+ loop do
51
+ text = random_string(len)
52
+ unless has_key?(text) or
53
+ RESERVED_KEYWORDS.include?(text) or
54
+ BUILTIN_VARS.include?(text)
55
+
56
+ self[text] = nil
57
+
58
+ return text
59
+ end
60
+ len += 1
61
+ end
62
+ end
63
+
64
+ # Re-maps your +var_name+ to a unique, random
65
+ # names in the current scope
66
+ #
67
+ # @param var_name [String] the name you want to replace. This
68
+ # name will be remembered in the #renames hash
69
+ # @param opts [Hash] the options hash
70
+ # @option opts [Boolean] :generate if the variable was not
71
+ # explicitly renamed before, in this scope or any parent
72
+ # scope, generate a new random name
73
+ #
74
+ # @return [String] the randomly generated replacement name
75
+ # @return nil if generate=false and +var_name+ was not already replaced
76
+ def rename_var(var_name, opts={})
77
+ return var_name if BUILTIN_VARS.include?(var_name)
78
+
79
+ generate = opts.fetch(:generate, true)
80
+ unresolved = opts.fetch(:unresolved, [])
81
+ renamed = @renames[var_name]
82
+
83
+ if renamed.nil? and parent
84
+ renamed = parent.rename_var(var_name, :generate => false)
85
+ end
86
+
87
+ if renamed.nil? and generate
88
+ @renames[var_name] = random_var_name
89
+ renamed = @renames[var_name]
90
+ end
91
+
92
+ #puts "Mapped #{var_name} => #{renamed}" if renamed
93
+
94
+ renamed
95
+ end
96
+
97
+ # @return [Boolean] scope has members
98
+ def empty?
99
+ self.keys.empty? and (parent.nil? or parent.empty?)
100
+ end
101
+
102
+ # @return [Boolean] scope has no parent
103
+ def top?
104
+ parent.nil?
105
+ end
106
+
107
+ def top
108
+ p = self
109
+ p = p.parent until p.parent.nil?
110
+ p
111
+ end
112
+
113
+ # Check if we've used this var before. This will also check any
114
+ # attached parent scopes (and their parents, recursively)
115
+ #
116
+ # @return [Boolean] whether var is in scope
117
+ def has_key?(key)
118
+ super or (parent and parent.has_key?(key))
119
+ end
120
+
121
+ # replaces this Scope in the "parent" chain with a copy,
122
+ # empties current scope, and returns. Essentially an in-place
123
+ # push operation
124
+ def push!
125
+ replacement = dup
126
+ replacement.parent = @parent
127
+ replacement.renames = @renames
128
+ @renames = {}
129
+ @parent = replacement
130
+ clear
131
+ end
132
+
133
+ # "Consumes" the parent and replaces self with it
134
+ def pop!
135
+ clear
136
+ if @parent
137
+ merge! @parent
138
+ @renames = @parent.renames
139
+ @parent = @parent.parent
140
+ end
141
+ end
142
+
143
+ # @return [String] a random string that can be used as a var
144
+ def random_string(len)
145
+ @first_char_set.sample + (len-1).times.map { @char_set.sample }.join
146
+ end
147
+
148
+ end
@@ -0,0 +1,366 @@
1
+ #
2
+ # Some quick utility functions to minimize dependencies
3
+ #
4
+ module JSObfu::Utils
5
+
6
+ #
7
+ # The maximum length of a string that can be passed through
8
+ # #transform_string without being chopped up into separate
9
+ # expressions and concatenated
10
+ #
11
+ MAX_STRING_CHUNK = 10000
12
+
13
+ ALPHA_CHARSET = ([*'A'..'Z']+[*'a'..'z']).freeze
14
+ ALPHANUMERIC_CHARSET = (ALPHA_CHARSET+[*'0'..'9']).freeze
15
+
16
+ # For escaping special chars in a Javascript quoted string
17
+ JS_ESCAPE_MAP = { '\\' => '\\\\', "\r\n" => '\n', "\n" => '\n', "\r" => '\n', '"' => '\\"', "'" => "\\'" }
18
+
19
+ # Returns a random alphanumeric string of the desired length
20
+ #
21
+ # @param [Integer] len the desired length
22
+ # @return [String] random a-zA-Z0-9 text
23
+ def self.rand_text_alphanumeric(len)
24
+ rand_text(ALPHANUMERIC_CHARSET, len)
25
+ end
26
+
27
+ # Returns a random alpha string of the desired length
28
+ #
29
+ # @param [Integer] len the desired length
30
+ # @return [String] random a-zA-Z text
31
+ def self.rand_text_alpha(len)
32
+ rand_text(ALPHA_CHARSET, len)
33
+ end
34
+
35
+ # Returns a random string of the desired length in the desired charset
36
+ #
37
+ # @param [Array] charset the available chars
38
+ # @param [Integer] len the desired length
39
+ # @return [String] random text
40
+ def self.rand_text(charset, len)
41
+ len.times.map { charset.sample }.join
42
+ end
43
+
44
+ # Encodes the bytes in +str+ as hex literals, each preceded by +delimiter+
45
+ #
46
+ # @param [String] str the string to encode
47
+ # @param [String] delimiter prepended to every hex byte
48
+ # @return [String] hex encoded copy of str
49
+ def self.to_hex(str, delimiter="\\x")
50
+ str.bytes.to_a.map { |byte| delimiter+byte.to_s(16) }.join
51
+ end
52
+
53
+ # @param [String] code a quoted Javascript string
54
+ # @return [String] containing javascript code that wraps +code+ in a
55
+ # call to +eval+. A random eval method is chosen.
56
+ def self.js_eval(code, scope)
57
+ code = '"' + escape_javascript(code) + '"'
58
+ ret_statement = random_string_encoding 'return '
59
+ case rand(7)
60
+ when 0; "window[#{transform_string('eval', scope, :quotes => false)}](#{code})"
61
+ when 1; "[].constructor.constructor(\"#{ret_statement}\"+#{code})()"
62
+ when 2; "(function(){}).constructor('', \"#{ret_statement}\"+#{code})()"
63
+ when 3; "''.constructor.constructor('', \"#{ret_statement}\"+#{code})()"
64
+ when 4; "Function(\"#{random_string_encoding 'eval'}\")()(#{code})"
65
+ when 5; "Function(\"#{ret_statement}\"+#{code})()"
66
+ when 6; "Function()(\"#{ret_statement}\"+#{code})()"
67
+ end + ' '
68
+ end
69
+
70
+ #
71
+ # Convert a number to a random base (decimal, octal, or hexedecimal).
72
+ #
73
+ # Given 10 as input, the possible return values are:
74
+ # "10"
75
+ # "0xa"
76
+ # "012"
77
+ #
78
+ # @param num [Integer] number to convert to random base
79
+ # @return [String] equivalent encoding in a different base
80
+ #
81
+ def self.rand_base(num)
82
+ case rand(3)
83
+ when 0; num.to_s
84
+ when 1; "0%o" % num
85
+ when 2; "0x%x" % num
86
+ end
87
+ end
88
+
89
+ # In Javascript, it is possible to refer to the same var in a couple
90
+ # different ways:
91
+ #
92
+ # var AB = 1;
93
+ # console.log(\u0041\u0042); // prints "1"
94
+ #
95
+ # @return [String] equivalent variable name
96
+ def self.random_var_encoding(var_name)
97
+ if var_name.length < 3 and rand(6) == 0
98
+ to_hex(var_name, "\\u00")
99
+ else
100
+ var_name
101
+ end
102
+ end
103
+
104
+ # Given a Javascript string +str+ with NO escape characters, returns an
105
+ # equivalent string with randomly escaped bytes
106
+ #
107
+ # @return [String] Javascript string with a randomly-selected encoding
108
+ # for every byte
109
+ def self.random_string_encoding(str)
110
+ encoded = ''
111
+ str.unpack("C*") { |c|
112
+ encoded << case rand(3)
113
+ when 0; "\\x%02x"%(c)
114
+ when 1; "\\#{c.to_s(8)}"
115
+ when 2; "\\u%04x"%(c)
116
+ when 3; [c].pack("C")
117
+ end
118
+ }
119
+ encoded
120
+ end
121
+
122
+ # Taken from Rails ActionView: http://api.rubyonrails.org/classes/ActionView/Helpers/JavaScriptHelper.html
123
+ #
124
+ # @return [String] +javascript+ with special chars (newlines, quotes) escaped correctly
125
+ def self.escape_javascript(javascript)
126
+ javascript.gsub(/(\|<\/|\r\n|\342\200\250|\342\200\251|[\n\r"'])/u) {|match| JS_ESCAPE_MAP[match] }
127
+ end
128
+
129
+ #
130
+ # Return a mathematical expression that will evaluate to the given number
131
+ # +num+.
132
+ #
133
+ # +num+ can be a float or an int, but should never be negative.
134
+ #
135
+ def self.transform_number(num)
136
+ case num
137
+ when Fixnum
138
+ if num == 0
139
+ r = rand(10) + 1
140
+ transformed = "('#{JSObfu::Utils.rand_text_alpha(r)}'.length-#{r})"
141
+ elsif num > 0 and num < 10
142
+ # use a random string.length for small numbers
143
+ transformed = "'#{JSObfu::Utils.rand_text_alpha(num)}'.length"
144
+ else
145
+ transformed = "("
146
+ divisor = rand(num) + 1
147
+ a = num / divisor.to_i
148
+ b = num - (a * divisor)
149
+ # recurse half the time for a
150
+ a = (rand(2) == 0) ? transform_number(a) : rand_base(a)
151
+ # recurse half the time for divisor
152
+ divisor = (rand(2) == 0) ? transform_number(divisor) : rand_base(divisor)
153
+ transformed << "#{a}*#{divisor}"
154
+ transformed << "+#{b}"
155
+ transformed << ")"
156
+ end
157
+ when Float
158
+ transformed = "(#{num-num.floor}+#{rand_base(num.floor)})"
159
+ end
160
+
161
+ transformed
162
+ end
163
+
164
+ #
165
+ # Convert a javascript string into something that will generate that string.
166
+ #
167
+ # Randomly calls one of the +transform_string_*+ methods
168
+ #
169
+ # @param str [String] the string to transform
170
+ # @param scope [Scope] the scope to use for variable allocation
171
+ # @param opts [Hash] an optional options hash
172
+ # @option opts :quotes [Boolean] str includes quotation marks (true)
173
+ def self.transform_string(str, scope, opts={})
174
+ includes_quotes = opts.fetch(:quotes, true)
175
+ str = str.dup
176
+ quote = includes_quotes ? str[0,1] : '"'
177
+
178
+ if includes_quotes
179
+ str = str[1,str.length - 2]
180
+ return quote*2 if str.length == 0
181
+ end
182
+
183
+ if str.length > MAX_STRING_CHUNK
184
+ return safe_split(str, :quote => quote).map { |arg| transform_string(arg, scope) }.join('+')
185
+ end
186
+
187
+ case rand(2)
188
+ when 0
189
+ transform_string_split_concat(str, quote, scope)
190
+ when 1
191
+ transform_string_fromCharCode(str)
192
+ end
193
+ end
194
+
195
+ #
196
+ # Split a javascript string, +str+, without breaking escape sequences.
197
+ #
198
+ # The maximum length of each piece of the string is half the total length
199
+ # of the string, ensuring we (almost) always split into at least two
200
+ # pieces. This won't always be true when given a string like "AA\x41",
201
+ # where escape sequences artificially increase the total length (escape
202
+ # sequences are considered a single character).
203
+ #
204
+ # Returns an array of two-element arrays. The zeroeth element is a
205
+ # randomly generated variable name, the first is a piece of the string
206
+ # contained in +quote+s.
207
+ #
208
+ # See #escape_length
209
+ #
210
+ # @param str [String] the String to split
211
+ # @param opts [Hash] the options hash
212
+ # @option opts [String] :quote the quoting character ("|')
213
+ #
214
+ # @return [Array] 2d array series of [[var_name, string], ...]
215
+ #
216
+ def self.safe_split(str, opts={})
217
+ quote = opts.fetch(:quote)
218
+
219
+ parts = []
220
+ max_len = str.length / 2
221
+ while str.length > 0
222
+ len = 0
223
+ loop do
224
+ e_len = escape_length(str[len..-1])
225
+ e_len = 1 if e_len.nil?
226
+ len += e_len
227
+ # if we've reached the end of the string, bail
228
+ break unless str[len]
229
+ break if len > max_len
230
+ # randomize the length of each part
231
+ break if (rand(max_len) == 0)
232
+ end
233
+
234
+ part = str.slice!(0, len)
235
+
236
+ parts.push("#{quote}#{part}#{quote}")
237
+ end
238
+
239
+ parts
240
+ end
241
+
242
+ #
243
+ # Stolen from obfuscatejs.rb
244
+ # Determines the length of an escape sequence
245
+ #
246
+ # @param str [String] the String to check the length on
247
+ # @return [Integer] the length of the character at the head of the string
248
+ #
249
+ def self.escape_length(str)
250
+ esc_len = nil
251
+ if str[0,1] == "\\"
252
+ case str[1,1]
253
+ when "u"; esc_len = 6 # unicode \u1234
254
+ when "x"; esc_len = 4 # hex, \x41
255
+ when /[0-7]/ # octal, \123, \0
256
+ str[1,3] =~ /([0-7]{1,3})/
257
+ if $1.to_i(8) > 255
258
+ str[1,3] =~ /([0-7]{1,2})/
259
+ end
260
+ esc_len = 1 + $1.length
261
+ else; esc_len = 2 # \" \n, etc.
262
+ end
263
+ end
264
+ esc_len
265
+ end
266
+
267
+ #
268
+ # Split a javascript string, +str+, into multiple randomly-ordered parts
269
+ # and return an anonymous javascript function that joins them in the
270
+ # correct order. This method can be called safely on strings containing
271
+ # escape sequences. See #safe_split.
272
+ #
273
+ def self.transform_string_split_concat(str, quote, scope)
274
+ parts = safe_split(str, :quote => quote).map {|s| [scope.random_var_name, s] }
275
+ func = "(function () { var "
276
+ ret = "; return "
277
+ parts.sort { |a,b| rand }.each do |part|
278
+ func << "#{part[0]}=#{part[1]},"
279
+ end
280
+ func.chop!
281
+
282
+ ret << parts.map{|part| part[0]}.join("+")
283
+ final = func + ret + " })()"
284
+
285
+ final
286
+ end
287
+
288
+ #
289
+ # Return a call to String.fromCharCode() with each char of the input as arguments
290
+ #
291
+ # Example:
292
+ # input : "A\n"
293
+ # output: String.fromCharCode(0x41, 10)
294
+ #
295
+ # @param str [String] the String to transform (with no quotes)
296
+ # @return [String] Javascript code that evaluates to #str
297
+ #
298
+ def self.transform_string_fromCharCode(str)
299
+ "String.fromCharCode(#{string_to_bytes(str)})"
300
+ end
301
+
302
+ #
303
+ # Converts a string to a series of byte values
304
+ #
305
+ # @param str [String] the Javascript string to encode (no quotes)
306
+ # @return [String] containing a comma-separated list of byte values
307
+ # with random encodings (decimal/hex/octal)
308
+ #
309
+ def self.string_to_bytes(str)
310
+ len = 0
311
+ bytes = str.unpack("C*")
312
+ encoded_bytes = []
313
+
314
+ while str.length > 0
315
+ if str[0,1] == "\\"
316
+ str.slice!(0,1)
317
+ # then this is an escape sequence and we need to deal with all
318
+ # the special cases
319
+ case str[0,1]
320
+ # For chars that contain their non-escaped selves, step past
321
+ # the backslash and let the rand_base() below decide how to
322
+ # represent the character.
323
+ when '"', "'", "\\", " "
324
+ char = str.slice!(0,1).unpack("C").first
325
+ # For symbolic escapes, use the known value
326
+ when "n"; char = 0x0a; str.slice!(0,1)
327
+ when "t"; char = 0x09; str.slice!(0,1)
328
+ # Lastly, if it's a hex, unicode, or octal escape, pull out the
329
+ # real value and use that
330
+ when "x"
331
+ # Strip the x
332
+ str.slice!(0,1)
333
+ char = str.slice!(0,2).to_i 16
334
+ when "u"
335
+ # This can potentially lose information in the case of
336
+ # characters like \u0041, but since regular ascii is stored
337
+ # as unicode internally, String.fromCharCode(0x41) will be
338
+ # represented as 00 41 in memory anyway, so it shouldn't
339
+ # matter.
340
+ str.slice!(0,1)
341
+ char = str.slice!(0,4).to_i 16
342
+ when /[0-7]/
343
+ # Octals are a bit harder since they are variable width and
344
+ # don't necessarily mean what you might think. For example,
345
+ # "\61" == "1" and "\610" == "10". 610 is a valid octal
346
+ # number, but not a valid ascii character. Javascript will
347
+ # interpreter as much as it can as a char and use the rest
348
+ # as a literal. Boo.
349
+ str =~ /([0-7]{1,3})/
350
+ char = $1.to_i 8
351
+ if char > 255
352
+ str =~ /([0-7]{1,2})/
353
+ char = $1.to_i 8
354
+ end
355
+ str.slice!(0, $1.length)
356
+ end
357
+ else
358
+ char = str.slice!(0,1).unpack("C").first
359
+ end
360
+ encoded_bytes << rand_base(char) if char
361
+ end
362
+
363
+ encoded_bytes.join(',')
364
+ end
365
+
366
+ end