@syldel/crypto-utils 1.0.2 → 1.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/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
@@ -1,60 +1,86 @@
1
1
  # @syldel/crypto-utils
2
2
 
3
- A lightweight, universal, and dependency-safe cryptographic toolkit for Node.js, Web browsers, and Mobile frameworks (Ionic/Capacitor).
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
- ## 🚀 Features
6
- - **Universal Compatibility**: Works everywhere (Node, Angular, React, Ionic, NestJS).
5
+ ## 🚀 Key Features
6
+ - **Native Security**: Built on top of Node.js crypto (OpenSSL) for hardware-accelerated encryption.
7
7
 
8
- - **Zero Node-only Dependencies**: Replaced native crypto and Buffer with cross-platform alternatives.
8
+ - **AES-256-GCM**: Industry-standard authenticated encryption (AEAD) to ensure both confidentiality and data integrity.
9
9
 
10
- - **AES-256-GCM**: Secure authenticated encryption with integrity verification.
10
+ - **Lightweight JWT**: Pure implementation for signing and verifying tokens without heavy dependencies.
11
11
 
12
- - **Pure JWT**: Sign and verify JSON Web Tokens without heavy libraries.
12
+ - **Standardized Error Handling**: Harmonized error messages ("Decryption failed") for better security and predictable testing.
13
13
 
14
14
  ## 🛠 Usage
15
15
 
16
- ### AES-GCM Encryption (Universal)
17
- Uses node-forge for authenticated encryption. It ensures that if the data is tampered with, decryption will fail.
16
+ ### AES-256-GCM (Authenticated Encryption)
17
+ Designed for high-security environments. It automatically handles Initialization Vectors (IV) and Authentication Tags.
18
18
 
19
19
  ```ts
20
20
  import { AesGcmUtil } from '@syldel/crypto-utils';
21
21
 
22
- const key = "your-64-char-hex-key...";
23
- const message = "Hello World";
22
+ const keyHex = "0123456789abcdef..."; // 64 hex characters (32 bytes)
23
+ const message = "Sensitive data";
24
24
 
25
25
  // Encrypt
26
- const encrypted = AesGcmUtil.encrypt(message, key);
27
- console.log(encrypted); // { data: '...', iv: '...', tag: '...' }
26
+ const encrypted = AesGcmUtil.encrypt(message, keyHex);
27
+ // Returns: { data: '...', iv: '...', tag: '...' }
28
28
 
29
29
  // Decrypt
30
- const original = AesGcmUtil.decrypt(encrypted, key);
30
+ try {
31
+ const original = AesGcmUtil.decrypt(encrypted, keyHex);
32
+ } catch (error) {
33
+ // "Invalid key: …" if the key is missing, not hex or not 32 bytes (a config error)
34
+ // "Decryption failed" for anything wrong with the payload, or the wrong key
35
+ }
31
36
  ```
32
37
 
33
- ### JWT Utilities
34
- Lightweight JWT implementation using crypto-js.
38
+ ### Pure JWT Utilities
39
+ A lightweight way to handle JWTs in your NestJS guards or services.
35
40
 
36
41
  ```ts
37
42
  import { PureJwtUtil } from '@syldel/crypto-utils';
38
43
 
39
- const secret = "my-super-secret";
40
- const payload = { sub: "123", role: "admin" };
44
+ const secret = "your-secure-secret";
45
+ const payload = { sub: "user_123", role: "admin" };
41
46
 
42
- // Sign
47
+ // Sign a token
43
48
  const token = PureJwtUtil.sign(payload, secret);
44
49
 
45
- // Verify
50
+ // Verify and decode
46
51
  try {
47
- const decoded = PureJwtUtil.verify(token, secret);
52
+ const decoded = PureJwtUtil.verify<MyUserType>(token, secret);
48
53
  } catch (err) {
49
- console.error("Invalid or expired token");
54
+ // Handles "Token expired" or "Invalid signature"
50
55
  }
51
56
  ```
