@syldel/crypto-utils 1.0.3 → 2.0.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/LICENSE.MD +4 -4
- package/README.md +90 -22
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/jwe-compact.d.ts +49 -0
- package/dist/jwe-compact.js +153 -0
- package/dist/jwe.util.d.ts +53 -0
- package/dist/jwe.util.js +138 -0
- package/dist/jwt.util.js +47 -9
- package/package.json +59 -49
- package/dist/aes-gcm.util.d.ts +0 -17
- package/dist/aes-gcm.util.js +0 -84
package/LICENSE.MD
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
Copyright (c) 2026, Syl-Studio Inc. c/o Syl. D.
|
|
2
|
-
|
|
3
|
-
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
|
4
|
-
|
|
1
|
+
Copyright (c) 2026, Syl-Studio Inc. c/o Syl. D.
|
|
2
|
+
|
|
3
|
+
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
|
4
|
+
|
|
5
5
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
CHANGED
|
@@ -3,45 +3,73 @@
|
|
|
3
3
|
A robust, high-performance cryptographic toolkit for Node.js and NestJS applications. This package prioritizes security by leveraging Node's native crypto module (OpenSSL) for maximum protection of sensitive data and private keys.
|
|
4
4
|
|
|
5
5
|
## 🚀 Key Features
|
|
6
|
+
|
|
6
7
|
- **Native Security**: Built on top of Node.js crypto (OpenSSL) for hardware-accelerated encryption.
|
|
7
8
|
|
|
8
|
-
- **
|
|
9
|
+
- **JWE compact (RFC 7516)**: authenticated encryption with `alg: "dir"` and `enc: "A256GCM"` (AES-256-GCM), bound to a key id and a context, checked against the RFC 7520 test vector.
|
|
9
10
|
|
|
10
11
|
- **Lightweight JWT**: Pure implementation for signing and verifying tokens without heavy dependencies.
|
|
11
12
|
|
|
12
|
-
- **
|
|
13
|
+
- **Errors that say what is wrong, and nothing more**: configuration and public-format errors are named; anything about the encrypted content is a single `Decryption failed`.
|
|
13
14
|
|
|
14
15
|
## 🛠 Usage
|
|
15
16
|
|
|
16
|
-
###
|
|
17
|
-
Designed for high-security environments. It automatically handles Initialization Vectors (IV) and Authentication Tags.
|
|
17
|
+
### JWE (Authenticated Encryption) — 2.0.0
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
A value is sealed into a JWE compact string. Its protected header names the key (`kid`) and the
|
|
20
|
+
context it belongs to (`ctx`), and the whole header is authenticated: neither can be rewritten.
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
```ts
|
|
23
|
+
import { JweUtil } from '@syldel/crypto-utils';
|
|
24
|
+
|
|
25
|
+
// Sealing: one key, named by its kid, and the context the value belongs to.
|
|
26
|
+
const token = JweUtil.seal(agentPrivateKey, {
|
|
27
|
+
kid: '2026-11',
|
|
28
|
+
key: masterKeyHex, // 32 bytes, as a Buffer or 64 hex characters
|
|
29
|
+
context: `hl-agent-key:${userId}`,
|
|
30
|
+
});
|
|
31
|
+
// eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwia2lkIjoiMjAyNi0xMSIs... (5 segments, the 2nd empty)
|
|
32
|
+
|
|
33
|
+
// Opening: every known key by kid, read and checked once at startup.
|
|
34
|
+
const keys = JweUtil.parseKeyring(process.env.AGENT_ENCRYPTION_KEYS); // "2026-11:<hex>,2025-02:<hex>"
|
|
35
|
+
const secret = JweUtil.open(token, { keys, context: `hl-agent-key:${userId}` });
|
|
36
|
+
```
|
|
24
37
|
|
|
25
|
-
|
|
26
|
-
const encrypted = AesGcmUtil.encrypt(message, keyHex);
|
|
27
|
-
// Returns: { data: '...', iv: '...', tag: '...' }
|
|
38
|
+
Protected header, byte for byte — no space, this key order, nothing more; anything else is refused:
|
|
28
39
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const original = AesGcmUtil.decrypt(encrypted, keyHex);
|
|
32
|
-
} catch (error) {
|
|
33
|
-
// Throws "Decryption failed" if data is tampered or key is wrong
|
|
34
|
-
}
|
|
40
|
+
```text
|
|
41
|
+
{"alg":"dir","enc":"A256GCM","kid":"2026-11","ctx":"hl-agent-key:<userId>","crit":["ctx"]}
|
|
35
42
|
```
|
|
36
43
|
|
|
44
|
+
| Error | Meaning |
|
|
45
|
+
| -------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
46
|
+
| `Invalid key …`, `Invalid kid …`, `Invalid context …`, `Invalid keyring …` | configuration of the caller; never quotes a key |
|
|
47
|
+
| `Unsupported format: …` | not a JWE string, e.g. the 1.x `{ data, iv, tag }` object |
|
|
48
|
+
| `Invalid JWE: …` | the public structure or the protected header is not the expected one |
|
|
49
|
+
| `Unknown key id: "…"` | no key with that kid in the keyring |
|
|
50
|
+
| `Decryption failed` | anything about the encrypted content, or the wrong key under the right kid |
|
|
51
|
+
| `Context mismatch: sealed for "…", expected "…"` | authenticated, so it states a fact: the value belongs to someone else |
|
|
52
|
+
|
|
53
|
+
#### Rotating a key
|
|
54
|
+
|
|
55
|
+
1. Add the new key to the keyring of every service that opens, and deploy.
|
|
56
|
+
2. Seal again with the new kid.
|
|
57
|
+
3. Remove the old key from the keyring.
|
|
58
|
+
|
|
59
|
+
#### Migrating from 1.x
|
|
60
|
+
|
|
61
|
+
`AesGcmUtil.encrypt`/`decrypt` and `EncryptedData` are removed. A 1.x object is refused by name
|
|
62
|
+
(`Unsupported format: { data, iv, tag } is the crypto-utils 1.x format`): seal the value again.
|
|
63
|
+
|
|
37
64
|
### Pure JWT Utilities
|
|
65
|
+
|
|
38
66
|
A lightweight way to handle JWTs in your NestJS guards or services.
|
|
39
67
|
|
|
40
68
|
```ts
|
|
41
69
|
import { PureJwtUtil } from '@syldel/crypto-utils';
|
|
42
70
|
|
|
43
|
-
const secret =
|
|
44
|
-
const payload = { sub:
|
|
71
|
+
const secret = 'your-secure-secret';
|
|
72
|
+
const payload = { sub: 'user_123', role: 'admin' };
|
|
45
73
|
|
|
46
74
|
// Sign a token
|
|
47
75
|
const token = PureJwtUtil.sign(payload, secret);
|
|
@@ -58,14 +86,43 @@ try {
|
|
|
58
86
|
|
|
59
87
|
- **Zero External Crypto Libs**: By using native Node.js APIs, this package minimizes the attack surface and dependency supply chain risks.
|
|
60
88
|
|
|
61
|
-
- **Side-Channel Protection**:
|
|
89
|
+
- **Side-Channel Protection**: JWT signatures are compared with `crypto.timingSafeEqual`, in constant time.
|
|
90
|
+
|
|
91
|
+
- **GCM Integrity**: Unlike AES-CTR or CBC, AES-GCM ensures that any single-bit modification to the encrypted data, the IV or the protected header will be detected during decryption.
|
|
92
|
+
|
|
93
|
+
### ⚠️ What is refused, and why
|
|
62
94
|
|
|
63
|
-
-
|
|
95
|
+
- `JweUtil.open` requires a 12-byte IV and a full 16-byte tag (RFC 7518 §5.3). Without this, Node accepts tags down to 4 bytes, and a random forged tag would pass with a 1 in 2^32 chance instead of 2^128.
|
|
96
|
+
- Every base64url segment and every hex key is decoded strictly and canonically: a stray character fails instead of being silently skipped.
|
|
97
|
+
- The protected header must be byte for byte the one `seal` writes: no extra field (`zip`, `jku`…), no duplicated field (which `JSON.parse` would silently overwrite), no token without a context.
|
|
98
|
+
- The key is chosen by its kid, never by trying every key: AES-GCM does not commit to its key, so one ciphertext can open under two keys.
|
|
99
|
+
- A kid is looked up as an own property only: `constructor` is not a key.
|
|
100
|
+
- `open` refuses a token above 2 MiB, `seal` a plaintext above `JweUtil.MAX_PLAINTEXT_BYTES` (1 MiB), before any decoding.
|
|
101
|
+
- A missing, non-hex or wrongly sized key throws `Invalid key: …`, never `Decryption failed`: it is a configuration error, and blaming the payload sent the search the wrong way. The message never quotes the key.
|
|
102
|
+
- `PureJwtUtil.verify` only accepts `alg: "HS256"`, a JSON object payload, a numeric `exp`, and the canonical base64url spelling of the signature.
|
|
103
|
+
- Both `sign` and `verify` refuse an empty or missing secret: HMAC happily signs with an empty key, which anyone can guess.
|
|
104
|
+
- `nbf`, `iat`, `aud` and `iss` are **not** checked: callers that rely on them must check them themselves.
|
|
64
105
|
|
|
106
|
+
### ⚠️ Known limit: the master key lives in plain text
|
|
107
|
+
|
|
108
|
+
Tink advises against keeping keys in plain text and recommends a key encryption key in a KMS
|
|
109
|
+
(Google Cloud KMS, AWS KMS, HashiCorp Vault). Here the keyring is an environment variable of each
|
|
110
|
+
service: whoever reads that environment reads the keys. A KMS is out of scope for now.
|
|
111
|
+
|
|
112
|
+
### Sources, read raw on 2026-11-02
|
|
113
|
+
|
|
114
|
+
- RFC 7516 (JWE) §5.1 step 14: the AAD is the ASCII of the encoded protected header; §4.1.6 `kid`.
|
|
115
|
+
- RFC 7518 (JWA) §4.5 `dir`, §5.3 AES-GCM (96-bit IV required, 128-bit tag), §8.4 at most 2^32 encryptions per key.
|
|
116
|
+
- RFC 7515 (JWS) §4.1.11 `crit`, which RFC 7516 §4.1.13 applies to JWE.
|
|
117
|
+
- RFC 7520 §5.6, the `dir` + AES-GCM test vector the tests reproduce in both directions.
|
|
118
|
+
- Tink, _Keysets_: rotation order (add, deploy, then make primary); the key-id prefix is not authenticated, only a hint.
|
|
119
|
+
- Tink, _AEAD_: a user id as associated data stops a value from moving to another user; AES-GCM is not key-committing.
|
|
120
|
+
- Tink, _Key management_: keys should not be stored in plain text (see the known limit above).
|
|
121
|
+
- AWS Encryption SDK specification, `framework/structures.md`: the encryption context is non-secret, canonically serialized, and authenticated.
|
|
65
122
|
|
|
66
123
|
## 🧪 Testing
|
|
67
124
|
|
|
68
|
-
Full test suite with 100% coverage
|
|
125
|
+
Full test suite with 100% coverage, including the RFC 7520 §5.6 vector and a reproducible fuzz of 3000 tampered tokens.
|
|
69
126
|
|
|
70
127
|
```bash
|
|
71
128
|
npm run test
|
|
@@ -74,18 +131,23 @@ npm run test
|
|
|
74
131
|
## 👨💻 Développement
|
|
75
132
|
|
|
76
133
|
### Installation des dépendances
|
|
134
|
+
|
|
77
135
|
```bash
|
|
78
136
|
npm install
|
|
79
137
|
```
|
|
80
138
|
|
|
81
139
|
### Compilation
|
|
140
|
+
|
|
82
141
|
Génère le dossier `dist/` contenant les fichiers JavaScript et les déclarations de types (`.d.ts`).
|
|
142
|
+
|
|
83
143
|
```bash
|
|
84
144
|
npm run build
|
|
85
145
|
```
|
|
86
146
|
|
|
87
147
|
### Qualité du code
|
|
148
|
+
|
|
88
149
|
Le projet utilise **ESLint** pour la logique et **Prettier** pour le formatage.
|
|
150
|
+
|
|
89
151
|
```bash
|
|
90
152
|
# Vérifier les erreurs
|
|
91
153
|
npx eslint .
|
|
@@ -109,17 +171,21 @@ Pour utiliser ce package dans tes autres projets sans le publier sur NPM :
|
|
|
109
171
|
Le projet utilise des **Granular Access Tokens** pour la publication afin de contourner la double authentification (2FA) manuelle tout en maintenant une sécurité maximale.
|
|
110
172
|
|
|
111
173
|
### Configuration du Token
|
|
174
|
+
|
|
112
175
|
1. Générer un token sur NPM avec les permissions `Read and Write`.
|
|
113
176
|
2. Restreindre l'accès au package `@syldel/crypto-utils` uniquement.
|
|
114
177
|
3. Utiliser l'option `Bypass 2FA` pour permettre l'automatisation.
|
|
115
178
|
|
|
116
179
|
### Commande de publication rapide
|
|
180
|
+
|
|
117
181
|
Si tu n'utilises pas de fichier `.npmrc`, tu peux publier en passant le token directement :
|
|
182
|
+
|
|
118
183
|
```bash
|
|
119
184
|
npm publish --access public --//registry.npmjs.org/:_authToken=TON_TOKEN_ICI
|
|
120
185
|
```
|
|
121
186
|
|
|
122
187
|
### Configuration de la publication
|
|
188
|
+
|
|
123
189
|
Créez un fichier .npmrc à la racine (ignoré par Git) pour l'authentification :
|
|
124
190
|
|
|
125
191
|
```
|
|
@@ -127,6 +193,7 @@ Créez un fichier .npmrc à la racine (ignoré par Git) pour l'authentification
|
|
|
127
193
|
```
|
|
128
194
|
|
|
129
195
|
### Publier une nouvelle version
|
|
196
|
+
|
|
130
197
|
La commande suivante automatise le build, l'incrémentation de version et l'envoi vers NPM :
|
|
131
198
|
|
|
132
199
|
```bash
|
|
@@ -134,6 +201,7 @@ npm run release
|
|
|
134
201
|
```
|
|
135
202
|
|
|
136
203
|
### Pousse le commit ET le tag sur GitHub
|
|
204
|
+
|
|
137
205
|
```bash
|
|
138
206
|
git push origin main --follow-tags
|
|
139
207
|
```
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export * from './jwt.util';
|
|
2
|
-
export * from './
|
|
2
|
+
export * from './jwe.util';
|
package/dist/index.js
CHANGED
|
@@ -15,4 +15,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./jwt.util"), exports);
|
|
18
|
-
__exportStar(require("./
|
|
18
|
+
__exportStar(require("./jwe.util"), exports);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cœur JWE (RFC 7516), sérialisation compacte, algorithme `dir` uniquement :
|
|
3
|
+
* la clé partagée sert directement de clé de contenu (RFC 7518 §4.5), la clé
|
|
4
|
+
* chiffrée est donc vide. Ce module ne décide d'aucune politique (quel en-tête,
|
|
5
|
+
* quelle clé, quel contexte) : c'est le rôle de `JweUtil`. Il est séparé pour
|
|
6
|
+
* être éprouvé tel quel contre le vecteur de la RFC 7520 §5.6, qui utilise
|
|
7
|
+
* A128GCM. Il n'est pas exporté par le paquet.
|
|
8
|
+
*/
|
|
9
|
+
export type ContentEncryption = 'A128GCM' | 'A256GCM';
|
|
10
|
+
/** RFC 7518 §5.3 : `enc` est fixé par la taille de la clé, jamais lu seul. */
|
|
11
|
+
export declare const ENC_BY_KEY_BYTES: Readonly<Record<number, ContentEncryption>>;
|
|
12
|
+
/** RFC 7518 §5.3 : IV de 96 bits REQUIS, tag de 128 bits quelle que soit la clé. */
|
|
13
|
+
export declare const IV_BYTES = 12;
|
|
14
|
+
export declare const TAG_BYTES = 16;
|
|
15
|
+
export interface CompactParts {
|
|
16
|
+
/** Le segment d'en-tête tel que reçu : c'est lui, et non une re-sérialisation, qui fait l'AAD. */
|
|
17
|
+
protectedHeader: string;
|
|
18
|
+
/** Le JSON de l'en-tête, décodé mais pas encore authentifié. */
|
|
19
|
+
headerJson: string;
|
|
20
|
+
header: Record<string, unknown>;
|
|
21
|
+
iv: string;
|
|
22
|
+
ciphertext: string;
|
|
23
|
+
tag: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* base64url strict et canonique. `Buffer.from(s, 'base64url')` saute les
|
|
27
|
+
* caractères invalides sans rien dire et accepte des bits de remplissage non
|
|
28
|
+
* nuls : deux écritures différentes donnaient le même octet, et un segment
|
|
29
|
+
* abîmé était lu à moitié.
|
|
30
|
+
*/
|
|
31
|
+
export declare function base64urlDecode(segment: string): Buffer;
|
|
32
|
+
/**
|
|
33
|
+
* Découpe et lit la structure, et lève `Invalid JWE: …`. Tout ce qui est
|
|
34
|
+
* vérifié ici est public :
|
|
35
|
+
* l'en-tête n'est pas chiffré, et rien de ce qui échoue ici ne dit quoi que
|
|
36
|
+
* ce soit sur la clé ou le clair.
|
|
37
|
+
*/
|
|
38
|
+
export declare function parseCompact(token: string): CompactParts;
|
|
39
|
+
/**
|
|
40
|
+
* Chiffre `plaintext` sous `header` (RFC 7516 §5.1). L'IV n'est passé que par
|
|
41
|
+
* les tests, pour reproduire le vecteur de la RFC 7520 ; partout ailleurs il
|
|
42
|
+
* est tiré au hasard.
|
|
43
|
+
*/
|
|
44
|
+
export declare function encryptDir(plaintext: string, key: Buffer, header: Readonly<Record<string, unknown>>, iv?: Buffer): string;
|
|
45
|
+
/**
|
|
46
|
+
* Déchiffre des segments déjà découpés. Toute erreur ici concerne le contenu
|
|
47
|
+
* chiffré : l'appelant la rend sous un message unique.
|
|
48
|
+
*/
|
|
49
|
+
export declare function decryptDir(parts: CompactParts, key: Buffer): string;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.TAG_BYTES = exports.IV_BYTES = exports.ENC_BY_KEY_BYTES = void 0;
|
|
37
|
+
exports.base64urlDecode = base64urlDecode;
|
|
38
|
+
exports.parseCompact = parseCompact;
|
|
39
|
+
exports.encryptDir = encryptDir;
|
|
40
|
+
exports.decryptDir = decryptDir;
|
|
41
|
+
const crypto = __importStar(require("crypto"));
|
|
42
|
+
/** RFC 7518 §5.3 : `enc` est fixé par la taille de la clé, jamais lu seul. */
|
|
43
|
+
exports.ENC_BY_KEY_BYTES = {
|
|
44
|
+
16: 'A128GCM',
|
|
45
|
+
32: 'A256GCM',
|
|
46
|
+
};
|
|
47
|
+
/** RFC 7518 §5.3 : IV de 96 bits REQUIS, tag de 128 bits quelle que soit la clé. */
|
|
48
|
+
exports.IV_BYTES = 12;
|
|
49
|
+
exports.TAG_BYTES = 16;
|
|
50
|
+
/**
|
|
51
|
+
* base64url strict et canonique. `Buffer.from(s, 'base64url')` saute les
|
|
52
|
+
* caractères invalides sans rien dire et accepte des bits de remplissage non
|
|
53
|
+
* nuls : deux écritures différentes donnaient le même octet, et un segment
|
|
54
|
+
* abîmé était lu à moitié.
|
|
55
|
+
*/
|
|
56
|
+
function base64urlDecode(segment) {
|
|
57
|
+
if (!/^[A-Za-z0-9_-]*$/.test(segment)) {
|
|
58
|
+
throw new Error('not base64url');
|
|
59
|
+
}
|
|
60
|
+
const bytes = Buffer.from(segment, 'base64url');
|
|
61
|
+
if (bytes.toString('base64url') !== segment) {
|
|
62
|
+
throw new Error('not canonical base64url');
|
|
63
|
+
}
|
|
64
|
+
return bytes;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Découpe et lit la structure, et lève `Invalid JWE: …`. Tout ce qui est
|
|
68
|
+
* vérifié ici est public :
|
|
69
|
+
* l'en-tête n'est pas chiffré, et rien de ce qui échoue ici ne dit quoi que
|
|
70
|
+
* ce soit sur la clé ou le clair.
|
|
71
|
+
*/
|
|
72
|
+
function parseCompact(token) {
|
|
73
|
+
const segments = token.split('.');
|
|
74
|
+
if (segments.length !== 5) {
|
|
75
|
+
throw new Error(`Invalid JWE: expected 5 segments, got ${segments.length}`);
|
|
76
|
+
}
|
|
77
|
+
const [protectedHeader, encryptedKey, iv, ciphertext, tag] = segments;
|
|
78
|
+
if (encryptedKey !== '') {
|
|
79
|
+
throw new Error('Invalid JWE: the encrypted key must be empty with alg "dir"');
|
|
80
|
+
}
|
|
81
|
+
let headerJson;
|
|
82
|
+
let header;
|
|
83
|
+
try {
|
|
84
|
+
headerJson = base64urlDecode(protectedHeader).toString('utf8');
|
|
85
|
+
header = JSON.parse(headerJson);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
throw new Error('Invalid JWE: unreadable protected header');
|
|
89
|
+
}
|
|
90
|
+
if (typeof header !== 'object' || header === null || Array.isArray(header)) {
|
|
91
|
+
throw new Error('Invalid JWE: the protected header is not a JSON object');
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
protectedHeader,
|
|
95
|
+
headerJson,
|
|
96
|
+
header: header,
|
|
97
|
+
iv,
|
|
98
|
+
ciphertext,
|
|
99
|
+
tag,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Chiffre `plaintext` sous `header` (RFC 7516 §5.1). L'IV n'est passé que par
|
|
104
|
+
* les tests, pour reproduire le vecteur de la RFC 7520 ; partout ailleurs il
|
|
105
|
+
* est tiré au hasard.
|
|
106
|
+
*/
|
|
107
|
+
function encryptDir(plaintext, key, header, iv = crypto.randomBytes(exports.IV_BYTES)) {
|
|
108
|
+
const enc = exports.ENC_BY_KEY_BYTES[key.length];
|
|
109
|
+
if (header.alg !== 'dir' || enc === undefined || header.enc !== enc) {
|
|
110
|
+
throw new Error('header and key do not match alg "dir" with A128GCM or A256GCM');
|
|
111
|
+
}
|
|
112
|
+
if (iv.length !== exports.IV_BYTES) {
|
|
113
|
+
throw new Error(`the IV must be ${exports.IV_BYTES} bytes`);
|
|
114
|
+
}
|
|
115
|
+
const protectedHeader = Buffer.from(JSON.stringify(header), 'utf8').toString('base64url');
|
|
116
|
+
const cipher = crypto.createCipheriv(enc === 'A128GCM' ? 'aes-128-gcm' : 'aes-256-gcm', key, iv, {
|
|
117
|
+
authTagLength: exports.TAG_BYTES,
|
|
118
|
+
});
|
|
119
|
+
// RFC 7516 §5.1 étape 14 : l'AAD est l'ASCII du segment d'en-tête encodé.
|
|
120
|
+
cipher.setAAD(Buffer.from(protectedHeader, 'ascii'));
|
|
121
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
|
122
|
+
return [
|
|
123
|
+
protectedHeader,
|
|
124
|
+
'',
|
|
125
|
+
iv.toString('base64url'),
|
|
126
|
+
ciphertext.toString('base64url'),
|
|
127
|
+
cipher.getAuthTag().toString('base64url'),
|
|
128
|
+
].join('.');
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Déchiffre des segments déjà découpés. Toute erreur ici concerne le contenu
|
|
132
|
+
* chiffré : l'appelant la rend sous un message unique.
|
|
133
|
+
*/
|
|
134
|
+
function decryptDir(parts, key) {
|
|
135
|
+
const enc = exports.ENC_BY_KEY_BYTES[key.length];
|
|
136
|
+
if (parts.header.alg !== 'dir' || enc === undefined || parts.header.enc !== enc) {
|
|
137
|
+
throw new Error('header and key do not match');
|
|
138
|
+
}
|
|
139
|
+
const iv = base64urlDecode(parts.iv);
|
|
140
|
+
const tag = base64urlDecode(parts.tag);
|
|
141
|
+
const ciphertext = base64urlDecode(parts.ciphertext);
|
|
142
|
+
// Sans ces deux contrôles et `authTagLength`, Node accepte un tag de 4 à 16
|
|
143
|
+
// octets : mesuré sur crypto-utils 1.0.3, un tag tronqué déchiffrait.
|
|
144
|
+
if (iv.length !== exports.IV_BYTES || tag.length !== exports.TAG_BYTES) {
|
|
145
|
+
throw new Error('wrong IV or tag length');
|
|
146
|
+
}
|
|
147
|
+
const decipher = crypto.createDecipheriv(enc === 'A128GCM' ? 'aes-128-gcm' : 'aes-256-gcm', key, iv, {
|
|
148
|
+
authTagLength: exports.TAG_BYTES,
|
|
149
|
+
});
|
|
150
|
+
decipher.setAAD(Buffer.from(parts.protectedHeader, 'ascii'));
|
|
151
|
+
decipher.setAuthTag(tag);
|
|
152
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
|
|
153
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** Une clé de 32 octets, en Buffer ou en 64 caractères hexadécimaux. */
|
|
2
|
+
export type JweKey = Buffer | string;
|
|
3
|
+
/** Les clés connues, par identifiant (`kid`). */
|
|
4
|
+
export type JweKeyring = Readonly<Record<string, JweKey>>;
|
|
5
|
+
export interface SealOptions {
|
|
6
|
+
/** Identifiant de la clé, écrit dans l'en-tête : `2026-11`, `session`. */
|
|
7
|
+
kid: string;
|
|
8
|
+
key: JweKey;
|
|
9
|
+
/** Ce à quoi le clair est lié, par exemple `hl-agent-key:<userId>`. Non secret. */
|
|
10
|
+
context: string;
|
|
11
|
+
}
|
|
12
|
+
export interface OpenOptions {
|
|
13
|
+
keys: JweKeyring;
|
|
14
|
+
/** Le contexte attendu : un JWE scellé pour un autre est refusé. */
|
|
15
|
+
context: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Chiffrement authentifié au format JWE compact (RFC 7516), `alg: "dir"` et
|
|
19
|
+
* `enc: "A256GCM"` (RFC 7518), sans dépendance.
|
|
20
|
+
*
|
|
21
|
+
* L'en-tête protégé porte le `kid` et le contexte (`ctx`), déclaré critique
|
|
22
|
+
* (`crit`, RFC 7515 §4.1.11) : une bibliothèque JOSE qui ne le connaît pas doit
|
|
23
|
+
* refuser le jeton au lieu d'ignorer le contrôle. L'en-tête entier est l'AAD,
|
|
24
|
+
* donc ni le `kid` ni le contexte ne se réécrivent sans casser le tag —
|
|
25
|
+
* contrairement au préfixe de clé de Tink, que sa doc dit non authentifié.
|
|
26
|
+
*
|
|
27
|
+
* Erreurs : ce qui concerne la configuration (`Invalid key`, `Invalid kid`,
|
|
28
|
+
* `Invalid context`, `Invalid keyring`) et la structure publique du jeton
|
|
29
|
+
* (`Invalid JWE`, `Unsupported format`, `Unknown key id`) se nomme. Tout ce
|
|
30
|
+
* qui touche au contenu chiffré reste un seul `Decryption failed`. Un contexte
|
|
31
|
+
* différent ne se signale qu'après authentification : le message dit alors un
|
|
32
|
+
* fait vérifié, pas ce que prétend un en-tête forgé.
|
|
33
|
+
*/
|
|
34
|
+
export declare class JweUtil {
|
|
35
|
+
/** Plus grand clair accepté (1 Mio), et donc plus grand jeton lu, avant tout décodage. */
|
|
36
|
+
static readonly MAX_PLAINTEXT_BYTES: number;
|
|
37
|
+
static seal(plaintext: string, { kid, key, context }: SealOptions): string;
|
|
38
|
+
static open(token: string, { keys, context }: OpenOptions): string;
|
|
39
|
+
/**
|
|
40
|
+
* Lit un trousseau écrit `kid:hex,kid:hex` (une variable d'environnement) et
|
|
41
|
+
* le valide entièrement : à appeler au démarrage, pour qu'un trousseau
|
|
42
|
+
* absent ou abîmé arrête le service au lieu du premier déchiffrement.
|
|
43
|
+
*/
|
|
44
|
+
static parseKeyring(raw: string | undefined): JweKeyring;
|
|
45
|
+
private static header;
|
|
46
|
+
private static assertKid;
|
|
47
|
+
private static assertContext;
|
|
48
|
+
/**
|
|
49
|
+
* Une clé de 32 octets, en Buffer ou en hexadécimal strict. Le message ne
|
|
50
|
+
* cite jamais la clé.
|
|
51
|
+
*/
|
|
52
|
+
private static toKey;
|
|
53
|
+
}
|
package/dist/jwe.util.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.JweUtil = void 0;
|
|
4
|
+
const jwe_compact_1 = require("./jwe-compact");
|
|
5
|
+
const KEY_BYTES = 32;
|
|
6
|
+
const KID_PATTERN = /^[a-z0-9-]{1,32}$/;
|
|
7
|
+
const MAX_CONTEXT_LENGTH = 256;
|
|
8
|
+
/**
|
|
9
|
+
* Chiffrement authentifié au format JWE compact (RFC 7516), `alg: "dir"` et
|
|
10
|
+
* `enc: "A256GCM"` (RFC 7518), sans dépendance.
|
|
11
|
+
*
|
|
12
|
+
* L'en-tête protégé porte le `kid` et le contexte (`ctx`), déclaré critique
|
|
13
|
+
* (`crit`, RFC 7515 §4.1.11) : une bibliothèque JOSE qui ne le connaît pas doit
|
|
14
|
+
* refuser le jeton au lieu d'ignorer le contrôle. L'en-tête entier est l'AAD,
|
|
15
|
+
* donc ni le `kid` ni le contexte ne se réécrivent sans casser le tag —
|
|
16
|
+
* contrairement au préfixe de clé de Tink, que sa doc dit non authentifié.
|
|
17
|
+
*
|
|
18
|
+
* Erreurs : ce qui concerne la configuration (`Invalid key`, `Invalid kid`,
|
|
19
|
+
* `Invalid context`, `Invalid keyring`) et la structure publique du jeton
|
|
20
|
+
* (`Invalid JWE`, `Unsupported format`, `Unknown key id`) se nomme. Tout ce
|
|
21
|
+
* qui touche au contenu chiffré reste un seul `Decryption failed`. Un contexte
|
|
22
|
+
* différent ne se signale qu'après authentification : le message dit alors un
|
|
23
|
+
* fait vérifié, pas ce que prétend un en-tête forgé.
|
|
24
|
+
*/
|
|
25
|
+
class JweUtil {
|
|
26
|
+
static seal(plaintext, { kid, key, context }) {
|
|
27
|
+
this.assertKid(kid);
|
|
28
|
+
this.assertContext(context);
|
|
29
|
+
const contentKey = this.toKey(key, 'Invalid key');
|
|
30
|
+
if (typeof plaintext !== 'string') {
|
|
31
|
+
throw new Error('Invalid plaintext: a string is required');
|
|
32
|
+
}
|
|
33
|
+
if (Buffer.byteLength(plaintext, 'utf8') > this.MAX_PLAINTEXT_BYTES) {
|
|
34
|
+
throw new Error('Invalid plaintext: above the size limit');
|
|
35
|
+
}
|
|
36
|
+
return (0, jwe_compact_1.encryptDir)(plaintext, contentKey, this.header(kid, context));
|
|
37
|
+
}
|
|
38
|
+
static open(token, { keys, context }) {
|
|
39
|
+
this.assertContext(context);
|
|
40
|
+
if (typeof token !== 'string') {
|
|
41
|
+
// Le format 1.x était un objet { data, iv, tag } : il se refuse en le
|
|
42
|
+
// nommant, pour que la cause se lise sans ouvrir le code.
|
|
43
|
+
const legacy = typeof token === 'object' && token !== null && 'data' in token && 'iv' in token;
|
|
44
|
+
throw new Error(legacy
|
|
45
|
+
? 'Unsupported format: { data, iv, tag } is the crypto-utils 1.x format; seal the value again'
|
|
46
|
+
: 'Unsupported format: a JWE compact string is required');
|
|
47
|
+
}
|
|
48
|
+
// base64url grossit de 4/3 : deux fois la limite couvre l'en-tête et la marge.
|
|
49
|
+
if (token.length > 2 * this.MAX_PLAINTEXT_BYTES) {
|
|
50
|
+
throw new Error('Invalid JWE: above the size limit');
|
|
51
|
+
}
|
|
52
|
+
const parts = (0, jwe_compact_1.parseCompact)(token);
|
|
53
|
+
// L'en-tête doit être exactement celui que `seal` écrit, octet pour octet :
|
|
54
|
+
// ni champ en plus (`zip`, `jku`…), ni doublon que JSON.parse aurait
|
|
55
|
+
// écrasé en silence, ni autre ordre ou espace.
|
|
56
|
+
const { kid, ctx } = parts.header;
|
|
57
|
+
if (typeof kid !== 'string' ||
|
|
58
|
+
typeof ctx !== 'string' ||
|
|
59
|
+
parts.headerJson !== JSON.stringify(this.header(kid, ctx))) {
|
|
60
|
+
throw new Error('Invalid JWE: the protected header must be exactly {alg:"dir", enc:"A256GCM", kid, ctx, crit:["ctx"]}');
|
|
61
|
+
}
|
|
62
|
+
// Une clé se choisit par son `kid`, jamais en les essayant toutes : GCM
|
|
63
|
+
// n'engage pas la clé, un chiffré peut s'ouvrir sous deux clés (Tink, page AEAD).
|
|
64
|
+
if (!Object.prototype.hasOwnProperty.call(keys, kid)) {
|
|
65
|
+
throw new Error(`Unknown key id: ${JSON.stringify(kid.slice(0, 64))}`);
|
|
66
|
+
}
|
|
67
|
+
const contentKey = this.toKey(keys[kid], `Invalid key ${JSON.stringify(kid)}`);
|
|
68
|
+
let plaintext;
|
|
69
|
+
try {
|
|
70
|
+
plaintext = (0, jwe_compact_1.decryptDir)(parts, contentKey);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
throw new Error('Decryption failed');
|
|
74
|
+
}
|
|
75
|
+
if (ctx !== context) {
|
|
76
|
+
throw new Error(`Context mismatch: sealed for ${JSON.stringify(ctx)}, expected ${JSON.stringify(context)}`);
|
|
77
|
+
}
|
|
78
|
+
return plaintext;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Lit un trousseau écrit `kid:hex,kid:hex` (une variable d'environnement) et
|
|
82
|
+
* le valide entièrement : à appeler au démarrage, pour qu'un trousseau
|
|
83
|
+
* absent ou abîmé arrête le service au lieu du premier déchiffrement.
|
|
84
|
+
*/
|
|
85
|
+
static parseKeyring(raw) {
|
|
86
|
+
if (typeof raw !== 'string' || raw.trim() === '') {
|
|
87
|
+
throw new Error('Invalid keyring: missing or empty (is the secret configured?)');
|
|
88
|
+
}
|
|
89
|
+
const keyring = Object.create(null);
|
|
90
|
+
for (const entry of raw.split(',')) {
|
|
91
|
+
const separator = entry.indexOf(':');
|
|
92
|
+
const kid = separator === -1 ? '' : entry.slice(0, separator).trim();
|
|
93
|
+
if (!KID_PATTERN.test(kid)) {
|
|
94
|
+
throw new Error('Invalid keyring: each entry must read kid:hex, kid matching [a-z0-9-]{1,32}');
|
|
95
|
+
}
|
|
96
|
+
if (kid in keyring) {
|
|
97
|
+
throw new Error(`Invalid keyring: key id ${JSON.stringify(kid)} appears twice`);
|
|
98
|
+
}
|
|
99
|
+
keyring[kid] = this.toKey(entry.slice(separator + 1).trim(), `Invalid keyring: key ${JSON.stringify(kid)}`);
|
|
100
|
+
}
|
|
101
|
+
return Object.freeze(keyring);
|
|
102
|
+
}
|
|
103
|
+
static header(kid, ctx) {
|
|
104
|
+
return { alg: 'dir', enc: 'A256GCM', kid, ctx, crit: ['ctx'] };
|
|
105
|
+
}
|
|
106
|
+
static assertKid(kid) {
|
|
107
|
+
if (typeof kid !== 'string' || !KID_PATTERN.test(kid)) {
|
|
108
|
+
throw new Error('Invalid kid: it must match [a-z0-9-]{1,32}');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
static assertContext(context) {
|
|
112
|
+
if (typeof context !== 'string' ||
|
|
113
|
+
context.length === 0 ||
|
|
114
|
+
context.length > MAX_CONTEXT_LENGTH) {
|
|
115
|
+
throw new Error(`Invalid context: a non-empty string of at most ${MAX_CONTEXT_LENGTH} characters is required`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Une clé de 32 octets, en Buffer ou en hexadécimal strict. Le message ne
|
|
120
|
+
* cite jamais la clé.
|
|
121
|
+
*/
|
|
122
|
+
static toKey(key, label) {
|
|
123
|
+
if (key === undefined || key === null || key.length === 0) {
|
|
124
|
+
throw new Error(`${label}: missing or empty (is the secret configured?)`);
|
|
125
|
+
}
|
|
126
|
+
if (typeof key === 'string' && (key.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(key))) {
|
|
127
|
+
throw new Error(`${label}: not a hex string`);
|
|
128
|
+
}
|
|
129
|
+
const bytes = typeof key === 'string' ? Buffer.from(key, 'hex') : key;
|
|
130
|
+
if (!Buffer.isBuffer(bytes) || bytes.length !== KEY_BYTES) {
|
|
131
|
+
throw new Error(`${label}: must be 32 bytes (64 hex characters)`);
|
|
132
|
+
}
|
|
133
|
+
return bytes;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
exports.JweUtil = JweUtil;
|
|
137
|
+
/** Plus grand clair accepté (1 Mio), et donc plus grand jeton lu, avant tout décodage. */
|
|
138
|
+
JweUtil.MAX_PLAINTEXT_BYTES = 1024 * 1024;
|
package/dist/jwt.util.js
CHANGED
|
@@ -40,22 +40,37 @@ class PureJwtUtil {
|
|
|
40
40
|
* Verifies a JWT's signature and expiration, then returns the decoded payload.
|
|
41
41
|
*/
|
|
42
42
|
static verify(token, secret) {
|
|
43
|
+
assertSecret(secret);
|
|
43
44
|
const parts = token.split('.');
|
|
44
45
|
if (parts.length !== 3)
|
|
45
46
|
throw new Error('Invalid token format');
|
|
46
47
|
const [headerB64, payloadB64, signatureB64] = parts;
|
|
47
48
|
const dataToVerify = `${headerB64}.${payloadB64}`;
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
const expected = crypto.createHmac('sha256', secret).update(dataToVerify).digest();
|
|
50
|
+
const received = Buffer.from(signatureB64, 'base64url');
|
|
51
|
+
// Comparaison en temps constant : `!==` s'arrête au premier caractère
|
|
52
|
+
// différent, et le temps de réponse renseigne sur la signature attendue.
|
|
53
|
+
// timingSafeEqual lance sur deux longueurs différentes, d'où le test avant.
|
|
54
|
+
// Le décodage base64url est tolérant : plusieurs écritures donnent les mêmes
|
|
55
|
+
// octets. Seule la forme canonique est admise, comme avec l'ancien `!==`.
|
|
56
|
+
if (received.toString('base64url') !== signatureB64 ||
|
|
57
|
+
received.length !== expected.length ||
|
|
58
|
+
!crypto.timingSafeEqual(received, expected)) {
|
|
53
59
|
throw new Error('Invalid signature');
|
|
54
60
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
// L'en-tête n'était jamais lu : un jeton annonçant « none » ou un autre
|
|
62
|
+
// algorithme passait du moment que sa signature HS256 était juste.
|
|
63
|
+
const header = parseJsonObject(headerB64, 'Invalid token header');
|
|
64
|
+
if (header.alg !== 'HS256') {
|
|
65
|
+
throw new Error('Unsupported algorithm');
|
|
66
|
+
}
|
|
67
|
+
const payload = parseJsonObject(payloadB64, 'Invalid token payload');
|
|
68
|
+
// Un `exp` non numérique était ignoré : le jeton n'expirait jamais.
|
|
69
|
+
if (payload.exp !== undefined) {
|
|
70
|
+
if (typeof payload.exp !== 'number' || !Number.isFinite(payload.exp)) {
|
|
71
|
+
throw new Error('Invalid token payload');
|
|
72
|
+
}
|
|
73
|
+
if (Date.now() >= payload.exp * 1000) {
|
|
59
74
|
throw new Error('Token expired');
|
|
60
75
|
}
|
|
61
76
|
}
|
|
@@ -65,6 +80,7 @@ class PureJwtUtil {
|
|
|
65
80
|
* Signs a payload and returns a compact JWT string.
|
|
66
81
|
*/
|
|
67
82
|
static sign(payload, secret) {
|
|
83
|
+
assertSecret(secret);
|
|
68
84
|
const header = { alg: 'HS256', typ: 'JWT' };
|
|
69
85
|
const encode = (obj) => Buffer.from(JSON.stringify(obj)).toString('base64url');
|
|
70
86
|
const encodedHeader = encode(header);
|
|
@@ -77,3 +93,25 @@ class PureJwtUtil {
|
|
|
77
93
|
}
|
|
78
94
|
}
|
|
79
95
|
exports.PureJwtUtil = PureJwtUtil;
|
|
96
|
+
/**
|
|
97
|
+
* createHmac signe avec une chaîne vide (mesuré sur le bot le 2026-10-06) : la
|
|
98
|
+
* signature attendue devient calculable par quiconque devine le secret vide.
|
|
99
|
+
*/
|
|
100
|
+
function assertSecret(secret) {
|
|
101
|
+
if (typeof secret !== 'string' || secret.length === 0) {
|
|
102
|
+
throw new Error('Invalid secret: a non-empty string is required');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function parseJsonObject(segment, error) {
|
|
106
|
+
let value;
|
|
107
|
+
try {
|
|
108
|
+
value = JSON.parse(Buffer.from(segment, 'base64url').toString('utf8'));
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
throw new Error(error);
|
|
112
|
+
}
|
|
113
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
114
|
+
throw new Error(error);
|
|
115
|
+
}
|
|
116
|
+
return value;
|
|
117
|
+
}
|
package/package.json
CHANGED
|
@@ -1,49 +1,59 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@syldel/crypto-utils",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Lightweight JWT and AES-256-GCM utilities",
|
|
5
|
-
"main": "dist/index.js",
|
|
6
|
-
"types": "dist/index.d.ts",
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@syldel/crypto-utils",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Lightweight JWT and JWE (AES-256-GCM) utilities",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Syldel/crypto-utils.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/Syldel/crypto-utils/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/Syldel/crypto-utils#readme",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"lint": "eslint .",
|
|
20
|
+
"lint:fix": "eslint . --fix",
|
|
21
|
+
"clean": "rm -rf dist",
|
|
22
|
+
"build": "npm run lint && npm run clean && tsc -p tsconfig.json",
|
|
23
|
+
"test": "jest",
|
|
24
|
+
"test:watch": "jest --watch",
|
|
25
|
+
"prepublishOnly": "npm run build",
|
|
26
|
+
"release": "npm run build && npm version patch && npm publish"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"jwt",
|
|
33
|
+
"jwe",
|
|
34
|
+
"aes",
|
|
35
|
+
"crypto",
|
|
36
|
+
"security",
|
|
37
|
+
"nestjs"
|
|
38
|
+
],
|
|
39
|
+
"author": "Syl. D.",
|
|
40
|
+
"license": "ISC",
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@eslint/js": "^10.0.1",
|
|
43
|
+
"@types/jest": "^30.0.0",
|
|
44
|
+
"@types/node": "^24.19.2",
|
|
45
|
+
"eslint": "^10.0.1",
|
|
46
|
+
"eslint-config-prettier": "^10.1.8",
|
|
47
|
+
"eslint-plugin-jsonc": "^3.0.0",
|
|
48
|
+
"eslint-plugin-prettier": "^5.5.5",
|
|
49
|
+
"jest": "^30.2.0",
|
|
50
|
+
"jsonc-eslint-parser": "^3.1.0",
|
|
51
|
+
"prettier": "^3.8.1",
|
|
52
|
+
"ts-jest": "^29.4.6",
|
|
53
|
+
"typescript": "^5.9.3",
|
|
54
|
+
"typescript-eslint": "^8.56.0"
|
|
55
|
+
},
|
|
56
|
+
"overrides": {
|
|
57
|
+
"minimatch": "^10.2.1"
|
|
58
|
+
}
|
|
59
|
+
}
|
package/dist/aes-gcm.util.d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export interface EncryptedData {
|
|
2
|
-
data: string;
|
|
3
|
-
iv: string;
|
|
4
|
-
tag: string;
|
|
5
|
-
}
|
|
6
|
-
export declare class AesGcmUtil {
|
|
7
|
-
private static readonly ALGORITHM;
|
|
8
|
-
private static validateKey;
|
|
9
|
-
/**
|
|
10
|
-
* Encrypts text using native Node.js crypto (AES-256-GCM)
|
|
11
|
-
*/
|
|
12
|
-
static encrypt(text: string, key: Buffer | string): EncryptedData;
|
|
13
|
-
/**
|
|
14
|
-
* Decrypts a payload using native Node.js crypto (AES-256-GCM)
|
|
15
|
-
*/
|
|
16
|
-
static decrypt(payload: EncryptedData, key: Buffer | string): string;
|
|
17
|
-
}
|
package/dist/aes-gcm.util.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.AesGcmUtil = void 0;
|
|
37
|
-
const crypto = __importStar(require("crypto"));
|
|
38
|
-
class AesGcmUtil {
|
|
39
|
-
static validateKey(key) {
|
|
40
|
-
const encryptionKey = typeof key === 'string' ? Buffer.from(key, 'hex') : key;
|
|
41
|
-
if (encryptionKey.length !== 32) {
|
|
42
|
-
throw new Error('Invalid Key: Key must be 32 bytes (64 hex characters)');
|
|
43
|
-
}
|
|
44
|
-
return encryptionKey;
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Encrypts text using native Node.js crypto (AES-256-GCM)
|
|
48
|
-
*/
|
|
49
|
-
static encrypt(text, key) {
|
|
50
|
-
try {
|
|
51
|
-
const encryptionKey = this.validateKey(key);
|
|
52
|
-
const iv = crypto.randomBytes(12);
|
|
53
|
-
const cipher = crypto.createCipheriv(this.ALGORITHM, encryptionKey, iv);
|
|
54
|
-
let encrypted = cipher.update(text, 'utf8', 'hex');
|
|
55
|
-
encrypted += cipher.final('hex');
|
|
56
|
-
return {
|
|
57
|
-
data: encrypted,
|
|
58
|
-
iv: iv.toString('hex'),
|
|
59
|
-
tag: cipher.getAuthTag().toString('hex'),
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
throw new Error('Encryption failed');
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Decrypts a payload using native Node.js crypto (AES-256-GCM)
|
|
68
|
-
*/
|
|
69
|
-
static decrypt(payload, key) {
|
|
70
|
-
try {
|
|
71
|
-
const encryptionKey = this.validateKey(key);
|
|
72
|
-
const decipher = crypto.createDecipheriv(this.ALGORITHM, encryptionKey, Buffer.from(payload.iv, 'hex'));
|
|
73
|
-
decipher.setAuthTag(Buffer.from(payload.tag, 'hex'));
|
|
74
|
-
let decrypted = decipher.update(payload.data, 'hex', 'utf8');
|
|
75
|
-
decrypted += decipher.final('utf8');
|
|
76
|
-
return decrypted;
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
throw new Error('Decryption failed');
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
exports.AesGcmUtil = AesGcmUtil;
|
|
84
|
-
AesGcmUtil.ALGORITHM = 'aes-256-gcm';
|