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 +75 -136
- package/dist/index.d.ts +18 -50
- package/dist/index.js +104 -136
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,208 +1,147 @@
|
|
|
1
1
|
# timebasedcipher
|
|
2
2
|
|
|
3
|
-
A lightweight, isomorphic (**Node +
|
|
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
|
-
|
|
7
|
+
This implementation uses the **Web Crypto API** and provides:
|
|
6
8
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
- Works
|
|
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
|
|
20
|
+
npm install timebasedcipher
|
|
18
21
|
# or
|
|
19
|
-
yarn add timebasedcipher
|
|
22
|
+
yarn add timebasedcipher
|
|
20
23
|
# or
|
|
21
|
-
pnpm add timebasedcipher
|
|
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 {
|
|
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
|
-
|
|
44
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
41
|
+
// Encrypt
|
|
42
|
+
const cipher = await encrypt(data, secret, intervalSeconds);
|
|
74
43
|
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
const interval = 120;
|
|
44
|
+
// Decrypt
|
|
45
|
+
const decrypted = await decrypt(cipher, secret, intervalSeconds);
|
|
78
46
|
|
|
79
|
-
|
|
47
|
+
console.log(decrypted);
|
|
80
48
|
```
|
|
81
49
|
|
|
82
50
|
---
|
|
83
51
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
Encrypts any serializable JavaScript object or string using **AES-256-CBC**.
|
|
52
|
+
## API Reference
|
|
87
53
|
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
68
|
+
**Returns**
|
|
115
69
|
|
|
116
|
-
|
|
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
|
-
|
|
74
|
+
### decrypt`<T>`{=html}(payload: string, secret: string, intervalSeconds: number): Promise`<T>`{=html}
|
|
123
75
|
|
|
124
|
-
|
|
76
|
+
Decrypts and validates ciphertext produced by `encrypt`.
|
|
125
77
|
|
|
126
|
-
|
|
78
|
+
---
|
|
127
79
|
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
133
|
-
`timeSlot = floor(Date.now() / (interval * 1000) + 1000)`
|
|
86
|
+
---
|
|
134
87
|
|
|
135
|
-
|
|
88
|
+
**Returns**
|
|
136
89
|
|
|
137
|
-
|
|
138
|
-
- **PKCS#7 padding**
|
|
139
|
-
- **Random 16-byte IV**
|
|
90
|
+
`Promise<T>` --- Decrypted JavaScript value.
|
|
140
91
|
|
|
141
|
-
|
|
92
|
+
**Throws**
|
|
142
93
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
94
|
+
- `Error("Invalid cipher format")`
|
|
95
|
+
- `Error("Invalid signature")`
|
|
96
|
+
- Decryption errors if authentication fails
|
|
146
97
|
|
|
147
|
-
|
|
98
|
+
---
|
|
148
99
|
|
|
149
|
-
|
|
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
|
-
|
|
104
|
+
- HKDF with SHA-256
|
|
105
|
+
- Salt = current time slot
|
|
106
|
+
- Info = "time-based-encryption"
|
|
156
107
|
|
|
157
|
-
|
|
108
|
+
Time slot calculation:
|
|
158
109
|
|
|
159
|
-
|
|
160
|
-
const now = Date.now();
|
|
161
|
-
const interval = 60;
|
|
162
|
-
const secret = "mySuperSecret";
|
|
110
|
+
timeSlot = floor(Date.now() / (intervalSeconds * 1000))
|
|
163
111
|
|
|
164
|
-
|
|
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
|
-
##
|
|
116
|
+
## Security Model
|
|
182
117
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
##
|
|
127
|
+
## Environment Support
|
|
192
128
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
202
|
-
-
|
|
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 ©
|
|
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
|
-
*
|
|
2
|
+
* Time-Based Cipher with Encrypted Signature
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* where:
|
|
7
|
-
* timeSlot = floor(Date.now() / (interval * 1000) + 1000)
|
|
4
|
+
* Format:
|
|
5
|
+
* sigCipherHex:sigIvHex:dataCipherHex:dataIvHex
|
|
8
6
|
*
|
|
9
|
-
*
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
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
|
|
12
|
+
* Encrypt JSON data with encrypted signature.
|
|
27
13
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
-
* @
|
|
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
|
|
20
|
+
export declare function encrypt<T>(data: T, secret: string, intervalSeconds: number): Promise<string>;
|
|
42
21
|
/**
|
|
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
|
|
22
|
+
* Decrypt ciphertext with encrypted signature validation.
|
|
50
23
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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
|
-
* @
|
|
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
|
|
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
|
-
*
|
|
10
|
-
*
|
|
3
|
+
* Time-Based Cipher with Encrypted Signature
|
|
4
|
+
*
|
|
5
|
+
* Format:
|
|
6
|
+
* sigCipherHex:sigIvHex:dataCipherHex:dataIvHex
|
|
11
7
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
8
|
+
* Requirements:
|
|
9
|
+
* - Node.js 18+
|
|
10
|
+
* - Modern browsers
|
|
15
11
|
*/
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
*
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
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
|
-
* @
|
|
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
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
98
|
+
* Decrypt ciphertext with encrypted signature validation.
|
|
110
99
|
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
131
|
-
throw new Error("Invalid cipher format
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
119
|
+
// Decrypt actual data
|
|
120
|
+
const dataPlain = await aesDecrypt(key, hexToBuffer(dataCipherHex), hexToBuffer(dataIvHex));
|
|
121
|
+
return JSON.parse(decoder.decode(dataPlain));
|
|
122
|
+
}
|