52
57
 
53
- ### 🔒 Security Principles
58
+ ## 🔒 Security & Performance
59
+
60
+ - **Zero External Crypto Libs**: By using native Node.js APIs, this package minimizes the attack surface and dependency supply chain risks.
61
+
62
+ - **Side-Channel Protection**: JWT signatures are compared with `crypto.timingSafeEqual`, in constant time.
63
+
64
+ - **GCM Integrity**: Unlike AES-CTR or CBC, AES-GCM ensures that any single-bit modification to the encrypted data will be detected during decryption.
65
+
66
+ ### ⚠️ What is refused, and why
54
67
 
55
- - **Integrity First**: Using AES-GCM (via Node-Forge) ensures that any modification to the ciphertext or the tag results in a decryption error, preventing "padding oracle" and bit-flipping attacks.
68
+ - `AesGcmUtil.decrypt` requires a full 16-byte tag and a 12-byte IV. 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.
69
+ - Every hex input (key, data, IV, tag) is decoded strictly: a stray character fails instead of being silently cut off.
70
+ - `decrypt` refuses a ciphertext above `AesGcmUtil.MAX_CIPHERTEXT_BYTES` (1 MiB) before decoding it.
71
+ - 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. Everything wrong with the payload stays a single generic `Decryption failed`, so an attacker learns nothing about which check failed.
72
+ - `PureJwtUtil.verify` only accepts `alg: "HS256"`, a JSON object payload, a numeric `exp`, and the canonical base64url spelling of the signature.
73
+ - Both `sign` and `verify` refuse an empty or missing secret: HMAC happily signs with an empty key, which anyone can guess.
74
+ - `nbf`, `iat`, `aud` and `iss` are **not** checked: callers that rely on them must check them themselves.
56
75
 
57
- - **Isomorphic Helpers**: Custom EncodingHelper uses standard Web APIs (atob/btoa) to handle Base64URL without needing the Node.js Buffer object.
76
+
77
+ ## 🧪 Testing
78
+
79
+ Full test suite with 100% coverage on core crypto logic.
80
+
81
+ ```bash
82
+ npm run test
83
+ ```
58
84
 
59
85
  ## 👨‍💻 Développement
60
86
 
@@ -4,6 +4,36 @@ export interface EncryptedData {
4
4
  tag: string;
5
5
  }
6
6
  export declare class AesGcmUtil {
7
- static encrypt(text: string, keyHex: string): EncryptedData;
8
- static decrypt(payload: EncryptedData, keyHex: string): string;
7
+ private static readonly ALGORITHM;
8
+ private static readonly KEY_BYTES;
9
+ private static readonly IV_BYTES;
10
+ private static readonly TAG_BYTES;
11
+ /**
12
+ * Largest ciphertext `decrypt` accepts, in bytes (1 MiB). Checked on the hex
13
+ * string before any decoding: without it, a payload of hundreds of MB was
14
+ * decoded in full before GCM refused it. Our largest real payload is an
15
+ * agent key, 66 characters.
16
+ */
17
+ static readonly MAX_CIPHERTEXT_BYTES: number;
18
+ /**
19
+ * Une clé invalide est une erreur de configuration de l'appelant, pas une
20
+ * entrée d'attaquant : elle se nomme au lieu de se fondre dans « Decryption
21
+ * failed », qui accusait le chiffré quand le secret manquait dans l'env.
22
+ * Le message ne cite jamais la clé.
23
+ */
24
+ private static validateKey;
25
+ /**
26
+ * Strict hex decoding. Buffer.from(hex) stops at the first invalid character
27
+ * without a word: a damaged field was read half-way, and a key followed by
28
+ * junk ("<64 hex>zz") was accepted as its first 32 bytes.
29
+ */
30
+ private static hexToBuffer;
31
+ /**
32
+ * Encrypts text using native Node.js crypto (AES-256-GCM)
33
+ */
34
+ static encrypt(text: string, key: Buffer | string): EncryptedData;
35
+ /**
36
+ * Decrypts a payload using native Node.js crypto (AES-256-GCM)
37
+ */
38
+ static decrypt(payload: EncryptedData, key: Buffer | string): string;
9
39
  }
