timebasedcipher 3.0.0 → 3.0.2

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,208 +1,147 @@
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.js 18+ and modern browsers**) library
4
+ for **time-based rotating AES-256-GCM encryption with integrity
5
+ validation**.
4
6
 
5
- Built with [`crypto-js`](https://www.npmjs.com/package/crypto-js), this package provides a simple interface to:
7
+ This implementation uses the **Web Crypto API** and provides:
6
8
 
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
9
+ - Time-based rotating AES-256 keys derived using HKDF-SHA256
10
+ - Authenticated encryption using AES-256-GCM
11
+ - Encrypted signature validation
12
+ - Works in both Node.js (v18+) and modern browser environments
13
+ - No external crypto dependencies
11
14
 
12
15
  ---
13
16
 
14
17
  ## Installation
15
18
 
16
19
  ```bash
17
- npm install timebasedcipher crypto-js
20
+ npm install timebasedcipher
18
21
  # or
19
- yarn add timebasedcipher crypto-js
22
+ yarn add timebasedcipher
20
23
  # or
21
- pnpm add timebasedcipher crypto-js
24
+ pnpm add timebasedcipher
22
25
  ```
23
26
 
27
+ Node.js 18+ is required for native Web Crypto support.
28
+
24
29
  ---
25
30
 
26
31
  ## Quick Start
27
32
 
28
33
  ```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);
34
+ import { encrypt, decrypt } from "timebasedcipher";
42
35
 
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:**
36
+ const secret = "mySuperSecret";
37
+ const intervalSeconds = 60; // key rotates every 60 seconds
68
38
 
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")`
39
+ const data = { user: "TestUser", role: "admin", timestamp: Date.now() };
72
40
 
73
- #### Example (with JWT validation)
41
+ // Encrypt
42
+ const cipher = await encrypt(data, secret, intervalSeconds);
74
43
 
75
- ```ts
76
- const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."; // your JWT
77
- const interval = 120;
44
+ // Decrypt
45
+ const decrypted = await decrypt(cipher, secret, intervalSeconds);
78
46
 
79
- const key = generateKey(jwt, interval, true); // validates exp claim
47
+ console.log(decrypted);
80
48
  ```
81
49
 
82
50
  ---
83
51
 
84
- ### `encrypt(data: any, encryptionKeyHex: string): string`
85
-
86
- Encrypts any serializable JavaScript object or string using **AES-256-CBC**.
52
+ ## API Reference
87
53
 
88
- | Parameter | Type | Description |
89
- | ------------------ | -------- | ------------------------------------------------- |
90
- | `data` | `any` | The object or value to encrypt. |
91
- | `encryptionKeyHex` | `string` | 64-character hex string key (from `generateKey`). |
54
+ ### encrypt`<T>`{=html}(data: T, secret: string, intervalSeconds: number): Promise`<string>`{=html}
92
55
 
93
- **Returns:**
94
- A string in format:
95
-
96
- ```
97
- cipherHex:ivHex
98
- ```
56
+ Encrypts JSON-serializable data using a time-rotating AES-256-GCM key.
99
57
 
100
58
  ---
101
59
 
102
- ### `decrypt(cipherText: string, encryptionKeyHex: string): any`
103
-
104
- Decrypts ciphertext previously produced by `encrypt`.
60
+ | Name | Type | Description |
61
+ | ----------------- | -------- | ------------------------------------- |
62
+ | `data` | `any` | JSON-serializable value |
63
+ | `secret` | `string` | Shared secret used for key derivation |
64
+ | `intervalSeconds` | `number` | Key rotation interval in seconds |
105
65
 
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).
66
+ ---
113
67
 
114
- **Throws:**
68
+ **Returns**
115
69
 
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")`
70
+ `Promise<string>` --- Ciphertext string.
119
71
 
120
72
  ---
121
73
 
122
- ## ⚙️ How It Works
74
+ ### decrypt`<T>`{=html}(payload: string, secret: string, intervalSeconds: number): Promise`<T>`{=html}
123
75
 
