Skip to content

Commit f683b93

Browse files
committed
fix(security): implement private fields and enhance input validation 🔒
- Add DATA_EMPTY_STRING error message for better error handling - Add empty string validation to prevent invalid token creation - Bump version to 1.1.1 for security patch release - Convert all sensitive properties to private fields (#secret, #payloadCache, #verifyCache, #expireInMs, #version) - Fix cache poisoning vulnerability by making caches truly private - Fix memory dump vulnerability by preventing external access to sensitive data - Update changelog with comprehensive v1.1.1 documentation - Update error count in tests to reflect new error message
1 parent 713dd13 commit f683b93

File tree

6 files changed

+64
-31
lines changed

6 files changed

+64
-31
lines changed

CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
---
99

10+
## [1.1.1] - 2025-09-06
11+
12+
### Security
13+
- Fixed memory dump vulnerability by implementing private fields
14+
- Fixed cache poisoning vulnerability by making caches private
15+
- Enhanced input validation to reject empty strings
16+
- Converted all sensitive properties to private fields (#secret, #payloadCache, #verifyCache, #expireInMs, #version)
17+
18+
### Bug Fixes
19+
- Added validation for empty string inputs
20+
- Improved error handling and messages
21+
- Enhanced code encapsulation and security
22+
23+
### Documentation
24+
- Added TypeScript badge to README
25+
- Fixed license badge URL with proper .svg extension
26+
- Enhanced usage examples with version parameter
27+
- Added examples for different data types (strings, numbers, arrays)
28+
- Added Mermaid architecture diagrams for JWT encoding process
29+
- Added security layers visualization
30+
- Improved README structure and readability
31+
32+
### Internal
33+
- Better encapsulation of sensitive data
34+
- Improved security posture
35+
- Enhanced error message coverage
36+
37+
---
38+
1039
## [1.1.0] - 2025-09-06
1140

1241
### Added

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@neabyte/secure-jwt",
33
"description": "Secure JWT with AES-256-GCM encryption, built-in caching, tamper detection, and TypeScript support",
4-
"version": "1.1.0",
4+
"version": "1.1.1",
55
"type": "module",
66
"main": "./dist/index.js",
77
"types": "./dist/index.d.ts",

src/index.ts

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ import {
2121
*/
2222
export default class SecureJWT {
2323
/** Secret key for encryption */
24-
private readonly secret: Buffer
24+
readonly #secret: Buffer
2525
/** Expiration time in milliseconds */
26-
private readonly expireInMs: number
26+
readonly #expireInMs: number
2727
/** Token version */
28-
private readonly version: string
28+
readonly #version: string
2929
/** Cache for decrypted payload data */
30-
private readonly payloadCache: Cache<unknown>
30+
readonly #payloadCache: Cache<unknown>
3131
/** Cache for token verification results */
32-
private readonly verifyCache: Cache<boolean>
32+
readonly #verifyCache: Cache<boolean>
3333

3434
/**
3535
* Creates a new SecureJWT instance
@@ -44,11 +44,11 @@ export default class SecureJWT {
4444
if (options.version !== undefined) {
4545
ErrorHandler.validateVersion(options.version)
4646
}
47-
this.secret = this.generateSecret(options.secret)
48-
this.expireInMs = parsetimeToMs(options.expireIn)
49-
this.version = options.version ?? '1.0.0'
50-
this.payloadCache = new Cache<unknown>(options.cached ?? 1000, this.expireInMs)
51-
this.verifyCache = new Cache<boolean>(options.cached ?? 1000, this.expireInMs)
47+
this.#secret = this.generateSecret(options.secret)
48+
this.#expireInMs = parsetimeToMs(options.expireIn)
49+
this.#version = options.version ?? '1.0.0'
50+
this.#payloadCache = new Cache<unknown>(options.cached ?? 1000, this.#expireInMs)
51+
this.#verifyCache = new Cache<boolean>(options.cached ?? 1000, this.#expireInMs)
5252
}
5353

5454
/**
@@ -72,10 +72,10 @@ export default class SecureJWT {
7272
private encrypt(data: string): TokenEncrypted {
7373
ErrorHandler.validateEncryptionData(data)
7474
const iv = randomBytes(16)
75-
const key = this.secret.subarray(0, 32)
75+
const key = this.#secret.subarray(0, 32)
7676
ErrorHandler.validateKeyLength(key)
7777
const cipher = createCipheriv('aes-256-gcm', key, iv)
78-
cipher.setAAD(Buffer.from(`secure-jwt-${this.version}`, 'utf8'))
78+
cipher.setAAD(Buffer.from(`secure-jwt-${this.#version}`, 'utf8'))
7979
let encrypted = cipher.update(data, 'utf8', 'hex')
8080
encrypted += cipher.final('hex')
8181
const tag = cipher.getAuthTag()
@@ -95,12 +95,12 @@ export default class SecureJWT {
9595
private decrypt(tokenEncrypted: TokenEncrypted): string {
9696
try {
9797
ErrorHandler.validateTokenEncrypted(tokenEncrypted)
98-
const key = this.secret.subarray(0, 32)
98+
const key = this.#secret.subarray(0, 32)
9999
ErrorHandler.validateKeyLength(key)
100100
ErrorHandler.validateIVFormat(tokenEncrypted.iv)
101101
ErrorHandler.validateTagFormat(tokenEncrypted.tag)
102102
const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(tokenEncrypted.iv, 'hex'))
103-
decipher.setAAD(Buffer.from(`secure-jwt-${this.version}`, 'utf8'))
103+
decipher.setAAD(Buffer.from(`secure-jwt-${this.#version}`, 'utf8'))
104104
decipher.setAuthTag(Buffer.from(tokenEncrypted.tag, 'hex'))
105105
let decrypted = decipher.update(tokenEncrypted.encrypted, 'hex', 'utf8')
106106
decrypted += decipher.final('utf8')
@@ -124,14 +124,14 @@ export default class SecureJWT {
124124
try {
125125
ErrorHandler.validateData(data)
126126
const now = Math.floor(Date.now() / 1000)
127-
const exp = now + Math.floor(this.expireInMs / 1000)
127+
const exp = now + Math.floor(this.#expireInMs / 1000)
128128
const maxExp = now + 365 * 24 * 60 * 60
129129
ErrorHandler.validateExpiration(exp, maxExp)
130130
const payload: PayloadData = {
131131
data,
132132
exp,
133133
iat: now,
134-
version: this.version
134+
version: this.#version
135135
}
136136
const payloadString = JSON.stringify(payload)
137137
ErrorHandler.validatePayloadSize(payloadString)
@@ -142,7 +142,7 @@ export default class SecureJWT {
142142
tag: tokenEncrypted.tag,
143143
exp,
144144
iat: now,
145-
version: this.version
145+
version: this.#version
146146
}
147147
const tokenString = JSON.stringify(tokenData)
148148
return Buffer.from(tokenString).toString('base64')
@@ -167,8 +167,8 @@ export default class SecureJWT {
167167
*/
168168
verify(token: string): boolean {
169169
try {
170-
if (this.verifyCache.has(token)) {
171-
const cachedResult = this.verifyCache.get(token)
170+
if (this.#verifyCache.has(token)) {
171+
const cachedResult = this.#verifyCache.get(token)
172172
if (cachedResult !== undefined) {
173173
return cachedResult
174174
}
@@ -185,10 +185,10 @@ export default class SecureJWT {
185185
)
186186
ErrorHandler.validateTokenDataIntegrity(tokenData)
187187
if (!isValidTokenData(tokenData)) {
188-
this.verifyCache.set(token, false, 0)
188+
this.#verifyCache.set(token, false, 0)
189189
return false
190190
}
191-
ErrorHandler.validateVersionCompatibility(tokenData.version, this.version)
191+
ErrorHandler.validateVersionCompatibility(tokenData.version, this.#version)
192192
ErrorHandler.checkTokenExpiration(tokenData.exp)
193193
const tokenEncrypted: TokenEncrypted = {
194194
encrypted: tokenData.encrypted,
@@ -201,16 +201,16 @@ export default class SecureJWT {
201201
getErrorMessage('INVALID_PAYLOAD_STRUCTURE')
202202
)
203203
if (!isValidPayloadData(payload)) {
204-
this.verifyCache.set(token, false, 0)
204+
this.#verifyCache.set(token, false, 0)
205205
return false
206206
}
207207
ErrorHandler.validateVersionCompatibility(payload.version, tokenData.version)
208208
ErrorHandler.checkTokenExpiration(payload.exp)
209209
ErrorHandler.validateTokenTimestamps(payload.exp, tokenData.exp, payload.iat, tokenData.iat)
210-
this.verifyCache.set(token, true, Math.max(0, payload.exp * 1000 - Date.now()))
210+
this.#verifyCache.set(token, true, Math.max(0, payload.exp * 1000 - Date.now()))
211211
return true
212212
} catch {
213-
this.verifyCache.set(token, false, 0)
213+
this.#verifyCache.set(token, false, 0)
214214
return false
215215
}
216216
}
@@ -238,7 +238,7 @@ export default class SecureJWT {
238238
if (!isValidTokenData(tokenData)) {
239239
throw new ValidationError(getErrorMessage('INVALID_TOKEN_DATA_STRUCTURE'))
240240
}
241-
ErrorHandler.validateVersionCompatibility(tokenData.version, this.version)
241+
ErrorHandler.validateVersionCompatibility(tokenData.version, this.#version)
242242
ErrorHandler.checkTokenExpiration(tokenData.exp)
243243
const tokenEncrypted: TokenEncrypted = {
244244
encrypted: tokenData.encrypted,
@@ -269,8 +269,8 @@ export default class SecureJWT {
269269
*/
270270
decode(token: string): unknown {
271271
try {
272-
if (this.payloadCache.has(token)) {
273-
const cachedResult = this.payloadCache.get(token)
272+
if (this.#payloadCache.has(token)) {
273+
const cachedResult = this.#payloadCache.get(token)
274274
if (cachedResult !== undefined) {
275275
return cachedResult
276276
}
@@ -289,7 +289,7 @@ export default class SecureJWT {
289289
if (!isValidTokenData(tokenData)) {
290290
throw new ValidationError(getErrorMessage('INVALID_TOKEN_DATA_STRUCTURE'))
291291
}
292-
ErrorHandler.validateVersionCompatibility(tokenData.version, this.version)
292+
ErrorHandler.validateVersionCompatibility(tokenData.version, this.#version)
293293
ErrorHandler.checkTokenExpiration(tokenData.exp)
294294
const tokenEncrypted: TokenEncrypted = {
295295
encrypted: tokenData.encrypted,
@@ -307,7 +307,7 @@ export default class SecureJWT {
307307
ErrorHandler.validateVersionCompatibility(payload.version, tokenData.version)
308308
ErrorHandler.checkTokenExpiration(payload.exp)
309309
ErrorHandler.validateTokenTimestamps(payload.exp, tokenData.exp, payload.iat, tokenData.iat)
310-
this.payloadCache.set(token, payload.data, Math.max(0, payload.exp * 1000 - Date.now()))
310+
this.#payloadCache.set(token, payload.data, Math.max(0, payload.exp * 1000 - Date.now()))
311311
return payload.data
312312
} catch (error) {
313313
if (

src/utils/ErrorHandler.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ export class ErrorHandler {
5353
if (data === null || data === undefined) {
5454
throw new ValidationError(getErrorMessage('DATA_NULL_UNDEFINED'))
5555
}
56+
if (typeof data === 'string' && data.length === 0) {
57+
throw new ValidationError(getErrorMessage('DATA_EMPTY_STRING'))
58+
}
5659
}
5760

5861
/**

src/utils/ErrorMap.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const errorCodes = {
1919
export const errorMessages = {
2020
// Data validation errors
2121
DATA_NULL_UNDEFINED: 'Data cannot be null or undefined',
22+
DATA_EMPTY_STRING: 'Data cannot be an empty string',
2223
DATA_INVALID_TYPE: 'Invalid data type provided',
2324
DATA_NON_EMPTY_STRING: 'Data must be a non-empty string',
2425

tests/ErrorMap.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ describe('ErrorMap', () => {
7575
})
7676

7777
it('should have consistent structure', () => {
78-
expect(Object.keys(errorMessages)).toHaveLength(59)
78+
expect(Object.keys(errorMessages)).toHaveLength(60)
7979
})
8080

8181
it('should have all messages as strings', () => {

0 commit comments

Comments
 (0)