@@ -1,41 +1,137 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
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
+ })();
5
35
  Object.defineProperty(exports, "__esModule", { value: true });
6
36
  exports.AesGcmUtil = void 0;
7
- const node_forge_1 = __importDefault(require("node-forge"));
37
+ const crypto = __importStar(require("crypto"));
8
38
  class AesGcmUtil {
9
- static encrypt(text, keyHex) {
10
- const key = node_forge_1.default.util.hexToBytes(keyHex);
11
- const iv = node_forge_1.default.random.getBytesSync(12); // IV standard GCM = 12 octets
12
- const cipher = node_forge_1.default.cipher.createCipher('AES-GCM', key);
13
- cipher.start({ iv: iv });
14
- cipher.update(node_forge_1.default.util.createBuffer(node_forge_1.default.util.encodeUtf8(text)));
15
- cipher.finish();
16
- return {
17
- data: node_forge_1.default.util.bytesToHex(cipher.output.getBytes()),
18
- iv: node_forge_1.default.util.bytesToHex(iv),
19
- tag: node_forge_1.default.util.bytesToHex(cipher.mode.tag.getBytes()),
20
- };
39
+ /**
40
+ * Une clé invalide est une erreur de configuration de l'appelant, pas une
41
+ * entrée d'attaquant : elle se nomme au lieu de se fondre dans « Decryption
42
+ * failed », qui accusait le chiffré quand le secret manquait dans l'env.
43
+ * Le message ne cite jamais la clé.
44
+ */
45
+ static validateKey(key) {
46
+ if (key === undefined || key === null || key.length === 0) {
47
+ throw new Error('Invalid key: missing or empty (is the secret configured?)');
48
+ }
49
+ let encryptionKey;
50
+ try {
51
+ encryptionKey = typeof key === 'string' ? this.hexToBuffer(key) : key;
52
+ }
53
+ catch {
54
+ throw new Error('Invalid key: not a hex string');
55
+ }
56
+ if (!Buffer.isBuffer(encryptionKey) || encryptionKey.length !== this.KEY_BYTES) {
57
+ throw new Error('Invalid key: must be 32 bytes (64 hex characters)');
58
+ }
59
+ return encryptionKey;
21
60
  }
22
- static decrypt(payload, keyHex) {
23
- const key = node_forge_1.default.util.hexToBytes(keyHex);
24
- const iv = node_forge_1.default.util.hexToBytes(payload.iv);
25
- const tag = node_forge_1.default.util.hexToBytes(payload.tag);
26
- const ciphertext = node_forge_1.default.util.hexToBytes(payload.data);
27
- const decipher = node_forge_1.default.cipher.createDecipher('AES-GCM', key);
28
- decipher.start({
29
- iv: iv,
30
- tag: node_forge_1.default.util.createBuffer(tag),
31
- });
32
- decipher.update(node_forge_1.default.util.createBuffer(ciphertext));
33
- // finish() renvoie 'false' si le tag est invalide (données corrompues ou mauvaise clé)
34
- const authenticated = decipher.finish();
35
- if (!authenticated) {
36
- throw new Error('Decryption failed: authentication failed (bad key or corrupted data)');
37
- }
38
- return node_forge_1.default.util.decodeUtf8(decipher.output.getBytes());
61
+ /**
62
+ * Strict hex decoding. Buffer.from(hex) stops at the first invalid character
63
+ * without a word: a damaged field was read half-way, and a key followed by
64
+ * junk ("<64 hex>zz") was accepted as its first 32 bytes.
65
+ */
66
+ static hexToBuffer(hex, expectedBytes) {
67
+ if (typeof hex !== 'string' || hex.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(hex)) {
68
+ throw new Error('Invalid hex string');
69
+ }
70
+ const buffer = Buffer.from(hex, 'hex');
71
+ if (expectedBytes !== undefined && buffer.length !== expectedBytes) {
72
+ throw new Error(`Expected ${expectedBytes} bytes, got ${buffer.length}`);
73
+ }
74
+ return buffer;
75
+ }
76
+ /**
77
+ * Encrypts text using native Node.js crypto (AES-256-GCM)
78
+ */
79
+ static encrypt(text, key) {
80
+ const encryptionKey = this.validateKey(key);
81
+ try {
82
+ const iv = crypto.randomBytes(this.IV_BYTES);
83
+ const cipher = crypto.createCipheriv(this.ALGORITHM, encryptionKey, iv, {
84
+ authTagLength: this.TAG_BYTES,
85
+ });
86
+ let encrypted = cipher.update(text, 'utf8', 'hex');
87
+ encrypted += cipher.final('hex');
88
+ return {
89
+ data: encrypted,
90
+ iv: iv.toString('hex'),
91
+ tag: cipher.getAuthTag().toString('hex'),
92
+ };
93
+ }
94
+ catch {
95
+ throw new Error('Encryption failed');
96
+ }
97
+ }
98
+ /**
99
+ * Decrypts a payload using native Node.js crypto (AES-256-GCM)
100
+ */
101
+ static decrypt(payload, key) {
102
+ const encryptionKey = this.validateKey(key);
103
+ try {
104
+ if (typeof payload.data !== 'string' || payload.data.length > 2 * this.MAX_CIPHERTEXT_BYTES) {
105
+ throw new Error('Ciphertext missing or above the size limit');
106
+ }
107
+ const iv = this.hexToBuffer(payload.iv, this.IV_BYTES);
108
+ const tag = this.hexToBuffer(payload.tag, this.TAG_BYTES);
109
+ const data = this.hexToBuffer(payload.data);
110
+ // Sans `authTagLength`, Node accepte un tag de 4 à 16 octets : mesuré sur
111
+ // la 1.0.3, un tag tronqué à 4 octets déchiffrait, et un tag forgé au
112
+ // hasard passait avec une chance sur 2^32 au lieu de 2^128.
113
+ const decipher = crypto.createDecipheriv(this.ALGORITHM, encryptionKey, iv, {
114
+ authTagLength: this.TAG_BYTES,
115
+ });
116
+ decipher.setAuthTag(tag);
117
+ let decrypted = decipher.update(data, undefined, 'utf8');
118
+ decrypted += decipher.final('utf8');
119
+ return decrypted;
120
+ }
121
+ catch {
122
+ throw new Error('Decryption failed');
123
+ }
39
124
  }
40
125
  }
41
126
  exports.AesGcmUtil = AesGcmUtil;
127
+ AesGcmUtil.ALGORITHM = 'aes-256-gcm';
128
+ AesGcmUtil.KEY_BYTES = 32;
129
+ AesGcmUtil.IV_BYTES = 12;
130
+ AesGcmUtil.TAG_BYTES = 16;
131
+ /**
132
+ * Largest ciphertext `decrypt` accepts, in bytes (1 MiB). Checked on the hex
133
+ * string before any decoding: without it, a payload of hundreds of MB was
134
+ * decoded in full before GCM refused it. Our largest real payload is an
135
+ * agent key, 66 characters.
136
+ */
137
+ AesGcmUtil.MAX_CIPHERTEXT_BYTES = 1024 * 1024;
@@ -1,4 +1,10 @@
1
1
  export declare class PureJwtUtil {
2
- static sign(payload: Record<string, any>, secret: string): string;
2
+ /**
3
+ * Verifies a JWT's signature and expiration, then returns the decoded payload.
4
+ */
3
5
  static verify<T extends object>(token: string, secret: string): T;
6
+ /**
7
+ * Signs a payload and returns a compact JWT string.
8
+ */
9
+ static sign(payload: Record<string, any>, secret: string): string;
4
10
  }
package/dist/jwt.util.js CHANGED
@@ -1,34 +1,117 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
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
+ })();
5
35
  Object.defineProperty(exports, "__esModule", { value: true });
