timebasedcipher 3.0.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Deb Kalyan Mohanty
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,208 @@
1
+ # timebasedcipher
2
+
3
+ A lightweight, isomorphic (**Node + Browser**) library for **time-based rotating AES encryption** with optional **JWT-based key validation**.
4
+
5
+ Built with [`crypto-js`](https://www.npmjs.com/package/crypto-js), this package provides a simple interface to:
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
11
+
12
+ ---
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install timebasedcipher crypto-js
18
+ # or
19
+ yarn add timebasedcipher crypto-js
20
+ # or
21
+ pnpm add timebasedcipher crypto-js
22
+ ```
23
+
24
+ ---
25
+
26
+ ## Quick Start
27
+
28
+ ```ts
29
+ import { generateKey, encrypt, decrypt } from "timebasedcipher";
30
+
31
+ const sharedSecret = "mySuperSecret";
32
+ const intervalSeconds = 60; // key changes every 60 seconds
33
+
34
+ // Step 1: Generate a rotating key
35
+ const key = generateKey(sharedSecret, intervalSeconds);
36
+ console.log("Generated Key:", key);
37
+
38
+ // Step 2: Encrypt some data
39
+ const data = { user: "Deb", role: "admin", timestamp: Date.now() };
40
+ const cipher = encrypt(data, key);
41
+ console.log("Encrypted:", cipher);
42
+
43
+ // Step 3: Decrypt it
44
+ const decrypted = decrypt(cipher, key);
45
+ console.log("Decrypted:", decrypted);
46
+ ```
47
+
48
+ Works in both **Node.js** and **browser** environments.
49
+
50
+ ---
51
+
52
+ ## Function Reference
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).
66
+
67
+ **Throws:**
68
+
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")`
72
+
73
+ #### Example (with JWT validation)
74
+
75
+ ```ts
76
+ const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."; // your JWT
77
+ const interval = 120;
78
+
79
+ const key = generateKey(jwt, interval, true); // validates exp claim
80
+ ```
81
+
82
+ ---
83
+
84
+ ### `encrypt(data: any, encryptionKeyHex: string): string`
85
+
86
+ Encrypts any serializable JavaScript object or string using **AES-256-CBC**.
87
+
88
+ | Parameter | Type | Description |
89
+ | ------------------ | -------- | ------------------------------------------------- |
90
+ | `data` | `any` | The object or value to encrypt. |
91
+ | `encryptionKeyHex` | `string` | 64-character hex string key (from `generateKey`). |
92
+
93
+ **Returns:**
94
+ A string in format:
95
+
96
+ ```
97
+ cipherHex:ivHex
98
+ ```
99
+
100
+ ---
101
+
102
+ ### `decrypt(cipherText: string, encryptionKeyHex: string): any`
103
+
104
+ Decrypts ciphertext previously produced by `encrypt`.
105
+
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). |
110
+
111
+ **Returns:**
112
+ The decrypted JavaScript value (automatically parsed from JSON).
113
+
114
+ **Throws:**
115
+
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")`
119
+
120
+ ---
121
+
122
+ ## ⚙️ How It Works
123
+
124
+ - The key **rotates** every defined interval (e.g., every 60 seconds)
125
+
126
+ - Derived using:
127
+
128
+ ```
129
+ SHA256(`${secretInput}:${timeSlot}`)
130
+ ```
131
+
132
+ where
133
+ `timeSlot = floor(Date.now() / (interval * 1000) + 1000)`
134
+
135
+ - AES encryption uses:
136
+
137
+ - **AES-256-CBC**
138
+ - **PKCS#7 padding**
139
+ - **Random 16-byte IV**
140
+
141
+ - Encrypted output format:
142
+
143
+ ```
144
+ <cipherHex>:<ivHex>
145
+ ```
146
+
147
+ **Both ends** must:
148
+
149
+ - Use the **same secret** (or JWT)
150
+ - Use the **same rotation interval**
151
+ - Have **roughly synchronized clocks** (within ±1 interval)
152
+
153
+ ---
154
+
155
+ ## Handling Key Rotation Drift
156
+
157
+ If decryption fails due to slight clock drift, try nearby intervals:
158
+
159
+ ```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
+ }
177
+ ```
178
+
179
+ ---
180
+
181
+ ## Browser + Node Compatibility
182
+
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 |
188
+
189
+ ---
190
+
191
+ ## Security Notes
192
+
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.
196
+
197
+ ---
198
+
199
+ ## Requirements
200
+
201
+ - Node.js ≥ 14 or any modern browser
202
+ - `crypto-js` dependency (installed automatically)
203
+
204
+ ---
205
+
206
+ ## License
207
+
208
+ MIT License © 2025 [Deb Kalyan Mohanty](https://github.com/debkalyanmohanty)
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Generate a time-based rotating key (SHA256 hash of sharedSecret + current interval).
3
+ *
4
+ * The key is derived from:
5
+ * SHA256(`${secretInput}:${timeSlot}`)
6
+ * where:
7
+ * timeSlot = floor(Date.now() / (interval * 1000) + 1000)
8
+ *
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.
23
+ */
24
+ export declare const generateKey: (sharedSecretOrJwt: string, interval: number, isJwt?: boolean) => string;
25
+ /**
26
+ * Encrypt an arbitrary JavaScript value using AES-256-CBC with a random IV.
27
+ *
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"
34
+ *
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.
40
+ */
41
+ export declare const encrypt: (data: any, encryptionKeyHex: string) => string;
42
+ /**
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
50
+ *
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
55
+ *
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.
61
+ */
62
+ export declare const decrypt: (cipherText: string, encryptionKeyHex: string) => unknown;
package/dist/index.js ADDED
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.decrypt = exports.encrypt = exports.generateKey = void 0;
7
+ const crypto_js_1 = __importDefault(require("crypto-js"));
8
+ /**
9
+ * Decode the payload of a JWT without verifying its signature.
10
+ * Used only to read the `exp` claim (in seconds since epoch).
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.
15
+ */
16
+ const decodeJwtPayload = (token) => {
17
+ const parts = token.split(".");
18
+ if (parts.length < 2) {
19
+ throw new Error("Invalid JWT: missing payload part");
20
+ }
21
+ const base64Url = parts[1];
22
+ const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
23
+ // Use CryptoJS to decode base64 → UTF-8
24
+ const wordArray = crypto_js_1.default.enc.Base64.parse(base64);
25
+ const json = wordArray.toString(crypto_js_1.default.enc.Utf8);
26
+ try {
27
+ return JSON.parse(json);
28
+ }
29
+ catch {
30
+ throw new Error("Invalid JWT payload JSON");
31
+ }
32
+ };
33
+ /**
34
+ * Generate a time-based rotating key (SHA256 hash of sharedSecret + current interval).
35
+ *
36
+ * The key is derived from:
37
+ * SHA256(`${secretInput}:${timeSlot}`)
38
+ * where:
39
+ * timeSlot = floor(Date.now() / (interval * 1000) + 1000)
40
+ *
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
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.
55
+ */
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
+ }
68
+ const timeSlot = Math.floor(Date.now() / (interval * 1000) + 1000);
69
+ const input = `${secretInput}:${timeSlot}`;
70
+ // SHA-256 → hex string
71
+ return crypto_js_1.default.SHA256(input).toString(crypto_js_1.default.enc.Hex);
72
+ };
73
+ exports.generateKey = generateKey;
74
+ /**
75
+ * Encrypt an arbitrary JavaScript value using AES-256-CBC with a random IV.
76
+ *
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"
83
+ *
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.
89
+ */
90
+ const encrypt = (data, encryptionKeyHex) => {
91
+ const json = JSON.stringify(data);
92
+ 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
96
+ const encrypted = crypto_js_1.default.AES.encrypt(json, key, { iv });
97
+ const cipherHex = encrypted.ciphertext.toString(crypto_js_1.default.enc.Hex);
98
+ const ivHex = iv.toString(crypto_js_1.default.enc.Hex);
99
+ return `${cipherHex}:${ivHex}`;
100
+ }
101
+ catch (err) {
102
+ // eslint-disable-next-line no-console
103
+ console.error("Error in encrypt:", err);
104
+ throw err;
105
+ }
106
+ };
107
+ exports.encrypt = encrypt;
108
+ /**
109
+ * Decrypt an AES-256-CBC ciphertext string produced by {@link encrypt}.
110
+ *
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
116
+ *
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.
127
+ */
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");
132
+ }
133
+ const key = crypto_js_1.default.enc.Hex.parse(encryptionKeyHex);
134
+ const iv = crypto_js_1.default.enc.Hex.parse(ivHex);
135
+ 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
+ });
140
+ try {
141
+ const decrypted = crypto_js_1.default.AES.decrypt(cipherParams, key, { iv });
142
+ const utf8 = decrypted.toString(crypto_js_1.default.enc.Utf8);
143
+ if (!utf8) {
144
+ throw new Error("Decryption produced empty string");
145
+ }
146
+ return JSON.parse(utf8);
147
+ }
148
+ 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");
152
+ }
153
+ };
154
+ exports.decrypt = decrypt;
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "timebasedcipher",
3
+ "version": "3.0.0",
4
+ "description": "Time-based key generation and AES encryption/decryption SDK",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "README.md"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "prepublishOnly": "npm run build"
15
+ },
16
+ "keywords": [
17
+ "crypto",
18
+ "aes",
19
+ "encryption",
20
+ "sdk",
21
+ "time-based",
22
+ "cipher",
23
+ "security",
24
+ "expiry",
25
+ "jwt",
26
+ "jwt based cipher"
27
+ ],
28
+ "author": "Vipul Naik & Deb Kalyan Mohanty",
29
+ "license": "MIT",
30
+ "devDependencies": {
31
+ "@types/crypto-js": "^4.2.2",
32
+ "@types/node": "^24.10.1",
33
+ "typescript": "^5.0.0"
34
+ },
35
+ "dependencies": {
36
+ "crypto-js": "^4.2.0"
37
+ }
38
+ }