En esta publicación, le mostraré cómo compilar "verdaderamente" el código Node.js (JavaScript) a V8 Bytecode. Esto le permite ocultar o proteger su código fuente de una mejor manera que la ofuscación u otros trucos no muy eficientes (como encriptar su código usando una clave secreta, que se incrustará en los archivos binarios de su aplicación, por eso dije "verdaderamente" arriba).
Entonces, usando la herramienta bytenode, puede distribuir una versión binaria .jsc de sus archivos JavaScript. También puede agrupar todos sus archivos .js usando Browserify, luego compilar ese único archivo en .jsc.
Compruebe el repositorio de bytenode en Github.
Larga historia corta..
[sudo] npm install -g bytenode
bytenode --compile my-file.js my-file.jsc
npm install --save bytenode
const bytenode = require('bytenode');
.jsc
extensión en el sistema de módulos Node.js. Es por eso que lo instalamos localmente también. const myFile = require('./my-file.jsc');
my-file.js
desde la construcción de producción. my-file.js
usando bytenode
clic: bytenode --run my-file.jsc
Ahora ya sabes cómo compilar
.js
archivos, cómo requerir la versión compilada en su código y cómo ejecutar .jsc
archivos desde la terminal. Pasemos a la larga historia.El motor V8 (en el que se basa Node.js) usa lo que se llama: compilación justo a tiempo (JIT), donde el código JavaScript se compila justo antes de la ejecución, luego se optimizará posteriormente.
Empezando desde
Node.js
v5.7.0, el módulo vm introdujo una propiedad llamada produceCachedData
en vm.Script
Función constructora, así que si haces algo como esto: let helloScript = new vm. Script(' console . log ( "Hello World!" ) ;', { produceCachedData: true /* This is required for Node.js < 10.0.0 */ });
Luego, obtenga el código de bytes o
cachedData
buffer: let helloBuffer = helloScript.cachedData; // or in Node.js >= 10 let helloBuffer = helloScript.createCachedData();
helloBuffer
se puede usar para crear un script idéntico que ejecutará las mismas instrucciones cuando se ejecute, pasándolo al vm.Script
Función constructora: let anotherHelloScript = new vm.Script( '' , { produceCachedData : true , cachedData : helloBuffer }); // This will fail!
Pero esto fallará, el motor V8 se quejará del primer argumento (esa cadena vacía
''
), cuando comprueba si es el mismo código que el que se utilizó para generar helloBuffer
tampón en primer lugar. Sin embargo, este proceso de verificación es bastante fácil, lo que importa es la longitud del código. Entonces, esto funcionará: let anotherHelloScript = new vm.Script( ' ' .repeat( 28 ), { produceCachedData : true , cachedData : helloBuffer });
Le damos una cadena vacía con la misma longitud (28) que el código original (
console.log("Hello World!");
) . ¡Eso es todo!Esto es interesante, utilizando el búfer almacenado en caché y la longitud del código original, pudimos crear un script idéntico. Ambos scripts se pueden ejecutar usando
.runInThisContext();
función. Entonces, si los ejecutó: helloScript.runInThisContext(); anotherHelloScript.runInThisContext();
verás '¡Hola Mundo!' dos veces.
(Tenga en cuenta que si ha utilizado la longitud incorrecta o si ha utilizado otra versión de Node.js/V8:
anotherHelloScript
no se ejecutará, y su propiedad cachedDataRejected
se establecerá en true
).Ahora a nuestro último paso, cuando definimos
anotherHelloScript
usamos un valor codificado (28) como la longitud de nuestro código. ¿Cómo podemos cambiar esto, para que en el tiempo de ejecución no tengamos que saber exactamente cuánto tiempo tenía el código fuente original?Después de investigar un poco en el código fuente de V8, descubrí que la información del encabezado se define aquí (en este archivo
code-serializer.h
): // The data header consists of uint32_t-sized entries: // [0] magic number and (internally provided) external reference count // [1] version hash // [2] source hash // [3] cpu features // [4] flag hash
Pero, el búfer de Node.js es una matriz de tipo Uint8Array. Esto significa que cada entrada de la
uint32
matriz tomará cuatro entradas en el uint8
buffer. Entonces, la longitud de la carga útil (que es source
hash en el índice [2]
, cual es [8, 9, 10, 11]
bytes en el búfer de nodo) será: let payloadLengthBytes = whateverBufferYouHave.slice( 8 , 12 );
<Buffer 1c 00 00 00>
, que es Little Endian, por lo que dice: 0x0000001c
. Esa es la longitud de nuestro código (28 en decimal).Para convertir estos cuatro bytes en un valor numérico, puede hacer algo como esto:
firstByte + (secodeByte * 256) + (thirdByte * 256**2) + (forthByte * 256**3),
O de una manera más elegante, puedes hacer esto:
let length = payloadLengthBytes.reduce( ( sum, number, power ) => sum += number * 256 **power , 0 );
Como hice aquí en mi biblioteca, verifíquelo para ver la receta completa.
Alternativamente, podríamos usar
buf.readIntLE()
función, que hace exactamente lo que queremos: let length = whateverBufferYouHave.readIntLE( 8 , 4 ); // 8 is the offset, 4 is the number of bytes to read
Una vez que haya leído la longitud del código original (que se utilizó para generar el
cachedData
buffer), ahora puede crear su script: let anotherHelloScript = new vm.Script( ' ' .repeat(length), { produceCachedData : true , cachedData : helloBuffer }); // later in your code anotherHelloScript.runInThisContext();
Finalmente, ¿esta técnica tiene un impacto en el rendimiento? Bueno, en las versiones recientes de v8 (y Node.js), el rendimiento es casi el mismo. Usando el índice de octancia no encontré ninguna diferencia en el rendimiento. Sé que Google desaprobó octance (porque los navegadores y los motores JS estaban haciendo trampa), pero los resultados en nuestra situación son significativos, porque estamos comparando el mismo código en el mismo motor JS. Entonces, la respuesta final es: Bytenode NO tiene un impacto negativo en el rendimiento.
Consulte mi repositorio de Github , donde puede encontrar ejemplos de trabajo completos. He agregado un ejemplo para Electron (que no tiene ninguna protección de código fuente) y para NW.js (que tiene una herramienta similar a nwjc, pero solo funciona con el código del lado del navegador). Agregaré más ejemplos (y pruebas) pronto, con suerte.