6
36
  exports.PureJwtUtil = void 0;
7
- const crypto_js_1 = __importDefault(require("crypto-js"));
8
- const encoding_helper_1 = require("./encoding.helper");
37
+ const crypto = __importStar(require("crypto"));
9
38
  class PureJwtUtil {
10
- static sign(payload, secret) {
11
- const header = { alg: 'HS256', typ: 'JWT' };
12
- const encodedHeader = encoding_helper_1.EncodingHelper.objToBase64Url(header);
13
- const encodedPayload = encoding_helper_1.EncodingHelper.objToBase64Url(payload);
14
- const signature = crypto_js_1.default.HmacSHA256(`${encodedHeader}.${encodedPayload}`, secret);
15
- // Conversion du WordArray CryptoJS en Base64URL
16
- const encodedSignature = encoding_helper_1.EncodingHelper.toBase64Url(signature.toString(crypto_js_1.default.enc.Base64));
17
- return `${encodedHeader}.${encodedPayload}.${encodedSignature}`;
18
- }
39
+ /**
40
+ * Verifies a JWT's signature and expiration, then returns the decoded payload.
41
+ */
19
42
  static verify(token, secret) {
20
- const [headerB64, payloadB64, signatureB64] = token.split('.');
21
- if (!signatureB64)
43
+ assertSecret(secret);
44
+ const parts = token.split('.');
45
+ if (parts.length !== 3)
22
46
  throw new Error('Invalid token format');
23
- const expectedSignature = encoding_helper_1.EncodingHelper.toBase64Url(crypto_js_1.default.HmacSHA256(`${headerB64}.${payloadB64}`, secret).toString(crypto_js_1.default.enc.Base64));
24
- if (signatureB64 !== expectedSignature) {
47
+ const [headerB64, payloadB64, signatureB64] = parts;
48
+ const dataToVerify = `${headerB64}.${payloadB64}`;
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)) {
25
59
  throw new Error('Invalid signature');
26
60
  }
27
- const payload = JSON.parse(encoding_helper_1.EncodingHelper.fromBase64(payloadB64.replace(/-/g, '+').replace(/_/g, '/')));
28
- if (payload.exp && Date.now() >= payload.exp * 1000) {
29
- throw new Error('Token expired');
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) {
74
+ throw new Error('Token expired');
75
+ }
30
76
  }
