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.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ZjQ5NmUzNTU1Mzk3ODdiZDNlMTkyNTQ5YWNmY2Y2MjJlNDVjNGZkMg==
5
+ data.tar.gz: !binary |-
6
+ MGJhOTA3NDJiMDg4ODY4MDE3NDBkYWRiMGFjNjhhY2FiZjdiYzczZA==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ NDA3NmNiYTI1MjM0ZjFhNWMyYTJjMWFkNTU2YTdhZjg4ODYxNWJlMmQwNjZh
10
+ MWRjMTUzZmUyNDcwMmJmNjYxZDliN2ViZTYyYjA2MWVmZmFkNzIzZGUwZDhj
11
+ ZDFmMDUwZmY2Y2JiMmNjNjU0MzA2ZjRiZTAyOGIwMzYxZjM5YWE=
12
+ data.tar.gz: !binary |-
13
+ ODI4ZTgyOTFkZDBlZTdhZDFkN2M0ODU2NWJkYmMwYjYwZTFiNTc2Yjg1OGEx
14
+ YzdhZTRmNjcwYTNhNzlkNTgxYzU0YjJjODVkNmIwYWE4ZWYwZTk3MTY4NzQ4
15
+ NDM4ODEzODQ4NTI0ODA3NTJhMWQwZjY1NDJlNzA3ZGFlOGQzN2I=
data/lib/jsobfu.rb ADDED
@@ -0,0 +1,73 @@
1
+ require 'rkelly'
2
+
3
+ # The primary class, used to parse and obfuscate Javascript code.
4
+ class JSObfu
5
+
6
+ require_relative 'jsobfu/scope'
7
+ require_relative 'jsobfu/utils'
8
+ require_relative 'jsobfu/ecma_tight'
9
+ require_relative 'jsobfu/hoister'
10
+ require_relative 'jsobfu/obfuscator'
11
+
12
+ # @return [JSObfu::Scope] the global scope
13
+ attr_reader :scope
14
+
15
+ # Saves +code+ for later obfuscation with #obfuscate
16
+ def initialize(code)
17
+ @code = code
18
+ @scope = Scope.new
19
+ end
20
+
21
+ # Add +str+ to the un-obfuscated code.
22
+ # Calling this method after #obfuscate is undefined
23
+ def <<(str)
24
+ @code << str
25
+ end
26
+
27
+ # @return [String] the (possibly obfuscated) code
28
+ def to_s
29
+ @code
30
+ end
31
+
32
+ # @return [RKelly::Nodes::SourceElementsNode] the abstract syntax tree
33
+ def ast
34
+ @ast || parse
35
+ end
36
+
37
+ # Parse and obfuscate
38
+ #
39
+ # @param opts [Hash] the options hash
40
+ # @option opts [Boolean] :strip_whitespace allow whitespace in the output code
41
+ #
42
+ # @return [String] if successful
43
+ def obfuscate(opts={})
44
+ @obfuscator = JSObfu::Obfuscator.new(scope: @scope)
45
+ @code = @obfuscator.accept(ast).to_s
46
+ if opts.fetch(:strip_whitespace, true)
47
+ @code.gsub!(/(^\s+|\s+$)/, '')
48
+ @code.delete!("\n")
49
+ @code.delete!("\r")
50
+ end
51
+ self
52
+ end
53
+
54
+ # Returns the obfuscated name for the variable or function +sym+
55
+ #
56
+ # @param [String] sym the name of the variable or function
57
+ # @return [String] the obfuscated name
58
+ def sym(sym)
59
+ raise RuntimeError, "Must obfuscate before calling #sym" if @obfuscator.nil?
60
+ @obfuscator.renames[sym.to_s]
61
+ end
62
+
63
+ protected
64
+
65
+ #
66
+ # Generate an Abstract Syntax Tree (#ast) for later obfuscation
67
+ #
68
+ def parse
69
+ parser = RKelly::Parser.new
70
+ @ast = parser.parse(@code)
71
+ end
72
+
73
+ end
@@ -0,0 +1,316 @@
1
+ #
2
+ # Implements an ECMAVisitor with minimal whitespace and indentation.
3
+ #
4
+ # Mostly a line-by-line copy of RKelly::Visitors::ECMAVisitor with the
5
+ # whitespace trimmed out.
6
+ #
7
+ class JSObfu::ECMANoWhitespaceVisitor < RKelly::Visitors::ECMAVisitor
8
+
9
+ def visit_ParentheticalNode(o)
10
+ "(#{o.value.accept(self)})"
11
+ end
12
+
13
+ def visit_SourceElementsNode(o)
14
+ o.value.map { |x| "#{x.accept(self)}" }.join
15
+ end
16
+
17
+ def visit_VarStatementNode(o)
18
+ "var #{o.value.map { |x| x.accept(self) }.join(',')};"
19
+ end
20
+
21
+ def visit_ConstStatementNode(o)
22
+ "const #{o.value.map { |x| x.accept(self) }.join(',')};"
23
+ end
24
+
25
+ def visit_VarDeclNode(o)
26
+ "#{o.name}#{o.value ? o.value.accept(self) : nil}"
27
+ end
28
+
29
+ def visit_AssignExprNode(o)
30
+ "=#{o.value.accept(self)}"
31
+ end
32
+
33
+ def visit_NumberNode(o)
34
+ o.value.to_s
35
+ end
36
+
37
+ def visit_ForNode(o)
38
+ init = o.init ? o.init.accept(self) : ';'
39
+ init << ';' unless init.end_with? ';' # make sure it has a ;
40
+ test = o.test ? o.test.accept(self) : ''
41
+ counter = o.counter ? o.counter.accept(self) : ''
42
+ "for(#{init}#{test};#{counter})#{o.value.accept(self)}"
43
+ end
44
+
45
+ def visit_LessNode(o)
46
+ "#{o.left.accept(self)}<#{o.value.accept(self)}"
47
+ end
48
+
49
+ def visit_ResolveNode(o)
50
+ o.value
51
+ end
52
+
53
+ def visit_PostfixNode(o)
54
+ "#{o.operand.accept(self)}#{o.value}"
55
+ end
56
+
57
+ def visit_PrefixNode(o)
58
+ "#{o.value}#{o.operand.accept(self)}"
59
+ end
60
+
61
+ def visit_BlockNode(o)
62
+ "{#{o.value.accept(self)}}"
63
+ end
64
+
65
+ def visit_ExpressionStatementNode(o)
66
+ "#{o.value.accept(self)};"
67
+ end
68
+
69
+ def visit_OpEqualNode(o)
70
+ "#{o.left.accept(self)}=#{o.value.accept(self)}"
71
+ end
72
+
73
+ def visit_FunctionCallNode(o)
74
+ "#{o.value.accept(self)}(#{o.arguments.accept(self)})"
75
+ end
76
+
77
+ def visit_ArgumentsNode(o)
78
+ o.value.map { |x| x.accept(self) }.join(',')
79
+ end
80
+
81
+ def visit_StringNode(o)
82
+ o.value
83
+ end
84
+
85
+ def visit_NullNode(o)
86
+ "null"
87
+ end
88
+
89
+ def visit_FunctionDeclNode(o)
90
+ "function #{o.value}" + function_params_and_body(o)
91
+ end
92
+
93
+ def visit_ParameterNode(o)
94
+ o.value
95
+ end
96
+
97
+ def visit_FunctionBodyNode(o)
98
+ "{#{o.value.accept(self)}}"
99
+ end
100
+
101
+ def visit_BreakNode(o)
102
+ "break" + (o.value ? " #{o.value}" : '') + ';'
103
+ end
104
+
105
+ def visit_ContinueNode(o)
106
+ "continue" + (o.value ? " #{o.value}" : '') + ';'
107
+ end
108
+
109
+ def visit_TrueNode(o)
110
+ "true"
111
+ end
112
+
113
+ def visit_FalseNode(o)
114
+ "false"
115
+ end
116
+
117
+ def visit_EmptyStatementNode(o)
118
+ ';'
119
+ end
120
+
121
+ def visit_RegexpNode(o)
122
+ o.value
123
+ end
124
+
125
+ def visit_DotAccessorNode(o)
126
+ "#{o.value.accept(self)}.#{o.accessor}"
127
+ end
128
+
129
+ def visit_ThisNode(o)
130
+ "this"
131
+ end
132
+
133
+ def visit_BitwiseNotNode(o)
134
+ "~#{o.value.accept(self)}"
135
+ end
136
+
137
+ def visit_DeleteNode(o)
138
+ "delete #{o.value.accept(self)}"
139
+ end
140
+
141
+ def visit_ArrayNode(o)
142
+ "[#{o.value.map { |x| x ? x.accept(self) : '' }.join(',')}]"
143
+ end
144
+
145
+ def visit_ElementNode(o)
146
+ o.value.accept(self)
147
+ end
148
+
149
+ def visit_LogicalNotNode(o)
150
+ "!#{o.value.accept(self)}"
151
+ end
152
+
153
+ def visit_UnaryMinusNode(o)
154
+ "-#{o.value.accept(self)}"
155
+ end
156
+
157
+ def visit_UnaryPlusNode(o)
158
+ "+#{o.value.accept(self)}"
159
+ end
160
+
161
+ def visit_ReturnNode(o)
162
+ "return" + (o.value ? " #{o.value.accept(self)}" : '') + ';'
163
+ end
164
+
165
+ def visit_ThrowNode(o)
166
+ "throw #{o.value.accept(self)};"
167
+ end
168
+
169
+ def visit_TypeOfNode(o)
170
+ "typeof #{o.value.accept(self)}"
171
+ end
172
+
173
+ def visit_VoidNode(o)
174
+ "void(#{o.value.accept(self)})"
175
+ end
176
+
177
+ [
178
+ [:Add, '+'],
179
+ [:BitAnd, '&'],
180
+ [:BitOr, '|'],
181
+ [:BitXOr, '^'],
182
+ [:Divide, '/'],
183
+ [:Equal, '=='],
184
+ [:Greater, '>'],
185
+ [:GreaterOrEqual, '>='],
186
+ [:In, 'in'],
187
+ [:InstanceOf, 'instanceof'],
188
+ [:LeftShift, '<<'],
189
+ [:LessOrEqual, '<='],
190
+ [:LogicalAnd, '&&'],
191
+ [:LogicalOr, '||'],
192
+ [:Modulus, '%'],
193
+ [:Multiply, '*'],
194
+ [:NotEqual, '!='],
195
+ [:NotStrictEqual, '!=='],
196
+ [:OpAndEqual, '&='],
197
+ [:OpDivideEqual, '/='],
198
+ [:OpLShiftEqual, '<<='],
199
+ [:OpMinusEqual, '-='],
200
+ [:OpModEqual, '%='],
201
+ [:OpMultiplyEqual, '*='],
202
+ [:OpOrEqual, '|='],
203
+ [:OpPlusEqual, '+='],
204
+ [:OpRShiftEqual, '>>='],
205
+ [:OpURShiftEqual, '>>>='],
206
+ [:OpXOrEqual, '^='],
207
+ [:RightShift, '>>'],
208
+ [:StrictEqual, '==='],
209
+ [:Subtract, '-'],
210
+ [:UnsignedRightShift, '>>>'],
211
+ ].each do |name,op|
212
+ define_method(:"visit_#{name}Node") do |o|
213
+ if op =~ /^[a-z]+$/
214
+ "#{o.left.accept(self)} #{op} #{o.value.accept(self)}"
215
+ else
216
+ "#{o.left.accept(self)}#{op}#{o.value.accept(self)}"
217
+ end
218
+ end
219
+ end
220
+
221
+ def visit_WhileNode(o)
222
+ "while(#{o.left.accept(self)})#{o.value.accept(self)}"
223
+ end
224
+
225
+ def visit_SwitchNode(o)
226
+ "switch(#{o.left.accept(self)})#{o.value.accept(self)}"
227
+ end
228
+
229
+ def visit_CaseBlockNode(o)
230
+ "{" + (o.value ? o.value.map { |x| x.accept(self) }.join('') : '') + "}"
231
+ end
232
+
233
+ def visit_CaseClauseNode(o)
234
+ if o.left
235
+ case_code = "case #{o.left.accept(self)}:"
236
+ else
237
+ case_code = "default:"
238
+ end
239
+ case_code += "#{o.value.accept(self)}"
240
+ case_code
241
+ end
242
+
243
+ def visit_DoWhileNode(o)
244
+ "do#{o.left.accept(self)}while(#{o.value.accept(self)});"
245
+ end
246
+
247
+ def visit_WithNode(o)
248
+ "with(#{o.left.accept(self)})#{o.value.accept(self)}"
249
+ end
250
+
251
+ def visit_LabelNode(o)
252
+ "#{o.name}:#{o.value.accept(self)}"
253
+ end
254
+
255
+ def visit_ObjectLiteralNode(o)
256
+ "{" + o.value.map { |x| x.accept(self) }.join(",\n") + '}'
257
+ end
258
+
259
+ def visit_PropertyNode(o)
260
+ "#{o.name}:#{o.value.accept(self)}"
261
+ end
262
+
263
+ def visit_GetterPropertyNode(o)
264
+ "get #{o.name}" + function_params_and_body(o.value)
265
+ end
266
+
267
+ def visit_SetterPropertyNode(o)
268
+ "set #{o.name}" + function_params_and_body(o.value)
269
+ end
270
+
271
+ def visit_FunctionExprNode(o)
272
+ name = (o.value == 'function') ? '' : ' '+o.value
273
+ "function" + name + function_params_and_body(o)
274
+ end
275
+
276
+ # Helper for all the various function nodes
277
+ def function_params_and_body(o)
278
+ "(#{o.arguments.map { |x| x.accept(self) }.join(',')})" +
279
+ "#{o.function_body.accept(self)}"
280
+ end
281
+
282
+ def visit_CommaNode(o)
283
+ "#{o.left.accept(self)},#{o.value.accept(self)}"
284
+ end
285
+
286
+ def visit_IfNode(o)
287
+ "if(#{o.conditions.accept(self)})#{o.value.accept(self)}" +
288
+ (o.else ? "else #{o.else.accept(self)}" : '')
289
+ end
290
+
291
+ def visit_ConditionalNode(o)
292
+ "#{o.conditions.accept(self)}?#{o.value.accept(self)}:" +
293
+ "#{o.else.accept(self)}"
294
+ end
295
+
296
+ def visit_ForInNode(o)
297
+ var = o.left.is_a?(RKelly::Nodes::VarDeclNode) ? 'var ' : ''
298
+ "for(#{var}#{o.left.accept(self)} in #{o.right.accept(self)})" +
299
+ "#{o.value.accept(self)}"
300
+ end
301
+
302
+ def visit_TryNode(o)
303
+ "try#{o.value.accept(self)}" +
304
+ (o.catch_block ? " catch(#{o.catch_var})#{o.catch_block.accept(self)}" : '') +
305
+ (o.finally_block ? " finally#{o.finally_block.accept(self)}" : '')
306
+ end
307
+
308
+ def visit_BracketAccessorNode(o)
309
+ "#{o.value.accept(self)}[#{o.accessor.accept(self)}]"
310
+ end
311
+
312
+ def visit_NewExprNode(o)
313
+ "new #{o.value.accept(self)}(#{o.arguments.accept(self)})"
314
+ end
315
+
316
+ end
@@ -0,0 +1,84 @@
1
+ #
2
+ # Walks a Javascript AST and finds the immediate members of the
3
+ # root scope, which is useful for "hoisting" var and function
4
+ # declaration to the top of the function.
5
+ #
6
+ class JSObfu::Hoister < RKelly::Visitors::Visitor
7
+
8
+ # @return [Hash] the scope maintained while walking the ast
9
+ attr_reader :scope
10
+
11
+ # @return [Array<String>] the function names in the first level of this closure
12
+ attr_reader :functions
13
+
14
+ def initialize(opts={})
15
+ @parent_scope = opts.fetch(:parent_scope, nil)
16
+ @max_depth = 1
17
+ @depth = 0
18
+ @scope = {}
19
+ @resolves = []
20
+ @functions = []
21
+ super()
22
+ end
23
+
24
+ def visit_SourceElementsNode(o)
25
+ return if @max_depth and @depth >= @max_depth
26
+ @depth += 1
27
+ o.value.each { |x| x.accept(self) }
28
+ @depth -= 1
29
+ end
30
+
31
+ def visit_VarDeclNode(o)
32
+ scope[o.name] = o
33
+ end
34
+
35
+ def visit_FunctionDeclNode(o)
36
+ @functions << o.value
37
+ scope[o.value] = o
38
+ end
39
+
40
+ %w{
41
+ AddNode ArgumentsNode ArrayNode AssignExprNode BitAndNode BitOrNode
42
+ BitXOrNode BitwiseNotNode BlockNode BracketAccessorNode BreakNode
43
+ CaseBlockNode CaseClauseNode CommaNode ConditionalNode
44
+ ConstStatementNode ContinueNode DeleteNode DivideNode
45
+ DoWhileNode DotAccessorNode ElementNode EmptyStatementNode EqualNode
46
+ ExpressionStatementNode FalseNode ForInNode ForNode FunctionBodyNode
47
+ FunctionExprNode GetterPropertyNode GreaterNode GreaterOrEqualNode
48
+ IfNode InNode InstanceOfNode LabelNode LeftShiftNode LessNode
49
+ LessOrEqualNode LogicalAndNode LogicalNotNode LogicalOrNode ModulusNode
50
+ MultiplyNode NewExprNode NotEqualNode NotStrictEqualNode NullNode
51
+ NumberNode ObjectLiteralNode OpAndEqualNode OpDivideEqualNode
52
+ OpEqualNode OpLShiftEqualNode OpMinusEqualNode OpModEqualNode
53
+ OpMultiplyEqualNode OpOrEqualNode OpPlusEqualNode OpRShiftEqualNode
54
+ OpURShiftEqualNode OpXOrEqualNode ParameterNode PostfixNode PrefixNode
55
+ PropertyNode RegexpNode ResolveNode ReturnNode RightShiftNode
56
+ SetterPropertyNode StrictEqualNode StringNode
57
+ SubtractNode SwitchNode ThisNode ThrowNode TrueNode TryNode TypeOfNode
58
+ UnaryMinusNode UnaryPlusNode UnsignedRightShiftNode
59
+ VoidNode WhileNode WithNode
60
+ }.each do |type|
61
+ define_method(:"visit_#{type}") do |o|
62
+ end
63
+ end
64
+
65
+ # @return [String] Javascript that declares the discovered variables
66
+ def scope_declaration(opts={})
67
+ keys = scope.keys.dup
68
+ if opts.fetch(:shuffle, true)
69
+ keys = keys.shuffle
70
+ end
71
+
72
+ keys.delete_if { |k| @functions.include? k }
73
+
74
+ if @parent_scope
75
+ keys.delete_if { |k| @parent_scope.has_key? k }
76
+ keys.map! { |k| @parent_scope.renames[k.to_s] || k }
77
+ end
78
+
79
+ str = if keys.empty? then '' else "var #{keys.join(",")};" end
80
+ # puts str if str.length>0
81
+ str
82
+ end
83
+
84
+ end