@hostwebhook/platform-node 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.
- package/dist/crypto.d.ts +24 -0
- package/dist/crypto.js +89 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -0
- package/package.json +26 -0
package/dist/crypto.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Una llave, o varias.
|
|
3
|
+
*
|
|
4
|
+
* Con varias, **la PRIMERA es la que cifra** y las demás sólo sirven para leer
|
|
5
|
+
* lo que se cifró antes. Existe porque las credenciales van a tener llave
|
|
6
|
+
* propia —separada de `api.secret`, que cifra otras seis cosas— y no se pueden
|
|
7
|
+
* re-cifrar todas en el mismo instante en que cambia el código.
|
|
8
|
+
*
|
|
9
|
+
* ⚠️ Que funcione depende de que esto sea AES-GCM: el tag de autenticación
|
|
10
|
+
* hace que descifrar con la llave equivocada FALLE en vez de devolver basura.
|
|
11
|
+
* Con un cifrado sin autenticar, probar llaves en cadena devolvería un secreto
|
|
12
|
+
* corrupto que parecería válido. Si algún día se cambia de algoritmo, esto se
|
|
13
|
+
* cae con él.
|
|
14
|
+
*/
|
|
15
|
+
export type Llaves = string | string[];
|
|
16
|
+
export declare function encrypt(text: string, llaves: Llaves): string;
|
|
17
|
+
/**
|
|
18
|
+
* Descifra probando cada llave, y con cada una los dos KDF.
|
|
19
|
+
*
|
|
20
|
+
* ⚠️ El orden importa para el COSTE, no para la corrección: el tag de GCM
|
|
21
|
+
* garantiza que sólo una combinación puede acertar. Se prueba primero la que
|
|
22
|
+
* cifra, que es la que acertará en cuanto la migración haya pasado.
|
|
23
|
+
*/
|
|
24
|
+
export declare function decrypt(encrypted: string, llaves: Llaves): string;
|
package/dist/crypto.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.encrypt = encrypt;
|
|
4
|
+
exports.decrypt = decrypt;
|
|
5
|
+
const crypto_1 = require("crypto");
|
|
6
|
+
const ALGO = 'aes-256-gcm';
|
|
7
|
+
const KDF_SALT = 'hostwebhook-credential-encryption';
|
|
8
|
+
const KDF_ITERATIONS = 100000;
|
|
9
|
+
/**
|
|
10
|
+
* Llaves ya derivadas, por secreto.
|
|
11
|
+
*
|
|
12
|
+
* `pbkdf2Sync` con 100.000 iteraciones cuesta **28 ms medidos**, y es
|
|
13
|
+
* SÍNCRONO: bloquea el bucle de eventos entero mientras corre. Se pagaba en
|
|
14
|
+
* CADA `encrypt`/`decrypt`, o sea en cada nodo que resuelve una credencial —
|
|
15
|
+
* un pipeline con tres se comía ~84 ms de API parada por evento.
|
|
16
|
+
*
|
|
17
|
+
* Y era gasto puro: la derivación depende sólo de la sal (constante) y del
|
|
18
|
+
* secreto, que es `api.secret` — el mismo durante toda la vida del proceso.
|
|
19
|
+
* Derivar 100.000 veces lo mismo no protege de nada más; el coste alto de
|
|
20
|
+
* PBKDF2 defiende contra fuerza bruta sobre la llave, y eso ya lo pagas la
|
|
21
|
+
* primera vez.
|
|
22
|
+
*
|
|
23
|
+
* `Map` y no un solo valor porque hay dos derivaciones (la nueva y la legacy)
|
|
24
|
+
* y nada impide que se cifre con más de un secreto en el mismo proceso.
|
|
25
|
+
*/
|
|
26
|
+
const llavesDerivadas = new Map();
|
|
27
|
+
function deriveKey(secret) {
|
|
28
|
+
const cacheada = llavesDerivadas.get(secret);
|
|
29
|
+
if (cacheada)
|
|
30
|
+
return cacheada;
|
|
31
|
+
const key = (0, crypto_1.pbkdf2Sync)(secret, KDF_SALT, KDF_ITERATIONS, 32, 'sha256');
|
|
32
|
+
llavesDerivadas.set(secret, key);
|
|
33
|
+
return key;
|
|
34
|
+
}
|
|
35
|
+
/** Legacy key derivation — only for decrypting old credentials */
|
|
36
|
+
function legacyKey(secret) {
|
|
37
|
+
return Buffer.from(secret.padEnd(32).slice(0, 32));
|
|
38
|
+
}
|
|
39
|
+
/** La que cifra. */
|
|
40
|
+
const paraCifrar = (l) => (Array.isArray(l) ? l[0] : l);
|
|
41
|
+
/** Todas las que pueden descifrar, en orden. */
|
|
42
|
+
const paraDescifrar = (l) => (Array.isArray(l) ? l : [l]);
|
|
43
|
+
function encrypt(text, llaves) {
|
|
44
|
+
const secret = paraCifrar(llaves);
|
|
45
|
+
const key = deriveKey(secret);
|
|
46
|
+
const iv = (0, crypto_1.randomBytes)(16);
|
|
47
|
+
const cipher = (0, crypto_1.createCipheriv)(ALGO, key, iv);
|
|
48
|
+
const encrypted = Buffer.concat([
|
|
49
|
+
cipher.update(text, 'utf8'),
|
|
50
|
+
cipher.final(),
|
|
51
|
+
]);
|
|
52
|
+
const tag = cipher.getAuthTag();
|
|
53
|
+
return `${iv.toString('hex')}:${tag.toString('hex')}:${encrypted.toString('hex')}`;
|
|
54
|
+
}
|
|
55
|
+
function conLlave(encrypted, key) {
|
|
56
|
+
const [ivHex, tagHex, dataHex] = encrypted.split(':');
|
|
57
|
+
const decipher = (0, crypto_1.createDecipheriv)(ALGO, key, Buffer.from(ivHex, 'hex'));
|
|
58
|
+
decipher.setAuthTag(Buffer.from(tagHex, 'hex'));
|
|
59
|
+
return decipher.update(dataHex, 'hex', 'utf8') + decipher.final('utf8');
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Descifra probando cada llave, y con cada una los dos KDF.
|
|
63
|
+
*
|
|
64
|
+
* ⚠️ El orden importa para el COSTE, no para la corrección: el tag de GCM
|
|
65
|
+
* garantiza que sólo una combinación puede acertar. Se prueba primero la que
|
|
66
|
+
* cifra, que es la que acertará en cuanto la migración haya pasado.
|
|
67
|
+
*/
|
|
68
|
+
function decrypt(encrypted, llaves) {
|
|
69
|
+
const secretos = paraDescifrar(llaves);
|
|
70
|
+
let ultimo;
|
|
71
|
+
for (const secret of secretos) {
|
|
72
|
+
try {
|
|
73
|
+
return conLlave(encrypted, deriveKey(secret));
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
ultimo = e;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
// Credenciales viejas, cifradas con la derivación anterior.
|
|
80
|
+
return conLlave(encrypted, legacyKey(secret));
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
ultimo = e;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
throw ultimo instanceof Error
|
|
87
|
+
? ultimo
|
|
88
|
+
: new Error('no se pudo descifrar con ninguna de las llaves');
|
|
89
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { encrypt, decrypt, type Llaves } from './crypto';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.decrypt = exports.encrypt = void 0;
|
|
4
|
+
var crypto_1 = require("./crypto");
|
|
5
|
+
Object.defineProperty(exports, "encrypt", { enumerable: true, get: function () { return crypto_1.encrypt; } });
|
|
6
|
+
Object.defineProperty(exports, "decrypt", { enumerable: true, get: function () { return crypto_1.decrypt; } });
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hostwebhook/platform-node",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Lo que los servicios de HostWebhook comparten del lado de NODE: cifrado y utilidades que no pueden estar escritas dos veces",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"test": "vitest run",
|
|
13
|
+
"prepublishOnly": "npm run build"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"hostwebhook",
|
|
17
|
+
"crypto",
|
|
18
|
+
"platform"
|
|
19
|
+
],
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^22.0.0",
|
|
23
|
+
"typescript": "^5.0.0",
|
|
24
|
+
"vitest": "^3.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|