31
77
  return payload;
32
78
  }
79
+ /**
80
+ * Signs a payload and returns a compact JWT string.
81
+ */
82
+ static sign(payload, secret) {
83
+ assertSecret(secret);
84
+ const header = { alg: 'HS256', typ: 'JWT' };
85
+ const encode = (obj) => Buffer.from(JSON.stringify(obj)).toString('base64url');
86
+ const encodedHeader = encode(header);
87
+ const encodedPayload = encode(payload);
88
+ const signature = crypto
89
+ .createHmac('sha256', secret)
90
+ .update(`${encodedHeader}.${encodedPayload}`)
91
+ .digest('base64url');
92
+ return `${encodedHeader}.${encodedPayload}.${signature}`;
93
+ }
33
94
  }
34
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,55 +1,58 @@
1
- {
2
- "name": "@syldel/crypto-utils",
3
- "version": "1.0.2",
4
- "description": "Lightweight JWT and AES-256-GCM utilities",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
7
- "files": [
8
- "dist"
9
- ],
10
- "scripts": {
11
- "lint": "eslint .",
12
- "lint:fix": "eslint . --fix",
13
- "clean": "rm -rf dist",
14
- "build": "npm run lint && npm run clean && tsc -p tsconfig.json",
15
- "test": "jest",
16
- "test:watch": "jest --watch",
17
- "prepublishOnly": "npm run build",
18
- "release": "npm run build && npm version patch && npm publish"
19
- },
20
- "publishConfig": {
21
- "access": "public"
22
- },
23
- "keywords": [
24
- "jwt",
25
- "aes",
26
- "crypto",
27
- "security",
28
- "nestjs"
29
- ],
30
- "author": "Syl. D.",
31
- "license": "ISC",
32
- "devDependencies": {
33
- "@eslint/js": "^10.0.1",
34
- "@types/crypto-js": "^4.2.2",
35
- "@types/jest": "^30.0.0",
36
- "@types/node-forge": "^1.3.14",
37
- "eslint": "^10.0.1",
38
- "eslint-config-prettier": "^10.1.8",
39
- "eslint-plugin-jsonc": "^3.0.0",
40
- "eslint-plugin-prettier": "^5.5.5",
41
- "jest": "^30.2.0",
42
- "jsonc-eslint-parser": "^3.1.0",
43
- "prettier": "^3.8.1",
44
- "ts-jest": "^29.4.6",
45
- "typescript": "^5.9.3",
46
- "typescript-eslint": "^8.56.0"
47
- },
48
- "overrides": {
49
- "minimatch": "^10.2.1"
50
- },
51
- "dependencies": {
52
- "crypto-js": "^4.2.0",
53
- "node-forge": "^1.3.3"
54
- }
55
- }
1
+ {
2
+ "name": "@syldel/crypto-utils",
3
+ "version": "1.1.0",
4
+ "description": "Lightweight JWT and 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
+ "aes",
34
+ "crypto",
35
+ "security",
36
+ "nestjs"
37
+ ],
38
+ "author": "Syl. D.",
39
+ "license": "ISC",
40
+ "devDependencies": {
41
+ "@eslint/js": "^10.0.1",
42
+ "@types/jest": "^30.0.0",
43
+ "@types/node": "^20.19.43",
44
+ "eslint": "^10.0.1",
45
+ "eslint-config-prettier": "^10.1.8",
46
+ "eslint-plugin-jsonc": "^3.0.0",
47
+ "eslint-plugin-prettier": "^5.5.5",
48
+ "jest": "^30.2.0",
49
+ "jsonc-eslint-parser": "^3.1.0",
50
+ "prettier": "^3.8.1",
51
+ "ts-jest": "^29.4.6",
52
+ "typescript": "^5.9.3",
53
+ "typescript-eslint": "^8.56.0"
54
+ },
55
+ "overrides": {
56
+ "minimatch": "^10.2.1"
57
+ }
58
+ }
@@ -1,9 +0,0 @@
1
- /**
2
- * Encode et décode en Base64URL sans dépendance à Buffer (Universal JS)
3
- */
4
- export declare const EncodingHelper: {
5
- toBase64: (str: string) => string;
6
- fromBase64: (base64: string) => string;
7
- toBase64Url: (base64: string) => string;
8
- objToBase64Url: (obj: object) => string;
9
- };
@@ -1,25 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.EncodingHelper = void 0;
4
- /**
5
- * Encode et décode en Base64URL sans dépendance à Buffer (Universal JS)
6
- */
7
- exports.EncodingHelper = {
8
- // Texte -> Base64
9
- toBase64: (str) => {
10
- return btoa(unescape(encodeURIComponent(str)));
11
- },
12
- // Base64 -> Texte
13
- fromBase64: (base64) => {
14
- return decodeURIComponent(escape(atob(base64)));
15
- },
16
- // Base64 -> Base64URL (pour JWT)
17
- toBase64Url: (base64) => {
18
- return base64.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
19
- },
20
- // Object -> Base64URL
21
- objToBase64Url: (obj) => {
22
- const str = JSON.stringify(obj);
23
- return exports.EncodingHelper.toBase64Url(exports.EncodingHelper.toBase64(str));
24
- },
25
- };