timebasedcipher 3.0.1 → 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 +71 -111
- package/dist/index.d.ts +18 -26
- package/dist/index.js +102 -114
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,187 +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
|
-
-
|
|
11
|
-
-
|
|
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
|
|
12
14
|
|
|
13
15
|
---
|
|
14
16
|
|
|
15
17
|
## Installation
|
|
16
18
|
|
|
17
19
|
```bash
|
|
18
|
-
npm install timebasedcipher
|
|
20
|
+
npm install timebasedcipher
|
|
19
21
|
# or
|
|
20
|
-
yarn add timebasedcipher
|
|
22
|
+
yarn add timebasedcipher
|
|
21
23
|
# or
|
|
22
|
-
pnpm add timebasedcipher
|
|
24
|
+
pnpm add timebasedcipher
|
|
23
25
|
```
|
|
24
26
|
|
|
27
|
+
Node.js 18+ is required for native Web Crypto support.
|
|
28
|
+
|
|
25
29
|
---
|
|
26
30
|
|
|
27
31
|
## Quick Start
|
|
28
32
|
|
|
29
33
|
```ts
|
|
30
|
-
import {
|
|
34
|
+
import { encrypt, decrypt } from "timebasedcipher";
|
|
31
35
|
|
|
32
|
-
const
|
|
36
|
+
const secret = "mySuperSecret";
|
|
33
37
|
const intervalSeconds = 60; // key rotates every 60 seconds
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
const key = generateKey(sharedSecret, intervalSeconds);
|
|
39
|
+
const data = { user: "TestUser", role: "admin", timestamp: Date.now() };
|
|
37
40
|
|
|
38
|
-
// Encrypt
|
|
39
|
-
const
|
|
40
|
-
const cipher = encrypt(data, key);
|
|
41
|
+
// Encrypt
|
|
42
|
+
const cipher = await encrypt(data, secret, intervalSeconds);
|
|
41
43
|
|
|
42
|
-
// Decrypt
|
|
43
|
-
const decrypted = decrypt(cipher,
|
|
44
|
+
// Decrypt
|
|
45
|
+
const decrypted = await decrypt(cipher, secret, intervalSeconds);
|
|
44
46
|
|
|
45
47
|
console.log(decrypted);
|
|
46
48
|
```
|
|
47
49
|
|
|
48
|
-
Works seamlessly in both **Node.js** and **browser** environments.
|
|
49
|
-
|
|
50
50
|
---
|
|
51
51
|
|
|
52
|
-
##
|
|
53
|
-
|
|
54
|
-
---
|
|
55
|
-
|
|
56
|
-
### `generateKey(sharedSecretOrJwt: string, interval: number): string`
|
|
57
|
-
|
|
58
|
-
Generates a **SHA-256–derived rotating key** that changes every `interval` seconds.
|
|
59
|
-
|
|
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
|
|
63
|
-
|
|
64
|
-
#### Parameters
|
|
65
|
-
|
|
66
|
-
| Name | Type | Description |
|
|
67
|
-
| ------------------- | -------- | ----------------------------- |
|
|
68
|
-
| `sharedSecretOrJwt` | `string` | A shared secret or JWT string |
|
|
69
|
-
| `interval` | `number` | Rotation interval in seconds |
|
|
70
|
-
|
|
71
|
-
#### Returns
|
|
72
|
-
|
|
73
|
-
- `string` — 64-character hex string (32-byte AES-256 key)
|
|
74
|
-
|
|
75
|
-
#### Key Derivation
|
|
76
|
-
|
|
77
|
-
```text
|
|
78
|
-
SHA256(`${secretInput}:${timeSlot}`)
|
|
79
|
-
```
|
|
52
|
+
## API Reference
|
|
80
53
|
|
|
81
|
-
|
|
54
|
+
### encrypt`<T>`{=html}(data: T, secret: string, intervalSeconds: number): Promise`<string>`{=html}
|
|
82
55
|
|
|
83
|
-
|
|
84
|
-
timeSlot = floor(Date.now() / (interval * 1000) + 1000)
|
|
85
|
-
```
|
|
56
|
+
Encrypts JSON-serializable data using a time-rotating AES-256-GCM key.
|
|
86
57
|
|
|
87
58
|
---
|
|
88
59
|
|
|
89
|
-
|
|
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 |
|
|
90
65
|
|
|
91
|
-
|
|
66
|
+
---
|
|
92
67
|
|
|
93
|
-
|
|
68
|
+
**Returns**
|
|
94
69
|
|
|
95
|
-
|
|
96
|
-
| ---------------------- | -------- | ---------------------------------- |
|
|
97
|
-
| `data` | `any` | Any JSON-serializable value |
|
|
98
|
-
| `encryptionKeyHex` | `string` | 64-char hex key from `generateKey` |
|
|
99
|
-
| `appName` _(optional)_ | `string` | Application context |
|
|
70
|
+
`Promise<string>` --- Ciphertext string.
|
|
100
71
|
|
|
101
|
-
|
|
72
|
+
---
|
|
102
73
|
|
|
103
|
-
|
|
74
|
+
### decrypt`<T>`{=html}(payload: string, secret: string, intervalSeconds: number): Promise`<T>`{=html}
|
|
104
75
|
|
|
105
|
-
|
|
106
|
-
cipherHex:ivHex:hmacHex
|
|
107
|
-
```
|
|
76
|
+
Decrypts and validates ciphertext produced by `encrypt`.
|
|
108
77
|
|
|
109
78
|
---
|
|
110
79
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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 |
|
|
116
85
|
|
|
117
|
-
|
|
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 |
|
|
86
|
+
---
|
|
122
87
|
|
|
123
|
-
|
|
88
|
+
**Returns**
|
|
124
89
|
|
|
125
|
-
|
|
90
|
+
`Promise<T>` --- Decrypted JavaScript value.
|
|
126
91
|
|
|
127
|
-
|
|
92
|
+
**Throws**
|
|
128
93
|
|
|
129
94
|
- `Error("Invalid cipher format")`
|
|
130
|
-
- `Error("Invalid
|
|
131
|
-
-
|
|
95
|
+
- `Error("Invalid signature")`
|
|
96
|
+
- Decryption errors if authentication fails
|
|
132
97
|
|
|
133
98
|
---
|
|
134
99
|
|
|
135
|
-
##
|
|
136
|
-
|
|
137
|
-
`appName` provides **cryptographic domain separation**.
|
|
100
|
+
## Key Derivation
|
|
138
101
|
|
|
139
|
-
|
|
140
|
-
- Prevents MAC key reuse
|
|
102
|
+
Keys are derived using:
|
|
141
103
|
|
|
142
|
-
|
|
104
|
+
- HKDF with SHA-256
|
|
105
|
+
- Salt = current time slot
|
|
106
|
+
- Info = "time-based-encryption"
|
|
143
107
|
|
|
144
|
-
|
|
145
|
-
const key = generateKey(secret, 60);
|
|
108
|
+
Time slot calculation:
|
|
146
109
|
|
|
147
|
-
|
|
148
|
-
const plain = decrypt(cipher, key, "billing-service");
|
|
149
|
-
```
|
|
110
|
+
timeSlot = floor(Date.now() / (intervalSeconds * 1000))
|
|
150
111
|
|
|
151
|
-
|
|
112
|
+
A new encryption key is automatically derived for each time window.
|
|
152
113
|
|
|
153
114
|
---
|
|
154
115
|
|
|
155
|
-
##
|
|
116
|
+
## Security Model
|
|
156
117
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
162
124
|
|
|
163
125
|
---
|
|
164
126
|
|
|
165
|
-
##
|
|
127
|
+
## Environment Support
|
|
166
128
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
129
|
+
| Name | Supported |
|
|
130
|
+
| ----------------- | --------- |
|
|
131
|
+
| `Node.js 18+` | Yes |
|
|
132
|
+
| `Modern Browsers` | Yes |
|
|
133
|
+
| `React / Next.js` | Yes |
|
|
172
134
|
|
|
173
135
|
---
|
|
174
136
|
|
|
175
137
|
## Requirements
|
|
176
138
|
|
|
177
|
-
- Node.js
|
|
178
|
-
-
|
|
139
|
+
- Node.js 18+ or modern browser with Web Crypto API
|
|
140
|
+
- No external cryptography libraries required
|
|
179
141
|
|
|
180
142
|
---
|
|
181
143
|
|
|
182
144
|
## License
|
|
183
145
|
|
|
184
|
-
MIT License © 2026
|
|
185
|
-
[
|
|
186
|
-
|
|
187
|
-
---
|
|
146
|
+
MIT License © 2026\
|
|
147
|
+
[Deb Kalyan Mohanty](https://github.com/debkalyanmohanty)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,38 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Time-Based Cipher with Encrypted Signature
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Format:
|
|
5
|
+
* sigCipherHex:sigIvHex:dataCipherHex:dataIvHex
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* - This function is purely a key-derivation utility
|
|
7
|
+
* Requirements:
|
|
8
|
+
* - Node.js 18+
|
|
9
|
+
* - Modern browsers
|
|
11
10
|
*/
|
|
12
|
-
export declare const generateKey: (sharedSecretOrJwt: string, interval: number) => string;
|
|
13
11
|
/**
|
|
14
|
-
* Encrypt
|
|
15
|
-
* - AES-256-CBC (confidentiality)
|
|
16
|
-
* - HMAC-SHA256 (integrity & authenticity)
|
|
12
|
+
* Encrypt JSON data with encrypted signature.
|
|
17
13
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
14
|
+
* @param data - JSON-serializable data.
|
|
15
|
+
* @param secret - Shared secret.
|
|
16
|
+
* @param intervalSeconds - Rotation interval in seconds.
|
|
20
17
|
*
|
|
21
|
-
*
|
|
22
|
-
* - Included ONLY in HMAC key derivation
|
|
23
|
-
* - Ensures ciphertexts are bound to an application context
|
|
24
|
-
* - Must match during decryption
|
|
18
|
+
* @returns Promise resolving to ciphertext string.
|
|
25
19
|
*/
|
|
26
|
-
export declare
|
|
20
|
+
export declare function encrypt<T>(data: T, secret: string, intervalSeconds: number): Promise<string>;
|
|
27
21
|
/**
|
|
28
|
-
* Decrypt
|
|
22
|
+
* Decrypt ciphertext with encrypted signature validation.
|
|
29
23
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* 3. Decrypt AES-256-CBC
|
|
34
|
-
* 4. Parse JSON payload
|
|
24
|
+
* @param payload - Ciphertext string.
|
|
25
|
+
* @param secret - Shared secret.
|
|
26
|
+
* @param intervalSeconds - Rotation interval.
|
|
35
27
|
*
|
|
36
|
-
*
|
|
28
|
+
* @returns Promise resolving to decrypted data.
|
|
37
29
|
*/
|
|
38
|
-
export declare
|
|
30
|
+
export declare function decrypt<T>(payload: string, secret: string, intervalSeconds: number): Promise<T>;
|
package/dist/index.js
CHANGED
|
@@ -1,134 +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
|
-
*
|
|
8
|
+
* Requirements:
|
|
9
|
+
* - Node.js 18+
|
|
10
|
+
* - Modern browsers
|
|
13
11
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
|
21
|
-
const wordArray = crypto_js_1.default.enc.Base64.parse(base64);
|
|
22
|
-
const json = wordArray.toString(crypto_js_1.default.enc.Utf8);
|
|
23
|
-
try {
|
|
24
|
-
return JSON.parse(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;
|
|
25
18
|
}
|
|
26
|
-
|
|
27
|
-
|
|
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();
|
|
23
|
+
/**
|
|
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.
|
|
33
|
+
*/
|
|
34
|
+
function hexToBuffer(hex) {
|
|
35
|
+
if (!/^[0-9a-fA-F]+$/.test(hex) || hex.length % 2 !== 0) {
|
|
36
|
+
throw new Error("Invalid hex string");
|
|
28
37
|
}
|
|
29
|
-
};
|
|
38
|
+
const bytes = new Uint8Array(hex.match(/.{1,2}/g).map((b) => parseInt(b, 16)));
|
|
39
|
+
return bytes.buffer;
|
|
40
|
+
}
|
|
30
41
|
/**
|
|
31
|
-
* Derive
|
|
32
|
-
*
|
|
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.
|
|
42
|
+
* Derive AES-256-GCM key using HKDF-SHA256.
|
|
38
43
|
*/
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
}
|
|
42
59
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* The key is derived as:
|
|
46
|
-
* SHA256(`${secretInput}:${timeSlot}`)
|
|
47
|
-
*
|
|
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
|
|
60
|
+
* Encrypt raw ArrayBuffer with AES-GCM.
|
|
52
61
|
*/
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
};
|
|
58
|
-
|
|
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
|
+
}
|
|
59
68
|
/**
|
|
60
|
-
*
|
|
61
|
-
|
|
62
|
-
|
|
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.
|
|
63
76
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
77
|
+
* @param data - JSON-serializable data.
|
|
78
|
+
* @param secret - Shared secret.
|
|
79
|
+
* @param intervalSeconds - Rotation interval in seconds.
|
|
66
80
|
*
|
|
67
|
-
*
|
|
68
|
-
* - Included ONLY in HMAC key derivation
|
|
69
|
-
* - Ensures ciphertexts are bound to an application context
|
|
70
|
-
* - Must match during decryption
|
|
81
|
+
* @returns Promise resolving to ciphertext string.
|
|
71
82
|
*/
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
catch (err) {
|
|
87
|
-
console.error("Error in encrypt:", err);
|
|
88
|
-
throw err;
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
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
|
+
}
|
|
92
97
|
/**
|
|
93
|
-
* Decrypt
|
|
98
|
+
* Decrypt ciphertext with encrypted signature validation.
|
|
94
99
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* 3. Decrypt AES-256-CBC
|
|
99
|
-
* 4. Parse JSON payload
|
|
100
|
+
* @param payload - Ciphertext string.
|
|
101
|
+
* @param secret - Shared secret.
|
|
102
|
+
* @param intervalSeconds - Rotation interval.
|
|
100
103
|
*
|
|
101
|
-
*
|
|
104
|
+
* @returns Promise resolving to decrypted data.
|
|
102
105
|
*/
|
|
103
|
-
|
|
104
|
-
const parts =
|
|
105
|
-
if (parts.length !==
|
|
106
|
-
throw new Error("Invalid cipher format
|
|
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");
|
|
106
|
+
async function decrypt(payload, secret, intervalSeconds) {
|
|
107
|
+
const parts = payload.split(":");
|
|
108
|
+
if (parts.length !== 4) {
|
|
109
|
+
throw new Error("Invalid cipher format");
|
|
116
110
|
}
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (!utf8) {
|
|
125
|
-
throw new Error("Decryption produced empty string");
|
|
126
|
-
}
|
|
127
|
-
return JSON.parse(utf8);
|
|
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");
|
|
128
118
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
};
|
|
134
|
-
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
|
+
}
|