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 +90 -111
- package/dist/index.d.ts +25 -49
- package/dist/index.js +62 -82
- package/package.json +1 -1
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
|
|
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
|
|
5
|
+
Built with [`crypto-js`](https://www.npmjs.com/package/crypto-js), this package provides:
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
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
|
|
33
|
+
const intervalSeconds = 60; // key rotates every 60 seconds
|
|
33
34
|
|
|
34
|
-
//
|
|
35
|
+
// Generate a rotating key
|
|
35
36
|
const key = generateKey(sharedSecret, intervalSeconds);
|
|
36
|
-
console.log("Generated Key:", key);
|
|
37
37
|
|
|
38
|
-
//
|
|
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
|
-
//
|
|
42
|
+
// Decrypt data
|
|
44
43
|
const decrypted = decrypt(cipher, key);
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
+
### `generateKey(sharedSecretOrJwt: string, interval: number): string`
|
|
68
57
|
|
|
69
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."; // your JWT
|
|
77
|
-
const interval = 120;
|
|
64
|
+
#### Parameters
|
|
78
65
|
|
|
79
|
-
|
|
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
|
-
|
|
73
|
+
- `string` — 64-character hex string (32-byte AES-256 key)
|
|
85
74
|
|
|
86
|
-
|
|
75
|
+
#### Key Derivation
|
|
87
76
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
| `encryptionKeyHex` | `string` | 64-character hex string key (from `generateKey`). |
|
|
77
|
+
```text
|
|
78
|
+
SHA256(`${secretInput}:${timeSlot}`)
|
|
79
|
+
```
|
|
92
80
|
|
|
93
|
-
|
|
94
|
-
A string in format:
|
|
81
|
+
Where:
|
|
95
82
|
|
|
96
|
-
```
|
|
97
|
-
|
|
83
|
+
```text
|
|
84
|
+
timeSlot = floor(Date.now() / (interval * 1000) + 1000)
|
|
98
85
|
```
|
|
99
86
|
|
|
100
87
|
---
|
|
101
88
|
|
|
102
|
-
### `
|
|
89
|
+
### `encrypt(data: any, encryptionKeyHex: string, appName?: string): string`
|
|
103
90
|
|
|
104
|
-
|
|
91
|
+
Encrypts data using **AES-256-CBC** and appends an **HMAC-SHA256** for integrity.
|
|
105
92
|
|
|
106
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
101
|
+
#### Returns
|
|
115
102
|
|
|
116
|
-
|
|
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
|
-
|
|
109
|
+
---
|
|
123
110
|
|
|
124
|
-
|
|
111
|
+
### `decrypt(cipherText: string, encryptionKeyHex: string, appName?: string): any`
|
|
125
112
|
|
|
126
|
-
|
|
113
|
+
Decrypts and verifies ciphertext produced by `encrypt`.
|
|
127
114
|
|
|
128
|
-
|
|
129
|
-
SHA256(`${secretInput}:${timeSlot}`)
|
|
130
|
-
```
|
|
115
|
+
#### Parameters
|
|
131
116
|
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
123
|
+
#### Returns
|
|
136
124
|
|
|
137
|
-
|
|
138
|
-
- **PKCS#7 padding**
|
|
139
|
-
- **Random 16-byte IV**
|
|
125
|
+
- Original decrypted JavaScript value (parsed from JSON)
|
|
140
126
|
|
|
141
|
-
|
|
127
|
+
#### Throws
|
|
142
128
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
129
|
+
- `Error("Invalid cipher format")`
|
|
130
|
+
- `Error("Invalid HMAC: ciphertext may be tampered")`
|
|
131
|
+
- `Error("Unable to decrypt")`
|
|
146
132
|
|
|
147
|
-
|
|
133
|
+
---
|
|
148
134
|
|
|
149
|
-
|
|
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
|
-
|
|
139
|
+
- Prevents ciphertexts from being reused across apps
|
|
140
|
+
- Prevents MAC key reuse
|
|
156
141
|
|
|
157
|
-
|
|
142
|
+
### Example
|
|
158
143
|
|
|
159
144
|
```ts
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
const
|
|
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
|
|
186
|
-
|
|
|
187
|
-
| Next.js
|
|
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
|
-
-
|
|
194
|
-
-
|
|
195
|
-
-
|
|
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
|
|
202
|
-
- `crypto-js` dependency
|
|
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 ©
|
|
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
|
|
2
|
+
* Generate a time-based rotating key using SHA-256.
|
|
3
3
|
*
|
|
4
|
-
* The key is derived
|
|
4
|
+
* The key is derived as:
|
|
5
5
|
* SHA256(`${secretInput}:${timeSlot}`)
|
|
6
|
-
* where:
|
|
7
|
-
* timeSlot = floor(Date.now() / (interval * 1000) + 1000)
|
|
8
6
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
|
12
|
+
export declare const generateKey: (sharedSecretOrJwt: string, interval: number) => string;
|
|
25
13
|
/**
|
|
26
|
-
* Encrypt
|
|
14
|
+
* Encrypt arbitrary data using:
|
|
15
|
+
* - AES-256-CBC (confidentiality)
|
|
16
|
+
* - HMAC-SHA256 (integrity & authenticity)
|
|
27
17
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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
|
|
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
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
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
|
-
*
|
|
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 (
|
|
10
|
+
* Used only to read the `exp` claim (seconds since epoch).
|
|
11
11
|
*
|
|
12
|
-
*
|
|
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
|
-
*
|
|
31
|
+
* Derive a dedicated HMAC key from the encryption key.
|
|
35
32
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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
|
-
*
|
|
42
|
-
*
|
|
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
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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
|
|
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 = `${
|
|
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
|
|
60
|
+
* Encrypt arbitrary data using:
|
|
61
|
+
* - AES-256-CBC (confidentiality)
|
|
62
|
+
* - HMAC-SHA256 (integrity & authenticity)
|
|
76
63
|
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
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
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
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
|
-
|
|
94
|
-
const
|
|
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
|
-
|
|
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
|
|
93
|
+
* Decrypt data encrypted by {@link encrypt}.
|
|
110
94
|
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
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
|
-
*
|
|
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
|
|
130
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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;
|