timebasedcipher 3.0.0 → 3.0.1

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/README.md CHANGED
@@ -1,13 +1,14 @@
1
1
  # timebasedcipher
2
2
 
3
- A lightweight, isomorphic (**Node + Browser**) library for **time-based rotating AES encryption** with optional **JWT-based key validation**.
3
+ A lightweight, isomorphic (**Node + Browser**) library for **time-based rotating AES encryption with integrity protection**.
4
4
 
5
- Built with [`crypto-js`](https://www.npmjs.com/package/crypto-js), this package provides a simple interface to:
5
+ Built with [`crypto-js`](https://www.npmjs.com/package/crypto-js), this package provides:
6
6
 
7
- - Generate **time-based rotating AES keys**
8
- - Encrypt and decrypt JavaScript objects or strings
9
- - Validate JWTs before key derivation (optional)
10
- - Works seamlessly in both **frontend** and **backend** environments
7
+ - **Time-based rotating AES-256 keys**
8
+ - **AES-256-CBC encryption**
9
+ -️ **HMAC-SHA256 integrity & tamper detection**
10
+ - **Application-level domain separation (`appName`)**
11
+ - Works in **Node.js and browser environments**
11
12
 
12
13
  ---
13
14
 
@@ -29,180 +30,158 @@ pnpm add timebasedcipher crypto-js
29
30
  import { generateKey, encrypt, decrypt } from "timebasedcipher";
30
31
 
31
32
  const sharedSecret = "mySuperSecret";
32
- const intervalSeconds = 60; // key changes every 60 seconds
33
+ const intervalSeconds = 60; // key rotates every 60 seconds
33
34
 
34
- // Step 1: Generate a rotating key
35
+ // Generate a rotating key
35
36
  const key = generateKey(sharedSecret, intervalSeconds);
36
- console.log("Generated Key:", key);
37
37
 
38
- // Step 2: Encrypt some data
38
+ // Encrypt data
39
39
  const data = { user: "Deb", role: "admin", timestamp: Date.now() };
40
40
  const cipher = encrypt(data, key);
41
- console.log("Encrypted:", cipher);
42
41
 
43
- // Step 3: Decrypt it
42
+ // Decrypt data
44
43
  const decrypted = decrypt(cipher, key);
45
- console.log("Decrypted:", decrypted);
44
+
45
+ console.log(decrypted);
46
46
  ```
47
47
 
48
- Works in both **Node.js** and **browser** environments.
48
+ Works seamlessly in both **Node.js** and **browser** environments.
49
49
 
50
50
  ---
51
51
 
52
52
  ## Function Reference
53
53
 
54
- ### `generateKey(sharedSecretOrJwt: string, interval: number, isJwt?: boolean): string`
55
-
56
- Generates a **SHA-256 key** that rotates every `interval` seconds.
57
- When `isJwt` is set to `true`, the input is treated as a **JWT string** — its `exp` claim is validated before key derivation.
58
-
59
- | Parameter | Type | Description |
60
- | -------------------- | --------- | --------------------------------------------------------------------------- |
61
- | `sharedSecretOrJwt` | `string` | A pre-shared secret or a JWT string when `isJwt = true`. |
62
- | `interval` | `number` | Time in seconds after which the key rotates. |
63
- | `isJwt` _(optional)_ | `boolean` | When `true`, validates JWT expiry and uses the raw JWT as the secret input. |
64
-
65
- **Returns:** `string` — a 64-character hex string (256-bit key).
54
+ ---
66
55
 
67
- **Throws:**
56
+ ### `generateKey(sharedSecretOrJwt: string, interval: number): string`
68
57
 
69
- - `Error("Invalid JWT: missing payload part")` — malformed token
70
- - `Error("JWT payload does not contain a valid 'exp' claim")`
71
- - `Error("JWT is expired")`
58
+ Generates a **SHA-256–derived rotating key** that changes every `interval` seconds.
72
59
 
73
- #### Example (with JWT validation)
60
+ - The input can be a **shared secret** or a **JWT string**
61
+ - JWTs are treated as **opaque entropy**
62
+ - **JWT validation (exp, signature)** is expected to be handled upstream
74
63
 
75
- ```ts
76
- const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."; // your JWT
77
- const interval = 120;
64
+ #### Parameters
78
65
 
79
- const key = generateKey(jwt, interval, true); // validates exp claim
80
- ```
66
+ | Name | Type | Description |
67
+ | ------------------- | -------- | ----------------------------- |
68
+ | `sharedSecretOrJwt` | `string` | A shared secret or JWT string |
69
+ | `interval` | `number` | Rotation interval in seconds |
81
70
 
82
- ---
71
+ #### Returns
83
72
 
84
- ### `encrypt(data: any, encryptionKeyHex: string): string`
73
+ - `string` 64-character hex string (32-byte AES-256 key)
85
74
 
86
- Encrypts any serializable JavaScript object or string using **AES-256-CBC**.
75
+ #### Key Derivation
87
76
 
88
- | Parameter | Type | Description |
89
- | ------------------ | -------- | ------------------------------------------------- |
90
- | `data` | `any` | The object or value to encrypt. |
91
- | `encryptionKeyHex` | `string` | 64-character hex string key (from `generateKey`). |
77
+ ```text
78
+ SHA256(`${secretInput}:${timeSlot}`)
79
+ ```
92
80
 
93
- **Returns:**
94
- A string in format:
81
+ Where:
95
82
 
96
- ```
97
- cipherHex:ivHex
83
+ ```text
84
+ timeSlot = floor(Date.now() / (interval * 1000) + 1000)
98
85
  ```
99
86
 
100
87
  ---
101
88
 
102
- ### `decrypt(cipherText: string, encryptionKeyHex: string): any`
89
+ ### `encrypt(data: any, encryptionKeyHex: string, appName?: string): string`
103
90
 
104
- Decrypts ciphertext previously produced by `encrypt`.
91
+ Encrypts data using **AES-256-CBC** and appends an **HMAC-SHA256** for integrity.
105
92
 
106
- | Parameter | Type | Description |
107
- | ------------------ | -------- | ------------------------------------------------------- |
108
- | `cipherText` | `string` | Ciphertext in `"cipherHex:ivHex"` format. |
109
- | `encryptionKeyHex` | `string` | 64-character hex string key (same key used to encrypt). |
93
+ #### Parameters
110
94
 
111
- **Returns:**
112
- The decrypted JavaScript value (automatically parsed from JSON).
95
+ | Name | Type | Description |
96
+ | ---------------------- | -------- | ---------------------------------- |
97
+ | `data` | `any` | Any JSON-serializable value |
98
+ | `encryptionKeyHex` | `string` | 64-char hex key from `generateKey` |
99
+ | `appName` _(optional)_ | `string` | Application context |
113
100
 
114
- **Throws:**
101
+ #### Returns
115
102
 
116
- - `Error("Invalid cipher format, expected cipherHex:ivHex")`
117
- - `Error("Decryption produced empty string")`
118
- - `Error("Unable to decrypt: no valid key found or invalid payload")`
103
+ A ciphertext string in the format:
119
104
 
120
- ---
105
+ ```text
106
+ cipherHex:ivHex:hmacHex
107
+ ```
121
108
 
122
- ## ⚙️ How It Works
109
+ ---
123
110
 
124
- - The key **rotates** every defined interval (e.g., every 60 seconds)
111
+ ### `decrypt(cipherText: string, encryptionKeyHex: string, appName?: string): any`
125
112
 
126
- - Derived using:
113
+ Decrypts and verifies ciphertext produced by `encrypt`.
127
114
 
128
- ```
129
- SHA256(`${secretInput}:${timeSlot}`)
130
- ```
115
+ #### Parameters
131
116
 
132
- where
133
- `timeSlot = floor(Date.now() / (interval * 1000) + 1000)`
117
+ | Name | Type | Description |
118
+ | ---------------------- | -------- | -------------------------------------------- |
119
+ | `cipherText` | `string` | Encrypted string (`cipherHex:ivHex:hmacHex`) |
120
+ | `encryptionKeyHex` | `string` | Same key used during encryption |
121
+ | `appName` _(optional)_ | `string` | Must match encryption appName |
134
122
 
135
- - AES encryption uses:
123
+ #### Returns
136
124
 
137
- - **AES-256-CBC**
138
- - **PKCS#7 padding**
139
- - **Random 16-byte IV**
125
+ - Original decrypted JavaScript value (parsed from JSON)
140
126
 
141
- - Encrypted output format:
127
+ #### Throws
142
128
 
143
- ```
144
- <cipherHex>:<ivHex>
145
- ```
129
+ - `Error("Invalid cipher format")`
130
+ - `Error("Invalid HMAC: ciphertext may be tampered")`
131
+ - `Error("Unable to decrypt")`
146
132
 
147
- **Both ends** must:
133
+ ---
148
134
 
149
- - Use the **same secret** (or JWT)
150
- - Use the **same rotation interval**
151
- - Have **roughly synchronized clocks** (within ±1 interval)
135
+ ## Application Context (`appName`)
152
136
 
153
- ---
137
+ `appName` provides **cryptographic domain separation**.
154
138
 
155
- ## Handling Key Rotation Drift
139
+ - Prevents ciphertexts from being reused across apps
140
+ - Prevents MAC key reuse
156
141
 
157
- If decryption fails due to slight clock drift, try nearby intervals:
142
+ ### Example
158
143
 
159
144
  ```ts
160
- const now = Date.now();
161
- const interval = 60;
162
- const secret = "mySuperSecret";
163
-
164
- const currentKey = generateKey(secret, interval);
165
- const prevKey = generateKey(secret, interval); // simulate earlier slot
166
- const nextKey = generateKey(secret, interval); // simulate next slot
167
-
168
- try {
169
- return decrypt(cipher, currentKey);
170
- } catch {
171
- try {
172
- return decrypt(cipher, prevKey);
173
- } catch {
174
- return decrypt(cipher, nextKey);
175
- }
176
- }
145
+ const key = generateKey(secret, 60);
146
+
147
+ const cipher = encrypt(data, key, "billing-service");
148
+ const plain = decrypt(cipher, key, "billing-service");
177
149
  ```
178
150
 
151
+ Using a different `appName` will **fail decryption**.
152
+
179
153
  ---
180
154
 
181
155
  ## Browser + Node Compatibility
182
156
 
183
- | Environment | Supported | Notes |
184
- | --------------- | --------- | ------------------------------------------ |
185
- | Node.js | ✅ | Uses `crypto-js`, no native crypto needed |
186
- | React / Browser | ✅ | Fully supported |
187
- | Next.js | ✅ | Works in both server and client components |
157
+ | Environment | Supported | Notes |
158
+ | --------------- | --------- | -------------------------- |
159
+ | Node.js | ✅ | Uses `crypto-js` |
160
+ | Browser | ✅ | Fully supported |
161
+ | React / Next.js | ✅ | Works client & server-side |
188
162
 
189
163
  ---
190
164
 
191
165
  ## Security Notes
192
166
 
193
- - AES-256-CBC provides **confidentiality**, not integrity — consider adding an **HMAC** if you need tamper detection.
194
- - Avoid embedding secrets in frontend code — end users can access them.
195
- - JWT mode only validates the `exp` claim.
167
+ - Uses **Encrypt-then-MAC** (AES + HMAC) secure against tampering
168
+ - `appName` ensures **cross-application isolation**
169
+ - JWTs are treated as entropy only **authentication must happen elsewhere**
170
+ - Do **not** embed secrets in frontend code
171
+ - Requires roughly synchronized clocks
196
172
 
197
173
  ---
198
174
 
199
175
  ## Requirements
200
176
 
201
- - Node.js ≥ 14 or any modern browser
202
- - `crypto-js` dependency (installed automatically)
177
+ - Node.js ≥ 14 or modern browser
178
+ - `crypto-js` dependency
203
179
 
204
180
  ---
205
181
 
206
182
  ## License
207
183
 
208
- MIT License © 2025 [Deb Kalyan Mohanty](https://github.com/debkalyanmohanty)
184
+ MIT License © 2026
185
+ [**Deb Kalyan Mohanty**](https://github.com/debkalyanmohanty)
186
+
187
+ ---
package/dist/index.d.ts CHANGED
@@ -1,62 +1,38 @@
1
1
  /**
2
- * Generate a time-based rotating key (SHA256 hash of sharedSecret + current interval).
2
+ * Generate a time-based rotating key using SHA-256.
3
3
  *
4
- * The key is derived from:
4
+ * The key is derived as:
5
5
  * SHA256(`${secretInput}:${timeSlot}`)
6
- * where:
7
- * timeSlot = floor(Date.now() / (interval * 1000) + 1000)
8
6
  *
9
- * When `isJwt` is true:
10
- * - `sharedSecretOrJwt` is treated as a JWT string
11
- * - The JWT payload is decoded
12
- * - The `exp` claim (if present) is checked against current time
13
- * - If the JWT is expired, an error is thrown and no key is generated
14
- * - The *raw JWT string* is still used as the secret input for key derivation
15
- *
16
- * @param sharedSecretOrJwt - A pre-shared secret string, or a JWT when `isJwt` is true.
17
- * @param interval - The rotation interval in seconds. The timeSlot calculation
18
- * is based on this value, so both sides must use the same interval.
19
- * @param isJwt - Optional flag (default: false). When true, `sharedSecretOrJwt` is
20
- * treated as a JWT and its `exp` is validated before deriving the key.
21
- * @returns A 64-character hexadecimal SHA-256 hash string to be used as an AES-256 key.
22
- * @throws If `isJwt` is true and the JWT is malformed or expired.
7
+ * Notes:
8
+ * - If `sharedSecretOrJwt` is a JWT, it is treated as opaque input
9
+ * - JWT validation (signature / exp) is assumed to be handled elsewhere
10
+ * - This function is purely a key-derivation utility
23
11
  */
24
- export declare const generateKey: (sharedSecretOrJwt: string, interval: number, isJwt?: boolean) => string;
12
+ export declare const generateKey: (sharedSecretOrJwt: string, interval: number) => string;
25
13
  /**
26
- * Encrypt an arbitrary JavaScript value using AES-256-CBC with a random IV.
14
+ * Encrypt arbitrary data using:
15
+ * - AES-256-CBC (confidentiality)
16
+ * - HMAC-SHA256 (integrity & authenticity)
27
17
  *
28
- * The function:
29
- * - JSON stringifies the input `data`
30
- * - Uses the provided hex-encoded key as AES-256 key material
31
- * - Generates a random 16-byte IV
32
- * - Encrypts using AES-256-CBC
33
- * - Returns a concatenated string in the format: "cipherHex:ivHex"
18
+ * Cipher format:
19
+ * cipherHex:ivHex:hmacHex
34
20
  *
35
- * @param data - Any serializable JavaScript value (e.g. object, array, string) to encrypt.
36
- * @param encryptionKeyHex - A 64-character hex string representing a 32-byte key
37
- * (typically the output of {@link generateKey}).
38
- * @returns A string in the format `"cipherHex:ivHex"` where both parts are hex-encoded.
39
- * @throws If encryption fails or JSON serialization fails.
21
+ * `appName`:
22
+ * - Included ONLY in HMAC key derivation
23
+ * - Ensures ciphertexts are bound to an application context
24
+ * - Must match during decryption
40
25
  */
41
- export declare const encrypt: (data: any, encryptionKeyHex: string) => string;
26
+ export declare const encrypt: (data: any, encryptionKeyHex: string, appName?: string) => string;
42
27
  /**
43
- * Decrypt an AES-256-CBC ciphertext string produced by {@link encrypt}.
44
- *
45
- * The function expects the input to be in the format:
46
- * "cipherHex:ivHex"
47
- * where:
48
- * - cipherHex is the hex-encoded ciphertext
49
- * - ivHex is the hex-encoded 16-byte IV
28
+ * Decrypt data encrypted by {@link encrypt}.
50
29
  *
51
- * It will:
52
- * - Split and parse the hex components
53
- * - Decrypt using AES-256-CBC with the provided key
54
- * - Parse the resulting UTF-8 JSON back into the original JavaScript value
30
+ * Steps:
31
+ * 1. Parse cipher format
32
+ * 2. Recompute and verify HMAC (tamper detection)
33
+ * 3. Decrypt AES-256-CBC
34
+ * 4. Parse JSON payload
55
35
  *
56
- * @param cipherText - The ciphertext string in the format `"cipherHex:ivHex"`.
57
- * @param encryptionKeyHex - A 64-character hex string representing a 32-byte key
58
- * (must match the key used for encryption).
59
- * @returns The decrypted JavaScript value (e.g. object, array, string).
60
- * @throws If the format is invalid, decryption fails, or the decrypted output is empty/invalid JSON.
36
+ * `appName` MUST match the value used during encryption.
61
37
  */
62
- export declare const decrypt: (cipherText: string, encryptionKeyHex: string) => unknown;
38
+ export declare const decrypt: (cipherText: string, encryptionKeyHex: string, appName?: string) => unknown;
package/dist/index.js CHANGED
@@ -7,11 +7,9 @@ exports.decrypt = exports.encrypt = exports.generateKey = void 0;
7
7
  const crypto_js_1 = __importDefault(require("crypto-js"));
8
8
  /**
9
9
  * Decode the payload of a JWT without verifying its signature.
10
- * Used only to read the `exp` claim (in seconds since epoch).
10
+ * Used only to read the `exp` claim (seconds since epoch).
11
11
  *
12
- * @param token - The JWT string (header.payload.signature)
13
- * @returns Parsed payload object.
14
- * @throws If the token is malformed or payload is not valid JSON.
12
+ * ⚠️ This does NOT verify authenticity — only structure and expiry.
15
13
  */
16
14
  const decodeJwtPayload = (token) => {
17
15
  const parts = token.split(".");
@@ -20,7 +18,6 @@ const decodeJwtPayload = (token) => {
20
18
  }
21
19
  const base64Url = parts[1];
22
20
  const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
23
- // Use CryptoJS to decode base64 → UTF-8
24
21
  const wordArray = crypto_js_1.default.enc.Base64.parse(base64);
25
22
  const json = wordArray.toString(crypto_js_1.default.enc.Utf8);
26
23
  try {
@@ -31,112 +28,96 @@ const decodeJwtPayload = (token) => {
31
28
  }
32
29
  };
33
30
  /**
34
- * Generate a time-based rotating key (SHA256 hash of sharedSecret + current interval).
31
+ * Derive a dedicated HMAC key from the encryption key.
35
32
  *
36
- * The key is derived from:
37
- * SHA256(`${secretInput}:${timeSlot}`)
38
- * where:
39
- * timeSlot = floor(Date.now() / (interval * 1000) + 1000)
33
+ * `appName` acts as cryptographic context (domain separation):
34
+ * - Different apps using the same encryption key will NOT share MAC keys
35
+ * - Prevents cross-application replay or substitution attacks
36
+ *
37
+ * Default appName = "timebasedcipher" for backward compatibility.
38
+ */
39
+ const deriveHmacKeyHex = (encryptionKeyHex, appName = "timebasedcipher") => {
40
+ return crypto_js_1.default.SHA256(`${appName}-hmac:${encryptionKeyHex}`).toString(crypto_js_1.default.enc.Hex);
41
+ };
42
+ /**
43
+ * Generate a time-based rotating key using SHA-256.
40
44
  *
41
- * When `isJwt` is true:
42
- * - `sharedSecretOrJwt` is treated as a JWT string
43
- * - The JWT payload is decoded
44
- * - The `exp` claim (if present) is checked against current time
45
- * - If the JWT is expired, an error is thrown and no key is generated
46
- * - The *raw JWT string* is still used as the secret input for key derivation
45
+ * The key is derived as:
46
+ * SHA256(`${secretInput}:${timeSlot}`)
47
47
  *
48
- * @param sharedSecretOrJwt - A pre-shared secret string, or a JWT when `isJwt` is true.
49
- * @param interval - The rotation interval in seconds. The timeSlot calculation
50
- * is based on this value, so both sides must use the same interval.
51
- * @param isJwt - Optional flag (default: false). When true, `sharedSecretOrJwt` is
52
- * treated as a JWT and its `exp` is validated before deriving the key.
53
- * @returns A 64-character hexadecimal SHA-256 hash string to be used as an AES-256 key.
54
- * @throws If `isJwt` is true and the JWT is malformed or expired.
48
+ * Notes:
49
+ * - If `sharedSecretOrJwt` is a JWT, it is treated as opaque input
50
+ * - JWT validation (signature / exp) is assumed to be handled elsewhere
51
+ * - This function is purely a key-derivation utility
55
52
  */
56
- const generateKey = (sharedSecretOrJwt, interval, isJwt = false) => {
57
- let secretInput = sharedSecretOrJwt;
58
- if (isJwt) {
59
- const payload = decodeJwtPayload(sharedSecretOrJwt);
60
- if (typeof payload.exp !== "number") {
61
- throw new Error("JWT payload does not contain a valid 'exp' claim");
62
- }
63
- const nowSeconds = Math.floor(Date.now() / 1000);
64
- if (payload.exp <= nowSeconds) {
65
- throw new Error("JWT is expired");
66
- }
67
- }
53
+ const generateKey = (sharedSecretOrJwt, interval) => {
68
54
  const timeSlot = Math.floor(Date.now() / (interval * 1000) + 1000);
69
- const input = `${secretInput}:${timeSlot}`;
70
- // SHA-256 → hex string
55
+ const input = `${sharedSecretOrJwt}:${timeSlot}`;
71
56
  return crypto_js_1.default.SHA256(input).toString(crypto_js_1.default.enc.Hex);
72
57
  };
73
58
  exports.generateKey = generateKey;
74
59
  /**
75
- * Encrypt an arbitrary JavaScript value using AES-256-CBC with a random IV.
60
+ * Encrypt arbitrary data using:
61
+ * - AES-256-CBC (confidentiality)
62
+ * - HMAC-SHA256 (integrity & authenticity)
76
63
  *
77
- * The function:
78
- * - JSON stringifies the input `data`
79
- * - Uses the provided hex-encoded key as AES-256 key material
80
- * - Generates a random 16-byte IV
81
- * - Encrypts using AES-256-CBC
82
- * - Returns a concatenated string in the format: "cipherHex:ivHex"
64
+ * Cipher format:
65
+ * cipherHex:ivHex:hmacHex
83
66
  *
84
- * @param data - Any serializable JavaScript value (e.g. object, array, string) to encrypt.
85
- * @param encryptionKeyHex - A 64-character hex string representing a 32-byte key
86
- * (typically the output of {@link generateKey}).
87
- * @returns A string in the format `"cipherHex:ivHex"` where both parts are hex-encoded.
88
- * @throws If encryption fails or JSON serialization fails.
67
+ * `appName`:
68
+ * - Included ONLY in HMAC key derivation
69
+ * - Ensures ciphertexts are bound to an application context
70
+ * - Must match during decryption
89
71
  */
90
- const encrypt = (data, encryptionKeyHex) => {
72
+ const encrypt = (data, encryptionKeyHex, appName = "timebasedcipher") => {
91
73
  const json = JSON.stringify(data);
92
74
  try {
93
- // key and IV as WordArray
94
- const key = crypto_js_1.default.enc.Hex.parse(encryptionKeyHex); // 32 bytes
95
- const iv = crypto_js_1.default.lib.WordArray.random(16); // 16 bytes
75
+ const key = crypto_js_1.default.enc.Hex.parse(encryptionKeyHex);
76
+ const iv = crypto_js_1.default.lib.WordArray.random(16);
96
77
  const encrypted = crypto_js_1.default.AES.encrypt(json, key, { iv });
97
78
  const cipherHex = encrypted.ciphertext.toString(crypto_js_1.default.enc.Hex);
98
79
  const ivHex = iv.toString(crypto_js_1.default.enc.Hex);
99
- return `${cipherHex}:${ivHex}`;
80
+ // Derive MAC key using encryption key + appName context
81
+ const hmacKeyHex = deriveHmacKeyHex(encryptionKeyHex, appName);
82
+ const hmacKey = crypto_js_1.default.enc.Hex.parse(hmacKeyHex);
83
+ const mac = crypto_js_1.default.HmacSHA256(`${cipherHex}:${ivHex}`, hmacKey).toString(crypto_js_1.default.enc.Hex);
84
+ return `${cipherHex}:${ivHex}:${mac}`;
100
85
  }
101
86
  catch (err) {
102
- // eslint-disable-next-line no-console
103
87
  console.error("Error in encrypt:", err);
104
88
  throw err;
105
89
  }
106
90
  };
107
91
  exports.encrypt = encrypt;
108
92
  /**
109
- * Decrypt an AES-256-CBC ciphertext string produced by {@link encrypt}.
93
+ * Decrypt data encrypted by {@link encrypt}.
110
94
  *
111
- * The function expects the input to be in the format:
112
- * "cipherHex:ivHex"
113
- * where:
114
- * - cipherHex is the hex-encoded ciphertext
115
- * - ivHex is the hex-encoded 16-byte IV
95
+ * Steps:
96
+ * 1. Parse cipher format
97
+ * 2. Recompute and verify HMAC (tamper detection)
98
+ * 3. Decrypt AES-256-CBC
99
+ * 4. Parse JSON payload
116
100
  *
117
- * It will:
118
- * - Split and parse the hex components
119
- * - Decrypt using AES-256-CBC with the provided key
120
- * - Parse the resulting UTF-8 JSON back into the original JavaScript value
121
- *
122
- * @param cipherText - The ciphertext string in the format `"cipherHex:ivHex"`.
123
- * @param encryptionKeyHex - A 64-character hex string representing a 32-byte key
124
- * (must match the key used for encryption).
125
- * @returns The decrypted JavaScript value (e.g. object, array, string).
126
- * @throws If the format is invalid, decryption fails, or the decrypted output is empty/invalid JSON.
101
+ * `appName` MUST match the value used during encryption.
127
102
  */
128
- const decrypt = (cipherText, encryptionKeyHex) => {
129
- const [encryptedHex, ivHex] = cipherText.split(":");
130
- if (!ivHex || !encryptedHex) {
131
- throw new Error("Invalid cipher format, expected cipherHex:ivHex");
103
+ const decrypt = (cipherText, encryptionKeyHex, appName = "timebasedcipher") => {
104
+ const parts = cipherText.split(":");
105
+ if (parts.length !== 3) {
106
+ throw new Error("Invalid cipher format, expected cipherHex:ivHex:hmacHex");
107
+ }
108
+ const [encryptedHex, ivHex, hmacHex] = parts;
109
+ // Re-derive MAC key using same appName context
110
+ const hmacKeyHex = deriveHmacKeyHex(encryptionKeyHex, appName);
111
+ const hmacKey = crypto_js_1.default.enc.Hex.parse(hmacKeyHex);
112
+ const recomputedMac = crypto_js_1.default.HmacSHA256(`${encryptedHex}:${ivHex}`, hmacKey).toString(crypto_js_1.default.enc.Hex);
113
+ // Not constant-time, but acceptable for most non-hostile environments
114
+ if (recomputedMac !== hmacHex) {
115
+ throw new Error("Invalid HMAC: ciphertext may be tampered or key/appName is wrong");
132
116
  }
133
117
  const key = crypto_js_1.default.enc.Hex.parse(encryptionKeyHex);
134
118
  const iv = crypto_js_1.default.enc.Hex.parse(ivHex);
135
119
  const ciphertext = crypto_js_1.default.enc.Hex.parse(encryptedHex);
136
- // Proper CipherParams object
137
- const cipherParams = crypto_js_1.default.lib.CipherParams.create({
138
- ciphertext,
139
- });
120
+ const cipherParams = crypto_js_1.default.lib.CipherParams.create({ ciphertext });
140
121
  try {
141
122
  const decrypted = crypto_js_1.default.AES.decrypt(cipherParams, key, { iv });
142
123
  const utf8 = decrypted.toString(crypto_js_1.default.enc.Utf8);
@@ -146,9 +127,8 @@ const decrypt = (cipherText, encryptionKeyHex) => {
146
127
  return JSON.parse(utf8);
147
128
  }
148
129
  catch (err) {
149
- // eslint-disable-next-line no-console
150
- console.error("Error decrypting with current key:", err);
151
- throw new Error("Unable to decrypt: no valid key found or invalid payload");
130
+ console.error("Error decrypting:", err);
131
+ throw new Error("Unable to decrypt: invalid key, appName, or corrupted payload");
152
132
  }
153
133
  };
154
134
  exports.decrypt = decrypt;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "timebasedcipher",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "Time-based key generation and AES encryption/decryption SDK",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",