如何防⽌代码被抄袭,浅谈前端代码加密
说到 Web 前端开发,我们⾸先能够想到的是浏览器、HTML、CSS 以及 JavaScript 这些开发时所必备使⽤的软件⼯具和编程语⾔。⽽在这个专业领域中,作为开发者我们众所周知的是,所有来⾃前端的数据都是“不可信”的,由于构成前端业务逻辑和交互界⾯的所有相关代码都是可以被⽤户直接查看到的,所以我们⽆法保证我们所确信的某个从前端传递到后端的数据没有被⽤户曾经修改过。
那么是否有办法可以将前端领域中那些与业务有关的代码(⽐如数据处理逻辑、验证逻辑等,通常是 JavaScript 代码)进⾏加密以防⽌⽤户进⾏恶意修改呢?本⽂我们将讨论这⽅⾯的内容。
提到“加密”,我们⾃然会想到众多与“对称加密”、“⾮对称加密”以及“散列加密”相关的算法,⽐如 AWS 算法、RSA 算法与 MD5算法等。在传统的 B-S 架构下,前端通过公钥进⾏加密处理的数据可以在后端服务器再通过相应私钥进⾏解密来得到原始数据,但是对于
前端的业务代码⽽⾔,由于浏览器本⾝⽆法识别运⾏这些被加密过的源代码,因此实际上传统的加密算法并不能帮助我们解决“如何完全⿊盒化前端业务逻辑代码”这⼀问题。
既然⽆法完全隐藏前端业务逻辑代码的实际执⾏细节,那我们就从另⼀条路以“降低代码可读性”的⽅式来“伪⿊盒化前端业务逻辑代码”。通常的⽅法有如下⼏种:
第三⽅插件
我们所熟知的可⽤在 Web 前端开发中的第三⽅插件主要有:Adobe Flash、Java Applet 以及 Silverlight 等。由于历史原因这⾥我们不
会深⼊介绍基于这些第三⽅插件的前端业务代码加密⽅案。其中 Adobe 将于 2020 年完全停⽌对 Flash 技术的⽀持,Chrome、Edge 等浏览器也开始逐渐对使⽤了 Flash 程序的 Web 页⾯进⾏阻⽌或弹出相应的警告。同样的,来⾃微软的 Silverlight5 也会在 2021 年停⽌维护,并完全终⽌后续新版本功能的开发。⽽ Java Applet 虽然还可以继续使⽤,但相较于早期上世纪 90 年代末,现在已然很少有⼈使
⽤(不完全统计)。并且需要基于 JRE 来运⾏也使得 Applet 应⽤的运⾏成本⼤⼤提⾼。
代码混淆
在现代前端开发过程中,我们最常⽤的⼀种可以“降低源代码可读性”的⽅法就是使⽤“代码混淆”。通常意义上的代码混淆可以压缩原始ASCII 代码的体积并将其中的诸如变量、常量名⽤简短的毫⽆意义的标识符进⾏代替,这⼀步可以简单地理解为“去语义化”。以我们最常⽤的 “Uglify” 和 “GCC (Google Closure Compiler)” 为例,⾸先是⼀段未经代码混淆的原始 ECMAScript5 源代码:
let times = 0.1 * 8 + 1;
function getExtra(n) {
return [1, 4, 6].map(function(i) {
return i * n;
});
}
var arr = [8, 94, 15, 88, 55, 76, 21, 39];
arr = getExtra(times).concat(arr.map(function(item) {
return item * 2;
}));
function sortarr(arr) {
for(i = 0; i < arr.length - 1; i++) {
for(j = 0; j < arr.length - 1 - i; j++) {
if(arr[j] > arr[j + 1]) {
var temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
return arr;
}
console.log(sortarr(arr));
经过 UglifyJS3 的代码压缩混淆处理后的结果:
let times=1.8;function getExtra(r){return[1,4,6].map(function(t){return t*r})}var arr=[8,94,15,88,55,76,21,39];function sortarr(r){for(i=0;i<r.length-1;i++)for(j=
经过 Google Closure Compiler 的代码压缩混淆处理后的结果:
var b=[8,94,15,88,55,76,21,39];b=function(a){return[1,4,6].map(function(c){return c*a})}(1.8).concat(b.map(function(a){return 2*a}));console.log(function(a
对⽐上述两种⼯具的代码混淆压缩结果我们可以看到,UglifyJS 不会对原始代码进⾏“重写”,所有的压缩⼯作都是在代码原有结构的基
础上进⾏的优化。⽽ GCC 对代码的优化则更靠近“编译器”,除了常见的变量、常量名去语义化外,还使⽤了常见的 DCE 优化策略,⽐
如对常量表达式(constexpr)进⾏提前求值(0.1 * 8 + 1)、通过 “inline” 减少中间变量的使⽤等等。
UglifyJS 在处理优化 JavaScript 源代码时都是以其 AST 的形式进⾏分析的。⽐如在 Node.js 脚本中进⾏源码处理时,我们通常会⾸先使⽤ UglifyJS.parse ⽅法将⼀段 JavaScript 代码转换成其对应的 AST 形式,然后再通过 UglifyJS.Compressor ⽅法对这些 AST 进⾏处理。最后还需要通过print_to_string ⽅法将处理后的 AST 结构转换成相应的 ASCII 可读代码形式。UglifyJS.Compressor 的本质是⼀个官⽅封装好的 “TreeTransformer” 类型,其内部已经封装好了众多常⽤的代码优化策略,⽽通过对 UglifyJS.TreeTransformer 进⾏
适当的封装,我们也可以编写⾃⼰的代码优化器。
如下所⽰我们编写了⼀个实现简单“常量传播”与“常量折叠”(注意这⾥其实是变量,但优化形式同 C++ 中的这两种基本优化策略相同)优化的 UglifyJS 转化器。
const UglifyJS = require('uglify-js');
var symbolTable = {};
var binaryOperations = {
"+": (x, y) => x + y,
"-": (x, y) => x - y,
"*": (x, y) => x * y
}
var constexpr = new UglifyJS.TreeTransformer(null, function(node) {
if (node instanceof UglifyJS.AST_Binary) {
if (Number.isInteger(node.left.value) && Number.isInteger(node.right.value)) {
return new UglifyJS.AST_Number({
value: binaryOperations[node.operator].call(this,
Number(node.left.value),
Number(node.right.value))
});
} else {
return new UglifyJS.AST_Number({
value: binaryOperations[node.operator].call(this,
Number(symbolTable[node.left.name].value),
Number(symbolTable[node.right.name].value))
})
}
}
if (node instanceof UglifyJS.AST_VarDef) {
// AST_VarDef -> AST_SymbolVar;
// 通过符号表来存储已求值的变量值(UglifyJS.AST_Number)引⽤;
symbolTable[node.name.name] = node.value;
}
});
var ast = UglifyJS.parse(`
var x = 10 * 2 + 6;
var y = 4 - 1 * 100;
console.log(x + y);
`);
// transform and print;
console.log(ast.print_to_string());
// output:
/
/ var x=26;var y=-96;console.log(-70);
这⾥我们通过识别特定的 Uglify AST 节点类型(UglifyJS.AST_Binary / UglifyJS.AST_VarDef)来达到对代码进⾏精准处理的⽬的。可以看到,变量 x 和 y 的值在代码处理过程中被提前计算。不仅如此,其作为变量的值还被传递到了表达式 a + b 中,此时如果能够再结合简单的 DCE 策略便可以完成最初级的代码优化效果。类似的,其实通过 Babel 的 @babel/traverse 插件,我们也可以实现同样的效果,其所基于的原理也都⼤同⼩异,即对代码的 AST 进⾏相应的转换和处理。
WebAssembly
关于 Wasm 的基本介绍,这⾥我们不再多谈。那么到底应该如何利⽤ Wasm 的“字节码”特性来做到尽可能地做到“降低 JavaScript 代码可读性”这⼀⽬的呢?⼀个简单的 JavaScript 代码“加密”服务系统架构图如下所⽰:
这⾥整个系统分为两个处理阶段:
第⼀阶段:先将明⽂的 JavaScript 代码转换为基于特定 JavaScript 引擎(VM)的 OpCode 代码,这些⼆进制的 OpCode 代码会再通
过诸如 Base64 等算法的处理⽽转换为经过编码的明⽂ ASCII 字符串格式;
第⼆阶段:将上述经过编码的 ASCII 字符串连同对应的 JavaScript 引擎内核代码统⼀编译成完整的 ASM / Wasm 模块。当模块在⽹页中加载时,内嵌的 JavaScript 引擎便会直接解释执⾏硬编码在模块中的、经过编码处理的 OpCode 代码;
⽐如我们以下⾯这段处于 Top-Level 层的 JavaScript 代码为例:
[1, 2, 3, 5, 6, 7, 8, 9].map(function(i) {
return i * 2;
}).reduce(function(p, i) {
return p + i;
}, 0);
按照正常的 VM 执⾏流程,上述代码在执⾏后会返回计算结果 82。这⾥我们以 JerryScript 这个开源的轻量级 JavaScript 引擎来作为例
⼦,第⼀步⾸先将上述 ASCII 形式的代码 Feed 到该引擎中,然后便可以获得对应该引擎中间状态的 ByteCode 字节码。
WVJSSgAAABYAAAAAAAAAgAAAAAEAAAAYAAEACAAJAAEEAgAAAAAABwAAAGcAAABAAAAAWDIAMhwyAjIBMgUyBDIHMgY6CCAIwAIoAB0AAToARsc
按照我们的架构思路,这部分被编码后的可见字符串会作为“加密”后的源代码被硬编码到包含有 VM 引擎核⼼的 Wasm 模块中。当模块被加载时,VM 会通过相反的顺序解码这段字符串,并得到⼆进制状态的 ByteCode。然后再通过⼀起打包进来的 VM 核⼼来执⾏这些中间状态的⽐。这⾥我们上述所提到的 ByteCode 实际上是以 JerryScript 内部的 SnapShot 快照结构存在于内存中的。
最后这⾥给出上述 Demo 的主要部分源码,详细代码可以参考 Github:
#include "jerryscript.h"
#include "cppcodec/base64_rfc4648.hpp"
#include <iostream>
#include <vector>
#define BUFFER_SIZE 256
#ifdef WASM
#include "emscripten.h"
#endif
std::string encode_code(const jerry_char_t*, size_t);
const unsigned char* transferToUC(const uint32_t* arr, size_t length) {
auto container = std::vector<unsigned char>();
for (size_t x = 0; x < length; x++) {
auto _t = arr[x];
container.push_back(_t >> 24);
container.push_back(_t >> 16);
container.push_back(_t >> 8);
container.push_back(_t);
}
return &container[0];
}
std::vector<uint32_t> transferToU32(const uint8_t* arr, size_t length) {
auto container = std::vector<uint32_t>();
for (size_t x = 0; x < length; x++) {
size_t index = x * 4;
uint32_t y = (arr[index + 0] << 24) | (arr[index + 1] << 16) | (arr[index + 2] << 8) | arr[index + 3];
container.push_back(y);
}
return container;
}
int main (int argc, char** argv) {
const jerry_char_t script_to_snapshot[] = u8R"(
[1, 2, 3, 5, 6, 7, 8, 9].map(function(i) {
return i * 2;
}).reduce(function(p, i) {
return p + i;
}, 0);
)";
std::cout << encode_code(script_to_snapshot, sizeof(script_to_snapshot)) << std::endl;
return 0;
}
std::string encode_code(const jerry_char_t script_to_snapshot[], size_t length) {
using base64 = cppcodec::base64_rfc4648;
// initialize engine;
js代码加密软件
jerry_init(JERRY_INIT_SHOW_OPCODES);
jerry_feature_t feature = JERRY_FEATURE_SNAPSHOT_SAVE;

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。