124
- - The key **rotates** every defined interval (e.g., every 60 seconds)
76
+ Decrypts and validates ciphertext produced by `encrypt`.
125
77
 
126
- - Derived using:
78
+ ---
127
79
 
128
- ```
129
- SHA256(`${secretInput}:${timeSlot}`)
130
- ```
80
+ | Name | Type | Description |
81
+ | ----------------- | -------- | ----------------------------------------- |
82
+ | `payload` | `string` | Encrypted string (`sigCipher:iv:data:iv`) |
83
+ | `secret` | `string` | Same shared secret used during encryption |
84
+ | `intervalSeconds` | `number` | Same rotation interval used during |
131
85
 
132
- where
133
- `timeSlot = floor(Date.now() / (interval * 1000) + 1000)`
86
+ ---
134
87
 
135
- - AES encryption uses:
88
+ **Returns**
136
89
 
137
- - **AES-256-CBC**
138
- - **PKCS#7 padding**
139
- - **Random 16-byte IV**
90
+ `Promise<T>` --- Decrypted JavaScript value.
140
91
 
141
- - Encrypted output format:
92
+ **Throws**
142
93
 
143
- ```
144
- <cipherHex>:<ivHex>
145
- ```
94
+ - `Error("Invalid cipher format")`
95
+ - `Error("Invalid signature")`
96
+ - Decryption errors if authentication fails
146
97
 
147
- **Both ends** must:
98
+ ---
148
99
 
149
- - Use the **same secret** (or JWT)
150
- - Use the **same rotation interval**
151
- - Have **roughly synchronized clocks** (within ±1 interval)
100
+ ## Key Derivation
152
101
 
153
- ---
102
+ Keys are derived using:
154
103
 
155
- ## Handling Key Rotation Drift
104
+ - HKDF with SHA-256
105
+ - Salt = current time slot
106
+ - Info = "time-based-encryption"
156
107
 
157
- If decryption fails due to slight clock drift, try nearby intervals:
108
+ Time slot calculation:
158
109
 
159
- ```ts
160
- const now = Date.now();
161
- const interval = 60;
162
- const secret = "mySuperSecret";
110
+ timeSlot = floor(Date.now() / (intervalSeconds * 1000))
163
111
 
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
- ```
112
+ A new encryption key is automatically derived for each time window.
178
113
 
179
114
  ---
180
115
 
181
- ## Browser + Node Compatibility
116
+ ## Security Model
182
117
 
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 |
118
+ - Uses AES-256-GCM (authenticated encryption)
119
+ - Provides built-in integrity and tamper detection
120
+ - Includes encrypted signature validation
121
+ - No manual HMAC required (GCM provides authentication)
122
+ - Requires loosely synchronized clocks between encrypting and
123
+ decrypting systems
188
124
 
189
125
  ---
190
126
 
191
- ## Security Notes
127
+ ## Environment Support
192
128
 
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.
129
+ | Name | Supported |
130
+ | ----------------- | --------- |
131
+ | `Node.js 18+` | Yes |
132
+ | `Modern Browsers` | Yes |
133
+ | `React / Next.js` | Yes |
196
134
 
197
135
  ---
198
136
 
199
137
  ## Requirements
200
138
 
201
- - Node.js 14 or any modern browser
202
- - `crypto-js` dependency (installed automatically)
139
+ - Node.js 18+ or modern browser with Web Crypto API
140
+ - No external cryptography libraries required
203
141
 
204
142
  ---
205
143
 
206
144
  ## License
207
145
 
