@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 +4 -4
- package/README.md +50 -24
- package/dist/aes-gcm.util.d.ts +32 -2
- package/dist/aes-gcm.util.js +129 -33
- package/dist/jwt.util.d.ts +7 -1
- package/dist/jwt.util.js +104 -21
- package/package.json +58 -55
- package/dist/encoding.helper.d.ts +0 -9
- package/dist/encoding.helper.js +0 -25
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
|
|
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
|
-
|
|
5
|
+
## 🚀 Key Features
|
|
6
|
+
- **Native Security**: Built on top of Node.js crypto (OpenSSL) for hardware-accelerated encryption.
|
|
7
7
|
|
|
8
|
-
- **
|
|
8
|
+
- **AES-256-GCM**: Industry-standard authenticated encryption (AEAD) to ensure both confidentiality and data integrity.
|
|
9
9
|
|
|
10
|
-
- **
|
|
10
|
+
- **Lightweight JWT**: Pure implementation for signing and verifying tokens without heavy dependencies.
|
|
11
11
|
|
|
12
|
-
- **
|
|
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
|
|
17
|
-
|
|
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
|
|
23
|
-
const message = "
|
|
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,
|
|
27
|
-
|
|
26
|
+
const encrypted = AesGcmUtil.encrypt(message, keyHex);
|
|
27
|
+
// Returns: { data: '...', iv: '...', tag: '...' }
|
|
28
28
|
|
|
29
29
|
// Decrypt
|
|
30
|
-
|
|
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
|
-
|
|
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 = "
|
|
40
|
-
const payload = { sub: "
|
|
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
|
-
|
|
54
|
+
// Handles "Token expired" or "Invalid signature"
|
|
50
55
|
}
|
|
51
56
|
```
|
|
52
57
|
|
|
53
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
|
package/dist/aes-gcm.util.d.ts
CHANGED
|
@@ -4,6 +4,36 @@ export interface EncryptedData {
|
|
|
4
4
|
tag: string;
|
|
5
5
|
}
|
|
6
6
|
export declare class AesGcmUtil {
|
|
7
|
-
static
|
|
8
|
-
static
|
|
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
|
}
|
package/dist/aes-gcm.util.js
CHANGED
|
@@ -1,41 +1,137 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var
|
|
3
|
-
|
|
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
|
|
37
|
+
const crypto = __importStar(require("crypto"));
|
|
8
38
|
class AesGcmUtil {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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;
|
package/dist/jwt.util.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
export declare class PureJwtUtil {
|
|
2
|
-
|
|
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
|
|
3
|
-
|
|
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
|
|
8
|
-
const encoding_helper_1 = require("./encoding.helper");
|
|
37
|
+
const crypto = __importStar(require("crypto"));
|
|
9
38
|
class PureJwtUtil {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
21
|
-
|
|
43
|
+
assertSecret(secret);
|
|
44
|
+
const parts = token.split('.');
|
|
45
|
+
if (parts.length !== 3)
|
|
22
46
|
throw new Error('Invalid token format');
|
|
23
|
-
const
|
|
24
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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
|
-
"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
"
|
|
53
|
-
"
|
|
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
|
-
};
|
package/dist/encoding.helper.js
DELETED
|
@@ -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
|
-
};
|