timebasedcipher 3.0.1 → 3.0.3
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/cipher.d.ts +81 -0
- package/dist/cipher.js +174 -0
- package/dist/cryptoProvider.d.ts +8 -0
- package/dist/cryptoProvider.js +29 -0
- package/dist/index.d.ts +2 -38
- package/dist/index.js +15 -131
- 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/cipher.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { RuntimeEnv } from "./cryptoProvider";
|
|
2
|
+
/**
|
|
3
|
+
* Options controlling encryption and decryption behavior.
|
|
4
|
+
*/
|
|
5
|
+
export interface CipherOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Runtime environment override.
|
|
8
|
+
*
|
|
9
|
+
* @default "auto"
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* encrypt(data, secret, 60, { env: "node" })
|
|
13
|
+
*/
|
|
14
|
+
env?: RuntimeEnv;
|
|
15
|
+
/**
|
|
16
|
+
* Token expiration time in seconds.
|
|
17
|
+
* Defaults to intervalSeconds if not provided.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* encrypt(data, secret, 60, { ttlSeconds: 300 })
|
|
21
|
+
*/
|
|
22
|
+
ttlSeconds?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Allowed clock drift tolerance in seconds.
|
|
25
|
+
*
|
|
26
|
+
* @default 30
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* decrypt(token, secret, 60, { clockToleranceSeconds: 60 })
|
|
30
|
+
*/
|
|
31
|
+
clockToleranceSeconds?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Custom signature used to validate token purpose/domain.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* encrypt(data, secret, 60, { signature: "auth-session" })
|
|
37
|
+
*/
|
|
38
|
+
signature?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Encrypts JSON data into a secure token.
|
|
42
|
+
*
|
|
43
|
+
* @template T
|
|
44
|
+
* @param data Data to encrypt
|
|
45
|
+
* @param secret Shared secret
|
|
46
|
+
* @param intervalSeconds Key rotation interval
|
|
47
|
+
* @param options Optional settings
|
|
48
|
+
*
|
|
49
|
+
* @returns Secure encrypted token
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* const token = await encrypt(
|
|
53
|
+
* { userId: 1 },
|
|
54
|
+
* "super-secret",
|
|
55
|
+
* 60,
|
|
56
|
+
* { signature: "auth-session", ttlSeconds: 300 }
|
|
57
|
+
* )
|
|
58
|
+
*/
|
|
59
|
+
export declare function encrypt<T>(data: T, secret: string, intervalSeconds: number, options?: CipherOptions): Promise<string>;
|
|
60
|
+
/**
|
|
61
|
+
* Decrypts a token and validates signature, expiration, and replay.
|
|
62
|
+
*
|
|
63
|
+
* @template T
|
|
64
|
+
* @param token Encrypted token
|
|
65
|
+
* @param secret Shared secret
|
|
66
|
+
* @param intervalSeconds Rotation interval
|
|
67
|
+
* @param options Validation options
|
|
68
|
+
*
|
|
69
|
+
* @returns Decrypted data
|
|
70
|
+
*
|
|
71
|
+
* @throws Error if token invalid, expired, replayed, or signature mismatch
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* const data = await decrypt(
|
|
75
|
+
* token,
|
|
76
|
+
* "super-secret",
|
|
77
|
+
* 60,
|
|
78
|
+
* { signature: "auth-session" }
|
|
79
|
+
* )
|
|
80
|
+
*/
|
|
81
|
+
export declare function decrypt<T>(token: string, secret: string, intervalSeconds: number, options?: CipherOptions): Promise<T>;
|
package/dist/cipher.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.encrypt = encrypt;
|
|
4
|
+
exports.decrypt = decrypt;
|
|
5
|
+
const cryptoProvider_1 = require("./cryptoProvider");
|
|
6
|
+
const encoder = new TextEncoder();
|
|
7
|
+
const decoder = new TextDecoder();
|
|
8
|
+
/**
|
|
9
|
+
* Converts ArrayBuffer → Base64URL string.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const encoded = b64url(buffer)
|
|
13
|
+
*/
|
|
14
|
+
function b64url(buffer) {
|
|
15
|
+
return Buffer.from(buffer)
|
|
16
|
+
.toString("base64")
|
|
17
|
+
.replace(/\+/g, "-")
|
|
18
|
+
.replace(/\//g, "_")
|
|
19
|
+
.replace(/=+$/, "");
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Converts Base64URL → ArrayBuffer.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* const buffer = fromB64url(encoded)
|
|
26
|
+
*/
|
|
27
|
+
function fromB64url(str) {
|
|
28
|
+
const pad = str.length % 4 ? "=".repeat(4 - (str.length % 4)) : "";
|
|
29
|
+
const base64 = str.replace(/-/g, "+").replace(/_/g, "/") + pad;
|
|
30
|
+
return Uint8Array.from(Buffer.from(base64, "base64")).buffer;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Cache for derived keys to improve performance.
|
|
34
|
+
*/
|
|
35
|
+
const keyCache = new Map();
|
|
36
|
+
/**
|
|
37
|
+
* Derives AES-256-GCM key using HKDF.
|
|
38
|
+
*
|
|
39
|
+
* @param cryptoObj Web Crypto instance
|
|
40
|
+
* @param secret Shared secret
|
|
41
|
+
* @param timeSlot Rotation slot
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* const key = await deriveKey(crypto, "secret", 12345)
|
|
45
|
+
*/
|
|
46
|
+
async function deriveKey(cryptoObj, secret, timeSlot) {
|
|
47
|
+
const cacheKey = `${secret}:${timeSlot}`;
|
|
48
|
+
if (keyCache.has(cacheKey))
|
|
49
|
+
return keyCache.get(cacheKey);
|
|
50
|
+
const baseKey = await cryptoObj.subtle.importKey("raw", encoder.encode(secret), { name: "HKDF" }, false, ["deriveKey"]);
|
|
51
|
+
const key = await cryptoObj.subtle.deriveKey({
|
|
52
|
+
name: "HKDF",
|
|
53
|
+
hash: "SHA-256",
|
|
54
|
+
salt: encoder.encode(String(timeSlot)),
|
|
55
|
+
info: encoder.encode("time-based-encryption"),
|
|
56
|
+
}, baseKey, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
|
|
57
|
+
keyCache.set(cacheKey, key);
|
|
58
|
+
if (keyCache.size > 50)
|
|
59
|
+
keyCache.clear();
|
|
60
|
+
return key;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* In-memory nonce store for replay protection.
|
|
64
|
+
*/
|
|
65
|
+
const nonceStore = new Set();
|
|
66
|
+
/**
|
|
67
|
+
* Validates nonce uniqueness.
|
|
68
|
+
*
|
|
69
|
+
* @throws Error if nonce already used
|
|
70
|
+
*/
|
|
71
|
+
function checkReplay(nonce) {
|
|
72
|
+
if (nonceStore.has(nonce)) {
|
|
73
|
+
throw new Error("Replay attack detected");
|
|
74
|
+
}
|
|
75
|
+
nonceStore.add(nonce);
|
|
76
|
+
if (nonceStore.size > 1000)
|
|
77
|
+
nonceStore.clear();
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Encrypts JSON data into a secure token.
|
|
81
|
+
*
|
|
82
|
+
* @template T
|
|
83
|
+
* @param data Data to encrypt
|
|
84
|
+
* @param secret Shared secret
|
|
85
|
+
* @param intervalSeconds Key rotation interval
|
|
86
|
+
* @param options Optional settings
|
|
87
|
+
*
|
|
88
|
+
* @returns Secure encrypted token
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* const token = await encrypt(
|
|
92
|
+
* { userId: 1 },
|
|
93
|
+
* "super-secret",
|
|
94
|
+
* 60,
|
|
95
|
+
* { signature: "auth-session", ttlSeconds: 300 }
|
|
96
|
+
* )
|
|
97
|
+
*/
|
|
98
|
+
async function encrypt(data, secret, intervalSeconds, options = {}) {
|
|
99
|
+
var _a, _b;
|
|
100
|
+
const cryptoObj = await (0, cryptoProvider_1.getCrypto)(options.env);
|
|
101
|
+
const now = Math.floor(Date.now() / 1000);
|
|
102
|
+
const ttl = (_a = options.ttlSeconds) !== null && _a !== void 0 ? _a : intervalSeconds;
|
|
103
|
+
const signature = (_b = options.signature) !== null && _b !== void 0 ? _b : "default-signature";
|
|
104
|
+
const payload = {
|
|
105
|
+
sig: signature,
|
|
106
|
+
iat: now,
|
|
107
|
+
exp: now + ttl,
|
|
108
|
+
nonce: cryptoObj.randomUUID(),
|
|
109
|
+
data,
|
|
110
|
+
};
|
|
111
|
+
const timeSlot = Math.floor(now / intervalSeconds);
|
|
112
|
+
const key = await deriveKey(cryptoObj, secret, timeSlot);
|
|
113
|
+
const iv = cryptoObj.getRandomValues(new Uint8Array(12));
|
|
114
|
+
const cipher = await cryptoObj.subtle.encrypt({ name: "AES-GCM", iv }, key, encoder.encode(JSON.stringify(payload)));
|
|
115
|
+
return `v1.${b64url(cipher)}.${b64url(iv.buffer)}`;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Decrypts a token and validates signature, expiration, and replay.
|
|
119
|
+
*
|
|
120
|
+
* @template T
|
|
121
|
+
* @param token Encrypted token
|
|
122
|
+
* @param secret Shared secret
|
|
123
|
+
* @param intervalSeconds Rotation interval
|
|
124
|
+
* @param options Validation options
|
|
125
|
+
*
|
|
126
|
+
* @returns Decrypted data
|
|
127
|
+
*
|
|
128
|
+
* @throws Error if token invalid, expired, replayed, or signature mismatch
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* const data = await decrypt(
|
|
132
|
+
* token,
|
|
133
|
+
* "super-secret",
|
|
134
|
+
* 60,
|
|
135
|
+
* { signature: "auth-session" }
|
|
136
|
+
* )
|
|
137
|
+
*/
|
|
138
|
+
async function decrypt(token, secret, intervalSeconds, options = {}) {
|
|
139
|
+
var _a, _b;
|
|
140
|
+
const cryptoObj = await (0, cryptoProvider_1.getCrypto)(options.env);
|
|
141
|
+
const expectedSignature = (_a = options.signature) !== null && _a !== void 0 ? _a : "default-signature";
|
|
142
|
+
const [version, cipherB64, ivB64] = token.split(".");
|
|
143
|
+
if (version !== "v1")
|
|
144
|
+
throw new Error("Unsupported token version");
|
|
145
|
+
const iv = fromB64url(ivB64);
|
|
146
|
+
const cipher = fromB64url(cipherB64);
|
|
147
|
+
const now = Math.floor(Date.now() / 1000);
|
|
148
|
+
const tolerance = (_b = options.clockToleranceSeconds) !== null && _b !== void 0 ? _b : 30;
|
|
149
|
+
const timeSlots = [
|
|
150
|
+
Math.floor(now / intervalSeconds),
|
|
151
|
+
Math.floor((now - tolerance) / intervalSeconds),
|
|
152
|
+
Math.floor((now + tolerance) / intervalSeconds),
|
|
153
|
+
];
|
|
154
|
+
let payload = null;
|
|
155
|
+
for (const slot of timeSlots) {
|
|
156
|
+
try {
|
|
157
|
+
const key = await deriveKey(cryptoObj, secret, slot);
|
|
158
|
+
const plain = await cryptoObj.subtle.decrypt({ name: "AES-GCM", iv }, key, cipher);
|
|
159
|
+
payload = JSON.parse(decoder.decode(plain));
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
catch { }
|
|
163
|
+
}
|
|
164
|
+
if (!payload)
|
|
165
|
+
throw new Error("Unable to decrypt token");
|
|
166
|
+
if (payload.sig !== expectedSignature) {
|
|
167
|
+
throw new Error("Invalid token signature");
|
|
168
|
+
}
|
|
169
|
+
if (payload.exp + tolerance < now) {
|
|
170
|
+
throw new Error("Token expired");
|
|
171
|
+
}
|
|
172
|
+
checkReplay(payload.nonce);
|
|
173
|
+
return payload.data;
|
|
174
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type RuntimeEnv = "auto" | "browser" | "node";
|
|
2
|
+
/**
|
|
3
|
+
* Returns a Web Crypto compatible Crypto object depending on runtime.
|
|
4
|
+
*
|
|
5
|
+
* - Browser → window.crypto
|
|
6
|
+
* - Node 18+ → node:crypto webcrypto
|
|
7
|
+
*/
|
|
8
|
+
export declare function getCrypto(env?: RuntimeEnv): Promise<Crypto>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getCrypto = getCrypto;
|
|
4
|
+
/**
|
|
5
|
+
* Returns a Web Crypto compatible Crypto object depending on runtime.
|
|
6
|
+
*
|
|
7
|
+
* - Browser → window.crypto
|
|
8
|
+
* - Node 18+ → node:crypto webcrypto
|
|
9
|
+
*/
|
|
10
|
+
async function getCrypto(env = "auto") {
|
|
11
|
+
// Browser detection
|
|
12
|
+
if (env === "browser" || (env === "auto" && typeof window !== "undefined")) {
|
|
13
|
+
if (!globalThis.crypto) {
|
|
14
|
+
throw new Error("Web Crypto API not available in browser");
|
|
15
|
+
}
|
|
16
|
+
return globalThis.crypto;
|
|
17
|
+
}
|
|
18
|
+
// Node detection
|
|
19
|
+
if (env === "node" || env === "auto") {
|
|
20
|
+
try {
|
|
21
|
+
const nodeCrypto = await import("node:crypto");
|
|
22
|
+
return nodeCrypto.webcrypto;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new Error("Web Crypto API not available in Node.js (requires Node 18+)");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
throw new Error("Unable to detect runtime crypto environment");
|
|
29
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,38 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* The key is derived as:
|
|
5
|
-
* SHA256(`${secretInput}:${timeSlot}`)
|
|
6
|
-
*
|
|
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
|
|
11
|
-
*/
|
|
12
|
-
export declare const generateKey: (sharedSecretOrJwt: string, interval: number) => string;
|
|
13
|
-
/**
|
|
14
|
-
* Encrypt arbitrary data using:
|
|
15
|
-
* - AES-256-CBC (confidentiality)
|
|
16
|
-
* - HMAC-SHA256 (integrity & authenticity)
|
|
17
|
-
*
|
|
18
|
-
* Cipher format:
|
|
19
|
-
* cipherHex:ivHex:hmacHex
|
|
20
|
-
*
|
|
21
|
-
* `appName`:
|
|
22
|
-
* - Included ONLY in HMAC key derivation
|
|
23
|
-
* - Ensures ciphertexts are bound to an application context
|
|
24
|
-
* - Must match during decryption
|
|
25
|
-
*/
|
|
26
|
-
export declare const encrypt: (data: any, encryptionKeyHex: string, appName?: string) => string;
|
|
27
|
-
/**
|
|
28
|
-
* Decrypt data encrypted by {@link encrypt}.
|
|
29
|
-
*
|
|
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
|
|
35
|
-
*
|
|
36
|
-
* `appName` MUST match the value used during encryption.
|
|
37
|
-
*/
|
|
38
|
-
export declare const decrypt: (cipherText: string, encryptionKeyHex: string, appName?: string) => unknown;
|
|
1
|
+
export * from "./cipher";
|
|
2
|
+
export * from "./cryptoProvider";
|
package/dist/index.js
CHANGED
|
@@ -1,134 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var
|
|
3
|
-
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
4
15
|
};
|
|
5
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Decode the payload of a JWT without verifying its signature.
|
|
10
|
-
* Used only to read the `exp` claim (seconds since epoch).
|
|
11
|
-
*
|
|
12
|
-
* ⚠️ This does NOT verify authenticity — only structure and expiry.
|
|
13
|
-
*/
|
|
14
|
-
const decodeJwtPayload = (token) => {
|
|
15
|
-
const parts = token.split(".");
|
|
16
|
-
if (parts.length < 2) {
|
|
17
|
-
throw new Error("Invalid JWT: missing payload part");
|
|
18
|
-
}
|
|
19
|
-
const base64Url = parts[1];
|
|
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);
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
throw new Error("Invalid JWT payload JSON");
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
/**
|
|
31
|
-
* Derive a dedicated HMAC key from the encryption key.
|
|
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.
|
|
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.
|
|
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
|
|
52
|
-
*/
|
|
53
|
-
const generateKey = (sharedSecretOrJwt, interval) => {
|
|
54
|
-
const timeSlot = Math.floor(Date.now() / (interval * 1000) + 1000);
|
|
55
|
-
const input = `${sharedSecretOrJwt}:${timeSlot}`;
|
|
56
|
-
return crypto_js_1.default.SHA256(input).toString(crypto_js_1.default.enc.Hex);
|
|
57
|
-
};
|
|
58
|
-
exports.generateKey = generateKey;
|
|
59
|
-
/**
|
|
60
|
-
* Encrypt arbitrary data using:
|
|
61
|
-
* - AES-256-CBC (confidentiality)
|
|
62
|
-
* - HMAC-SHA256 (integrity & authenticity)
|
|
63
|
-
*
|
|
64
|
-
* Cipher format:
|
|
65
|
-
* cipherHex:ivHex:hmacHex
|
|
66
|
-
*
|
|
67
|
-
* `appName`:
|
|
68
|
-
* - Included ONLY in HMAC key derivation
|
|
69
|
-
* - Ensures ciphertexts are bound to an application context
|
|
70
|
-
* - Must match during decryption
|
|
71
|
-
*/
|
|
72
|
-
const encrypt = (data, encryptionKeyHex, appName = "timebasedcipher") => {
|
|
73
|
-
const json = JSON.stringify(data);
|
|
74
|
-
try {
|
|
75
|
-
const key = crypto_js_1.default.enc.Hex.parse(encryptionKeyHex);
|
|
76
|
-
const iv = crypto_js_1.default.lib.WordArray.random(16);
|
|
77
|
-
const encrypted = crypto_js_1.default.AES.encrypt(json, key, { iv });
|
|
78
|
-
const cipherHex = encrypted.ciphertext.toString(crypto_js_1.default.enc.Hex);
|
|
79
|
-
const ivHex = iv.toString(crypto_js_1.default.enc.Hex);
|
|
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}`;
|
|
85
|
-
}
|
|
86
|
-
catch (err) {
|
|
87
|
-
console.error("Error in encrypt:", err);
|
|
88
|
-
throw err;
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
exports.encrypt = encrypt;
|
|
92
|
-
/**
|
|
93
|
-
* Decrypt data encrypted by {@link encrypt}.
|
|
94
|
-
*
|
|
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
|
|
100
|
-
*
|
|
101
|
-
* `appName` MUST match the value used during encryption.
|
|
102
|
-
*/
|
|
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");
|
|
116
|
-
}
|
|
117
|
-
const key = crypto_js_1.default.enc.Hex.parse(encryptionKeyHex);
|
|
118
|
-
const iv = crypto_js_1.default.enc.Hex.parse(ivHex);
|
|
119
|
-
const ciphertext = crypto_js_1.default.enc.Hex.parse(encryptedHex);
|
|
120
|
-
const cipherParams = crypto_js_1.default.lib.CipherParams.create({ ciphertext });
|
|
121
|
-
try {
|
|
122
|
-
const decrypted = crypto_js_1.default.AES.decrypt(cipherParams, key, { iv });
|
|
123
|
-
const utf8 = decrypted.toString(crypto_js_1.default.enc.Utf8);
|
|
124
|
-
if (!utf8) {
|
|
125
|
-
throw new Error("Decryption produced empty string");
|
|
126
|
-
}
|
|
127
|
-
return JSON.parse(utf8);
|
|
128
|
-
}
|
|
129
|
-
catch (err) {
|
|
130
|
-
console.error("Error decrypting:", err);
|
|
131
|
-
throw new Error("Unable to decrypt: invalid key, appName, or corrupted payload");
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
exports.decrypt = decrypt;
|
|
17
|
+
__exportStar(require("./cipher"), exports);
|
|
18
|
+
__exportStar(require("./cryptoProvider"), exports);
|