208
- MIT License © 2025 [Deb Kalyan Mohanty](https://github.com/debkalyanmohanty)
146
+ MIT License © 2026\
147
+ [Deb Kalyan Mohanty](https://github.com/debkalyanmohanty)
package/dist/index.d.ts CHANGED
@@ -1,62 +1,30 @@
1
1
  /**
2
- * Generate a time-based rotating key (SHA256 hash of sharedSecret + current interval).
2
+ * Time-Based Cipher with Encrypted Signature
3
3
  *
4
- * The key is derived from:
5
- * SHA256(`${secretInput}:${timeSlot}`)
6
- * where:
7
- * timeSlot = floor(Date.now() / (interval * 1000) + 1000)
4
+ * Format:
5
+ * sigCipherHex:sigIvHex:dataCipherHex:dataIvHex
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
+ * Requirements:
8
+ * - Node.js 18+
9
+ * - Modern browsers
23
10
  */
24
- export declare const generateKey: (sharedSecretOrJwt: string, interval: number, isJwt?: boolean) => string;
25
11
  /**
26
- * Encrypt an arbitrary JavaScript value using AES-256-CBC with a random IV.
12
+ * Encrypt JSON data with encrypted signature.
27
13
  *
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"
14
+ * @param data - JSON-serializable data.
15
+ * @param secret - Shared secret.
16
+ * @param intervalSeconds - Rotation interval in seconds.
34
17
  *
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.
18
+ * @returns Promise resolving to ciphertext string.
40
19
  */
41
- export declare const encrypt: (data: any, encryptionKeyHex: string) => string;
20
+ export declare function encrypt<T>(data: T, secret: string, intervalSeconds: number): Promise<string>;
42
21
  /**
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
22
+ * Decrypt ciphertext with encrypted signature validation.
50
23
  *
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
24
+ * @param payload - Ciphertext string.
25
+ * @param secret - Shared secret.
26
+ * @param intervalSeconds - Rotation interval.
55
27
  *
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.
28
+ * @returns Promise resolving to decrypted data.
61
29
  */
62
- export declare const decrypt: (cipherText: string, encryptionKeyHex: string) => unknown;
30
+ export declare function decrypt<T>(payload: string, secret: string, intervalSeconds: number): Promise<T>;
package/dist/index.js CHANGED
@@ -1,154 +1,122 @@
1
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
2
  /**
9
- * Decode the payload of a JWT without verifying its signature.
10
- * Used only to read the `exp` claim (in seconds since epoch).
3
+ * Time-Based Cipher with Encrypted Signature
4
+ *
5
+ * Format:
6
+ * sigCipherHex:sigIvHex:dataCipherHex:dataIvHex
11
7
  *
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.
8
+ * Requirements:
9
+ * - Node.js 18+
10
+ * - Modern browsers
15
11
  */
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");
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.encrypt = encrypt;
14
+ exports.decrypt = decrypt;
15
+ const cryptoObj = (() => {
16
+ if (typeof globalThis !== "undefined" && globalThis.crypto) {
17
+ return globalThis.crypto;
31
18
  }
32
- };
19
+ throw new Error("Web Crypto API not available. Requires Node 18+ or modern browser.");
20
+ })();
21
+ const encoder = new TextEncoder();
22
+ const decoder = new TextDecoder();
33
23
  /**
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.
24
+ * Convert ArrayBuffer to hex string.
25
+ */
26
+ function toHex(buffer) {
27
+ return Array.from(new Uint8Array(buffer))
28
+ .map((b) => b.toString(16).padStart(2, "0"))
29
+ .join("");
30
+ }
31
+ /**
32
+ * Convert hex string to ArrayBuffer.
55
33
  */
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
- }
34
+ function hexToBuffer(hex) {
35
+ if (!/^[0-9a-fA-F]+$/.test(hex) || hex.length % 2 !== 0) {
36
+ throw new Error("Invalid hex string");
67
37
  }
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;
38
+ const bytes = new Uint8Array(hex.match(/.{1,2}/g).map((b) => parseInt(b, 16)));
39
+ return bytes.buffer;
40
+ }
74
41
  /**
75
- * Encrypt an arbitrary JavaScript value using AES-256-CBC with a random IV.
42
+ * Derive AES-256-GCM key using HKDF-SHA256.
43
+ */
44
+ async function deriveKey(secret, intervalSeconds) {
45
+ if (!secret)
46
+ throw new Error("Secret must not be empty");
47
+ if (intervalSeconds <= 0)
48
+ throw new Error("intervalSeconds must be positive");
49
+ const now = Date.now() - 1000;
50
+ const timeSlot = Math.floor(now / (intervalSeconds * 1000));
51
+ const baseKey = await cryptoObj.subtle.importKey("raw", encoder.encode(secret).buffer, { name: "HKDF" }, false, ["deriveKey"]);
52
+ return cryptoObj.subtle.deriveKey({
53
+ name: "HKDF",
54
+ hash: "SHA-256",
55
+ salt: encoder.encode(String(timeSlot)).buffer,
56
+ info: encoder.encode("time-based-encryption").buffer,
57
+ }, baseKey, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
58
+ }
59
+ /**
60
+ * Encrypt raw ArrayBuffer with AES-GCM.
61
+ */
62
+ async function aesEncrypt(key, data) {
63
+ const ivBytes = cryptoObj.getRandomValues(new Uint8Array(12));
64
+ const iv = ivBytes.buffer;
65
+ const cipher = await cryptoObj.subtle.encrypt({ name: "AES-GCM", iv }, key, data);
66
+ return { cipher, iv };
67
+ }
68
+ /**
69
+ * Decrypt raw ArrayBuffer with AES-GCM.
70
+ */
71
+ async function aesDecrypt(key, cipher, iv) {
72
+ return cryptoObj.subtle.decrypt({ name: "AES-GCM", iv }, key, cipher);
73
+ }
74
+ /**
75
+ * Encrypt JSON data with encrypted signature.
76
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"
77
+ * @param data - JSON-serializable data.
78
+ * @param secret - Shared secret.
79
+ * @param intervalSeconds - Rotation interval in seconds.
83
80
  *
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.
81
+ * @returns Promise resolving to ciphertext string.
89
82
  */
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;
83
+ async function encrypt(data, secret, intervalSeconds) {
84
+ const key = await deriveKey(secret, intervalSeconds);
85
+ // Signature payload (can be customized)
86
+ const signaturePayload = encoder.encode("signature-v1").buffer;
87
+ const sigEncrypted = await aesEncrypt(key, signaturePayload);
88
+ const dataBuffer = encoder.encode(JSON.stringify(data)).buffer;
89
+ const dataEncrypted = await aesEncrypt(key, dataBuffer);
90
+ return [
91
+ toHex(sigEncrypted.cipher),
92
+ toHex(sigEncrypted.iv),
93
+ toHex(dataEncrypted.cipher),
94
+ toHex(dataEncrypted.iv),
95
+ ].join(":");
96
+ }
108
97
  /**
109
- * Decrypt an AES-256-CBC ciphertext string produced by {@link encrypt}.
98
+ * Decrypt ciphertext with encrypted signature validation.
110
99
  *
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
100
+ * @param payload - Ciphertext string.
101
+ * @param secret - Shared secret.
102
+ * @param intervalSeconds - Rotation interval.
116
103
  *
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.
104
+ * @returns Promise resolving to decrypted data.
127
105
  */
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);
106
+ async function decrypt(payload, secret, intervalSeconds) {
107
+ const parts = payload.split(":");
108
+ if (parts.length !== 4) {
109
+ throw new Error("Invalid cipher format");
147
110
  }
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");
111
+ const [sigCipherHex, sigIvHex, dataCipherHex, dataIvHex,] = parts;
112
+ const key = await deriveKey(secret, intervalSeconds);
113
+ // Decrypt signature
114
+ const sigPlain = await aesDecrypt(key, hexToBuffer(sigCipherHex), hexToBuffer(sigIvHex));
115
+ const sigText = decoder.decode(sigPlain);
116
+ if (sigText !== "signature-v1") {
117
+ throw new Error("Invalid signature");
152
118
  }
153
- };
154
- exports.decrypt = decrypt;
119
+ // Decrypt actual data
120
+ const dataPlain = await aesDecrypt(key, hexToBuffer(dataCipherHex), hexToBuffer(dataIvHex));
121
+ return JSON.parse(decoder.decode(dataPlain));
122
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "timebasedcipher",
3
- "version": "3.0.0",
3
+ "version": "3.0.2",
4
4
  "description": "Time-based key generation and AES encryption/decryption SDK",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",