tecto 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +242 -0
- package/dist/index.d.ts +441 -0
- package/dist/index.js +9 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# TECTO
|
|
2
|
+
|
|
3
|
+
**Transport Encrypted Compact Token Object**
|
|
4
|
+
|
|
5
|
+
An ultra-secure, opaque token protocol powered by **XChaCha20-Poly1305** authenticated encryption. Unlike JWTs, TECTO tokens are fully encrypted — their contents are mathematically unreadable without the 32-byte secret key.
|
|
6
|
+
|
|
7
|
+
## Why Not JWT?
|
|
8
|
+
|
|
9
|
+
| Property | JWT | TECTO |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| **Payload visibility** | Base64-encoded (readable by anyone) | Fully encrypted (opaque noise) |
|
|
12
|
+
| **Cipher** | None (signed, not encrypted) | XChaCha20-Poly1305 (AEAD) |
|
|
13
|
+
| **Nonce** | N/A | 24-byte CSPRNG per token |
|
|
14
|
+
| **Key size** | Variable | Exactly 256-bit (enforced) |
|
|
15
|
+
| **Tamper detection** | HMAC/RSA signature | Poly1305 authentication tag |
|
|
16
|
+
| **Error specificity** | Reveals failure reason | Generic "Invalid token" (prevents oracles) |
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
bun add tecto
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import {
|
|
28
|
+
generateSecureKey,
|
|
29
|
+
MemoryKeyStore,
|
|
30
|
+
TectoCoder,
|
|
31
|
+
} from "tecto";
|
|
32
|
+
|
|
33
|
+
// 1. Generate a 256-bit key
|
|
34
|
+
const key = generateSecureKey();
|
|
35
|
+
|
|
36
|
+
// 2. Set up the key store
|
|
37
|
+
const store = new MemoryKeyStore();
|
|
38
|
+
store.addKey("my-key-2024", key);
|
|
39
|
+
|
|
40
|
+
// 3. Create a coder
|
|
41
|
+
const coder = new TectoCoder(store);
|
|
42
|
+
|
|
43
|
+
// 4. Encrypt a payload
|
|
44
|
+
const token = coder.encrypt(
|
|
45
|
+
{ userId: 42, role: "admin" },
|
|
46
|
+
{ expiresIn: "1h", issuer: "my-app" }
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
console.log(token);
|
|
50
|
+
// → tecto.v1.my-key-2024.base64url_nonce.base64url_ciphertext
|
|
51
|
+
|
|
52
|
+
// 5. Decrypt it
|
|
53
|
+
const payload = coder.decrypt(token);
|
|
54
|
+
console.log(payload.userId); // 42
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Token Format
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
tecto.v1.<kid>.<nonce>.<ciphertext>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
| Segment | Description |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `tecto` | Protocol identifier |
|
|
66
|
+
| `v1` | Protocol version |
|
|
67
|
+
| `<kid>` | Key identifier (for key rotation) |
|
|
68
|
+
| `<nonce>` | 24-byte Base64URL-encoded CSPRNG nonce |
|
|
69
|
+
| `<ciphertext>` | Base64URL-encoded XChaCha20-Poly1305 ciphertext + auth tag |
|
|
70
|
+
|
|
71
|
+
## API Reference
|
|
72
|
+
|
|
73
|
+
### Security Utilities
|
|
74
|
+
|
|
75
|
+
#### `generateSecureKey(): Uint8Array`
|
|
76
|
+
|
|
77
|
+
Generates a 32-byte cryptographically random key using the platform CSPRNG.
|
|
78
|
+
|
|
79
|
+
#### `constantTimeCompare(a: Uint8Array, b: Uint8Array): boolean`
|
|
80
|
+
|
|
81
|
+
Constant-time byte comparison. Prevents timing side-channel attacks when comparing secrets.
|
|
82
|
+
|
|
83
|
+
#### `assertEntropy(key: Uint8Array): void`
|
|
84
|
+
|
|
85
|
+
Validates a key has sufficient entropy. Rejects all-zeros, repeating bytes, and keys with fewer than 8 unique byte values.
|
|
86
|
+
|
|
87
|
+
### `KeyStoreAdapter` (Interface)
|
|
88
|
+
|
|
89
|
+
The contract for all key store implementations. `TectoCoder` accepts any `KeyStoreAdapter`.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
interface KeyStoreAdapter {
|
|
93
|
+
addKey(id: string, secret: Uint8Array): void | Promise<void>;
|
|
94
|
+
getKey(id: string): Uint8Array;
|
|
95
|
+
rotate(newId: string, newSecret: Uint8Array): void | Promise<void>;
|
|
96
|
+
removeKey(id: string): void | Promise<void>;
|
|
97
|
+
getCurrentKeyId(): string;
|
|
98
|
+
readonly size: number;
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### `MemoryKeyStore`
|
|
103
|
+
|
|
104
|
+
Built-in adapter. Keys live in memory and are lost on restart.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
const store = new MemoryKeyStore();
|
|
108
|
+
store.addKey("key-id", key); // Add a key (first key becomes current)
|
|
109
|
+
store.getKey("key-id"); // Retrieve by ID
|
|
110
|
+
store.rotate("new-key-id", newKey); // Add new key + set as current
|
|
111
|
+
store.removeKey("old-key-id"); // Revoke and zero memory
|
|
112
|
+
store.getCurrentKeyId(); // Get current key ID
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Custom Adapter
|
|
116
|
+
|
|
117
|
+
Implement `KeyStoreAdapter` to use any storage backend:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { MemoryKeyStore, assertEntropy } from "tecto";
|
|
121
|
+
import type { KeyStoreAdapter } from "tecto";
|
|
122
|
+
|
|
123
|
+
class MyDatabaseKeyStore implements KeyStoreAdapter {
|
|
124
|
+
private mem = new MemoryKeyStore();
|
|
125
|
+
|
|
126
|
+
addKey(id: string, secret: Uint8Array): void {
|
|
127
|
+
assertEntropy(secret);
|
|
128
|
+
this.mem.addKey(id, secret);
|
|
129
|
+
// persist to your DB here
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
getKey(id: string): Uint8Array { return this.mem.getKey(id); }
|
|
133
|
+
rotate(newId: string, s: Uint8Array): void { this.mem.rotate(newId, s); }
|
|
134
|
+
removeKey(id: string): void { this.mem.removeKey(id); }
|
|
135
|
+
getCurrentKeyId(): string { return this.mem.getCurrentKeyId(); }
|
|
136
|
+
get size(): number { return this.mem.size; }
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### `TectoCoder`
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
const coder = new TectoCoder(store); // any KeyStoreAdapter
|
|
144
|
+
|
|
145
|
+
// Encrypt
|
|
146
|
+
const token = coder.encrypt(payload, options?);
|
|
147
|
+
|
|
148
|
+
// Decrypt
|
|
149
|
+
const payload = coder.decrypt<MyType>(token);
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
#### `SignOptions`
|
|
153
|
+
|
|
154
|
+
| Option | Type | Description |
|
|
155
|
+
|---|---|---|
|
|
156
|
+
| `expiresIn` | `string` | Duration string: `"1h"`, `"30m"`, `"7d"`, `"120s"` |
|
|
157
|
+
| `issuer` | `string` | Sets the `iss` claim |
|
|
158
|
+
| `audience` | `string` | Sets the `aud` claim |
|
|
159
|
+
| `jti` | `string` | Custom token ID (auto-generated if omitted) |
|
|
160
|
+
| `notBefore` | `string` | Duration string for `nbf` delay |
|
|
161
|
+
|
|
162
|
+
### Error Classes
|
|
163
|
+
|
|
164
|
+
| Error | Code | When |
|
|
165
|
+
|---|---|---|
|
|
166
|
+
| `TectoError` | `TECTO_*` | Base class for all errors |
|
|
167
|
+
| `TokenExpiredError` | `TECTO_TOKEN_EXPIRED` | `exp` claim is in the past |
|
|
168
|
+
| `TokenNotActiveError` | `TECTO_TOKEN_NOT_ACTIVE` | `nbf` claim is in the future |
|
|
169
|
+
| `InvalidSignatureError` | `TECTO_INVALID_TOKEN` | Any decryption/auth failure (generic) |
|
|
170
|
+
| `KeyError` | `TECTO_KEY_ERROR` | Invalid or missing key |
|
|
171
|
+
|
|
172
|
+
## Security Properties
|
|
173
|
+
|
|
174
|
+
- **Opacity:** Tokens are encrypted, not just signed. Without the key, the payload is indistinguishable from random noise.
|
|
175
|
+
- **Authenticated Encryption:** Poly1305 tag ensures integrity. Any modification to the ciphertext, nonce, or key ID causes immediate rejection.
|
|
176
|
+
- **Unique Nonces:** Every `encrypt()` call generates a fresh 24-byte nonce from CSPRNG. XChaCha20's 192-bit nonce space makes collisions negligible.
|
|
177
|
+
- **Generic Errors:** All decryption failures produce the same `InvalidSignatureError` to prevent padding/authentication oracles.
|
|
178
|
+
- **Entropy Enforcement:** Keys are validated for length (32 bytes), non-zero, non-repeating, and minimum byte diversity.
|
|
179
|
+
- **Timing-Safe Comparison:** `constantTimeCompare()` prevents timing side-channels when comparing secrets.
|
|
180
|
+
|
|
181
|
+
## Key Rotation
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
store.addKey("key-2024-01", key1);
|
|
185
|
+
// ... time passes ...
|
|
186
|
+
store.rotate("key-2024-06", key2);
|
|
187
|
+
|
|
188
|
+
// New tokens use key-2024-06, old tokens still decrypt via key-2024-01
|
|
189
|
+
store.removeKey("key-2024-01"); // after all old tokens expire
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Testing
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
bun test
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Examples
|
|
199
|
+
|
|
200
|
+
Each example implements `KeyStoreAdapter` with a different storage backend.
|
|
201
|
+
|
|
202
|
+
### Memory (Default)
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
bun run examples/memory/index.ts
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### SQLite
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
bun run examples/sqlite/index.ts
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### MariaDB / MySQL
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
bun add mysql2
|
|
218
|
+
bun run examples/mariadb/index.ts
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### PostgreSQL
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
bun add pg @types/pg
|
|
225
|
+
bun run examples/postgres/index.ts
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Architecture
|
|
229
|
+
|
|
230
|
+
All adapters follow the same pattern — compose a `MemoryKeyStore` internally for runtime lookups and sync writes to your database:
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
┌──────────┐ load ┌────────────────┐ encrypt/decrypt ┌────────────┐
|
|
234
|
+
│ Database │ ───────→ │ KeyStoreAdapter│ ←────────────────→ │ TectoCoder │
|
|
235
|
+
│ (persist)│ ←─────── │ (runtime) │ │ │
|
|
236
|
+
└──────────┘ save └────────────────┘ └────────────┘
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## License
|
|
240
|
+
|
|
241
|
+
MIT
|
|
242
|
+
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TECTO Type Definitions
|
|
3
|
+
*
|
|
4
|
+
* Strict type definitions for token payloads and signing options.
|
|
5
|
+
* All types are designed to enforce correctness at compile time.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Standard registered claims for a TECTO token.
|
|
11
|
+
*
|
|
12
|
+
* @security All time-based claims (`exp`, `nbf`, `iat`) are stored as
|
|
13
|
+
* Unix timestamps (seconds since epoch) to avoid timezone ambiguities
|
|
14
|
+
* that could lead to premature or delayed expiration.
|
|
15
|
+
*/
|
|
16
|
+
interface TectoRegisteredClaims {
|
|
17
|
+
/** Expiration time (Unix timestamp in seconds). */
|
|
18
|
+
readonly exp?: number;
|
|
19
|
+
/** Not-before time (Unix timestamp in seconds). */
|
|
20
|
+
readonly nbf?: number;
|
|
21
|
+
/** Issued-at time (Unix timestamp in seconds). */
|
|
22
|
+
readonly iat?: number;
|
|
23
|
+
/** Unique token identifier for replay protection. */
|
|
24
|
+
readonly jti?: string;
|
|
25
|
+
/** Issuer identifier. */
|
|
26
|
+
readonly iss?: string;
|
|
27
|
+
/** Audience identifier. */
|
|
28
|
+
readonly aud?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Complete TECTO token payload including registered claims
|
|
32
|
+
* and user-defined custom fields.
|
|
33
|
+
*
|
|
34
|
+
* @typeParam T - User-defined payload fields. Must be a plain object type.
|
|
35
|
+
*
|
|
36
|
+
* @security Custom fields are encrypted alongside registered claims,
|
|
37
|
+
* ensuring all data is opaque without the secret key.
|
|
38
|
+
*/
|
|
39
|
+
type TectoPayload<T extends Record<string, unknown> = Record<string, unknown>> = TectoRegisteredClaims & T;
|
|
40
|
+
/**
|
|
41
|
+
* Options for token encryption (signing).
|
|
42
|
+
*
|
|
43
|
+
* @security The `expiresIn` field accepts human-readable duration strings
|
|
44
|
+
* (e.g., `"1h"`, `"30m"`, `"7d"`) and is converted to an absolute `exp`
|
|
45
|
+
* claim internally. Always prefer short-lived tokens.
|
|
46
|
+
*/
|
|
47
|
+
interface SignOptions {
|
|
48
|
+
/**
|
|
49
|
+
* Token lifetime as a duration string.
|
|
50
|
+
* Supported units: `s` (seconds), `m` (minutes), `h` (hours), `d` (days).
|
|
51
|
+
*
|
|
52
|
+
* @example "1h" — expires in 1 hour
|
|
53
|
+
* @example "30m" — expires in 30 minutes
|
|
54
|
+
* @example "7d" — expires in 7 days
|
|
55
|
+
*/
|
|
56
|
+
readonly expiresIn?: string;
|
|
57
|
+
/** Issuer claim (`iss`). */
|
|
58
|
+
readonly issuer?: string;
|
|
59
|
+
/** Audience claim (`aud`). */
|
|
60
|
+
readonly audience?: string;
|
|
61
|
+
/** Unique token ID (`jti`). If omitted, one is generated automatically. */
|
|
62
|
+
readonly jti?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Not-before delay as a duration string.
|
|
65
|
+
* The token will not be valid until this duration has elapsed from `iat`.
|
|
66
|
+
*
|
|
67
|
+
* @example "5m" — token becomes active 5 minutes after issuance
|
|
68
|
+
*/
|
|
69
|
+
readonly notBefore?: string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The contract that all key store implementations must satisfy.
|
|
73
|
+
*
|
|
74
|
+
* Implementations include `MemoryKeyStore` (built-in), `SqliteKeyStore`,
|
|
75
|
+
* `MariaDbKeyStore`, and `PostgresKeyStore`.
|
|
76
|
+
*
|
|
77
|
+
* @security All implementations MUST:
|
|
78
|
+
* - Store keys as `Uint8Array` (never strings) to prevent JS engine internalization.
|
|
79
|
+
* - Validate key entropy via `assertEntropy()` before storing.
|
|
80
|
+
* - Clone key material on ingestion to prevent external mutation.
|
|
81
|
+
*/
|
|
82
|
+
interface KeyStoreAdapter {
|
|
83
|
+
/**
|
|
84
|
+
* Adds a key to the store. If this is the first key, it becomes the current key.
|
|
85
|
+
*
|
|
86
|
+
* @param id - A unique identifier for the key (e.g., `"key-2024-01"`).
|
|
87
|
+
* @param secret - A 32-byte `Uint8Array` key. Must pass entropy validation.
|
|
88
|
+
* @throws {KeyError} If `secret` is not exactly 32 bytes or has insufficient entropy.
|
|
89
|
+
*/
|
|
90
|
+
addKey(id: string, secret: Uint8Array): void | Promise<void>;
|
|
91
|
+
/**
|
|
92
|
+
* Retrieves a key by its identifier.
|
|
93
|
+
*
|
|
94
|
+
* @param id - The key identifier to look up.
|
|
95
|
+
* @returns The key as a `Uint8Array`.
|
|
96
|
+
* @throws {KeyError} If no key exists with the given identifier.
|
|
97
|
+
*/
|
|
98
|
+
getKey(id: string): Uint8Array;
|
|
99
|
+
/**
|
|
100
|
+
* Rotates to a new key. The new key becomes the current key used for
|
|
101
|
+
* encryption. Old keys MUST be retained for decrypting existing tokens.
|
|
102
|
+
*
|
|
103
|
+
* @param newId - Identifier for the new key.
|
|
104
|
+
* @param newSecret - A 32-byte `Uint8Array` key.
|
|
105
|
+
* @throws {KeyError} If `newSecret` fails entropy validation.
|
|
106
|
+
*/
|
|
107
|
+
rotate(newId: string, newSecret: Uint8Array): void | Promise<void>;
|
|
108
|
+
/**
|
|
109
|
+
* Removes a key from the store.
|
|
110
|
+
*
|
|
111
|
+
* @param id - The key identifier to remove.
|
|
112
|
+
* @throws {KeyError} If no key exists or if attempting to remove the current key.
|
|
113
|
+
*/
|
|
114
|
+
removeKey(id: string): void | Promise<void>;
|
|
115
|
+
/**
|
|
116
|
+
* Returns the identifier of the current active key used for encryption.
|
|
117
|
+
*
|
|
118
|
+
* @throws {KeyError} If no keys have been added to the store.
|
|
119
|
+
*/
|
|
120
|
+
getCurrentKeyId(): string;
|
|
121
|
+
/** Returns the total number of keys in the store. */
|
|
122
|
+
readonly size: number;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* TECTO Core — Encrypt & Decrypt
|
|
127
|
+
*
|
|
128
|
+
* Implements the TECTO token protocol using XChaCha20-Poly1305 (AEAD)
|
|
129
|
+
* for authenticated encryption. Tokens are fully opaque — their payload
|
|
130
|
+
* cannot be read without the 32-byte secret key.
|
|
131
|
+
*
|
|
132
|
+
* **Token format:** `tecto.v1.<kid>.<nonce_b64url>.<ciphertext_b64url>`
|
|
133
|
+
*
|
|
134
|
+
* @security
|
|
135
|
+
* - A fresh 24-byte nonce is generated via CSPRNG for every encryption call.
|
|
136
|
+
* XChaCha20's 192-bit nonce space makes collision probability negligible
|
|
137
|
+
* (birthday bound at ~2^96 messages per key).
|
|
138
|
+
* - Poly1305 provides authentication: any modification to the ciphertext
|
|
139
|
+
* or nonce causes decryption to fail.
|
|
140
|
+
* - Decryption failures always throw a generic `InvalidSignatureError`
|
|
141
|
+
* to prevent oracle attacks.
|
|
142
|
+
*
|
|
143
|
+
* @module
|
|
144
|
+
*/
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The main TECTO encoder/decoder. Encrypts payloads into opaque tokens
|
|
148
|
+
* and decrypts them back, with automatic claim validation.
|
|
149
|
+
*
|
|
150
|
+
* @security
|
|
151
|
+
* - Every `encrypt()` call generates a unique 24-byte nonce from CSPRNG.
|
|
152
|
+
* - Every `decrypt()` failure throws a generic `InvalidSignatureError`.
|
|
153
|
+
* - `exp` and `nbf` claims are validated automatically after decryption.
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```ts
|
|
157
|
+
* const store = new MemoryKeyStore();
|
|
158
|
+
* store.addKey("k1", generateSecureKey());
|
|
159
|
+
*
|
|
160
|
+
* const coder = new TectoCoder(store);
|
|
161
|
+
* const token = coder.encrypt({ userId: 42 }, { expiresIn: "1h" });
|
|
162
|
+
* const payload = coder.decrypt(token);
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
declare class TectoCoder {
|
|
166
|
+
private readonly keyStore;
|
|
167
|
+
/**
|
|
168
|
+
* Creates a new `TectoCoder` bound to the given key store.
|
|
169
|
+
*
|
|
170
|
+
* @param keyStore - Any implementation of `KeyStoreAdapter`.
|
|
171
|
+
*/
|
|
172
|
+
constructor(keyStore: KeyStoreAdapter);
|
|
173
|
+
/**
|
|
174
|
+
* Encrypts a payload into an opaque TECTO token.
|
|
175
|
+
*
|
|
176
|
+
* @typeParam T - The shape of the custom payload fields.
|
|
177
|
+
* @param payload - The data to encrypt. Must be JSON-serializable.
|
|
178
|
+
* @param options - Optional signing options (expiration, issuer, etc.).
|
|
179
|
+
* @returns An opaque token string in the format `tecto.v1.<kid>.<nonce>.<ciphertext>`.
|
|
180
|
+
*
|
|
181
|
+
* @security
|
|
182
|
+
* - A fresh 24-byte nonce is generated for EVERY call via CSPRNG.
|
|
183
|
+
* Never reuse nonces — XChaCha20's security relies on nonce uniqueness.
|
|
184
|
+
* - The `iat` claim is always set to the current time.
|
|
185
|
+
* - If `expiresIn` is provided, `exp` is computed as `iat + duration`.
|
|
186
|
+
*/
|
|
187
|
+
encrypt<T extends Record<string, unknown>>(payload: T, options?: SignOptions): string;
|
|
188
|
+
/**
|
|
189
|
+
* Decrypts and validates an opaque TECTO token.
|
|
190
|
+
*
|
|
191
|
+
* @typeParam T - The expected shape of the custom payload fields.
|
|
192
|
+
* @param token - The opaque token string to decrypt.
|
|
193
|
+
* @returns The decrypted and validated payload.
|
|
194
|
+
* @throws {InvalidSignatureError} If the token structure is invalid,
|
|
195
|
+
* the key is wrong, or the ciphertext has been tampered with.
|
|
196
|
+
* @throws {TokenExpiredError} If the `exp` claim is in the past.
|
|
197
|
+
* @throws {TokenNotActiveError} If the `nbf` claim is in the future.
|
|
198
|
+
*
|
|
199
|
+
* @security
|
|
200
|
+
* - All structural and cryptographic failures throw the same generic
|
|
201
|
+
* `InvalidSignatureError` to prevent oracle attacks.
|
|
202
|
+
* - Time-based claim errors (`exp`, `nbf`) are thrown only AFTER
|
|
203
|
+
* successful decryption, so they cannot be used to probe ciphertext.
|
|
204
|
+
*/
|
|
205
|
+
decrypt<T extends Record<string, unknown>>(token: string): TectoPayload<T>;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* TECTO Error Hierarchy
|
|
210
|
+
*
|
|
211
|
+
* All errors extend the base `TectoError` class. Error messages are
|
|
212
|
+
* intentionally generic where security-sensitive to prevent oracle attacks.
|
|
213
|
+
*
|
|
214
|
+
* @security Generic error messages prevent attackers from distinguishing
|
|
215
|
+
* between padding failures, authentication failures, and other decryption
|
|
216
|
+
* errors — eliminating side-channel information leakage.
|
|
217
|
+
*
|
|
218
|
+
* @module
|
|
219
|
+
*/
|
|
220
|
+
/**
|
|
221
|
+
* Base error class for all TECTO-related errors.
|
|
222
|
+
*
|
|
223
|
+
* @security Consumers should catch `TectoError` as the top-level type
|
|
224
|
+
* and avoid exposing internal error details to end users.
|
|
225
|
+
*/
|
|
226
|
+
declare class TectoError extends Error {
|
|
227
|
+
readonly code: string;
|
|
228
|
+
constructor(message: string, code: string);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Thrown when a token's `exp` claim is in the past.
|
|
232
|
+
*
|
|
233
|
+
* @security This is safe to throw distinctly from `InvalidSignatureError`
|
|
234
|
+
* because expiration is checked *after* successful decryption and
|
|
235
|
+
* authentication, so no ciphertext oracle is possible.
|
|
236
|
+
*/
|
|
237
|
+
declare class TokenExpiredError extends TectoError {
|
|
238
|
+
readonly expiredAt: Date;
|
|
239
|
+
constructor(expiredAt: Date);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Thrown when decryption or authentication fails for any reason.
|
|
243
|
+
*
|
|
244
|
+
* @security This error intentionally uses a single generic message for ALL
|
|
245
|
+
* decryption failures — whether the key is wrong, the ciphertext is tampered,
|
|
246
|
+
* the nonce is invalid, or the Poly1305 tag doesn't match. Revealing the
|
|
247
|
+
* specific failure mode would create a padding/authentication oracle.
|
|
248
|
+
*/
|
|
249
|
+
declare class InvalidSignatureError extends TectoError {
|
|
250
|
+
constructor();
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Thrown when a cryptographic key fails validation.
|
|
254
|
+
*
|
|
255
|
+
* @security Key-related errors are safe to be descriptive because they
|
|
256
|
+
* occur during setup/configuration, not during token verification flows
|
|
257
|
+
* that an attacker could probe.
|
|
258
|
+
*/
|
|
259
|
+
declare class KeyError extends TectoError {
|
|
260
|
+
constructor(message: string);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Thrown when a token's `nbf` (not before) claim is in the future.
|
|
264
|
+
*
|
|
265
|
+
* @security Like `TokenExpiredError`, this is checked after successful
|
|
266
|
+
* decryption so it does not leak ciphertext information.
|
|
267
|
+
*/
|
|
268
|
+
declare class TokenNotActiveError extends TectoError {
|
|
269
|
+
readonly activeAt: Date;
|
|
270
|
+
constructor(activeAt: Date);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* TECTO In-Memory Key Store
|
|
275
|
+
*
|
|
276
|
+
* Manages cryptographic keys for the TECTO protocol. Keys are stored
|
|
277
|
+
* exclusively as `Uint8Array` and are never converted to strings.
|
|
278
|
+
*
|
|
279
|
+
* @security Key material stored as strings can be interned by the
|
|
280
|
+
* JavaScript engine, making it impossible to reliably clear from memory.
|
|
281
|
+
* `Uint8Array` can be zeroed out explicitly when keys are removed.
|
|
282
|
+
*
|
|
283
|
+
* @module
|
|
284
|
+
*/
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* An in-memory key store that supports key rotation and explicit revocation.
|
|
288
|
+
*
|
|
289
|
+
* @security
|
|
290
|
+
* - Keys are stored as `Uint8Array` (never strings) to avoid JS engine internalization.
|
|
291
|
+
* - On `removeKey()`, the key buffer is zeroed before deletion.
|
|
292
|
+
* - `rotate()` adds a new key without removing old ones, so existing tokens
|
|
293
|
+
* encrypted under previous keys remain decryptable during the rotation window.
|
|
294
|
+
*
|
|
295
|
+
* @example
|
|
296
|
+
* ```ts
|
|
297
|
+
* const store = new MemoryKeyStore();
|
|
298
|
+
* store.addKey("key-2024-01", generateSecureKey());
|
|
299
|
+
* const key = store.getKey("key-2024-01");
|
|
300
|
+
* ```
|
|
301
|
+
*/
|
|
302
|
+
declare class MemoryKeyStore implements KeyStoreAdapter {
|
|
303
|
+
private readonly keys;
|
|
304
|
+
private currentKeyId;
|
|
305
|
+
/**
|
|
306
|
+
* Adds a key to the store. If this is the first key, it becomes the current key.
|
|
307
|
+
*
|
|
308
|
+
* @param id - A unique identifier for the key (e.g., `"key-2024-01"`).
|
|
309
|
+
* @param secret - A 32-byte `Uint8Array` key. Validated for entropy.
|
|
310
|
+
* @throws {KeyError} If `secret` is not exactly 32 bytes or has insufficient entropy.
|
|
311
|
+
*
|
|
312
|
+
* @security The secret is cloned internally to prevent external mutation.
|
|
313
|
+
* The original array can be safely zeroed after calling this method.
|
|
314
|
+
*/
|
|
315
|
+
addKey(id: string, secret: Uint8Array): void;
|
|
316
|
+
/**
|
|
317
|
+
* Retrieves a key by its identifier.
|
|
318
|
+
*
|
|
319
|
+
* @param id - The key identifier to look up.
|
|
320
|
+
* @returns The key as a `Uint8Array`.
|
|
321
|
+
* @throws {KeyError} If no key exists with the given identifier.
|
|
322
|
+
*
|
|
323
|
+
* @security Returns a reference to the internal buffer. Do NOT
|
|
324
|
+
* modify or zero the returned array — use `removeKey()` for revocation.
|
|
325
|
+
*/
|
|
326
|
+
getKey(id: string): Uint8Array;
|
|
327
|
+
/**
|
|
328
|
+
* Rotates to a new key. The new key becomes the current key used for
|
|
329
|
+
* encryption. Old keys are retained for decrypting existing tokens.
|
|
330
|
+
*
|
|
331
|
+
* @param newId - Identifier for the new key.
|
|
332
|
+
* @param newSecret - A 32-byte `Uint8Array` key.
|
|
333
|
+
* @throws {KeyError} If `newSecret` fails entropy validation.
|
|
334
|
+
*
|
|
335
|
+
* @security Old keys are intentionally kept so that tokens encrypted
|
|
336
|
+
* under previous keys can still be decrypted. Call `removeKey()` to
|
|
337
|
+
* explicitly revoke an old key when all tokens using it have expired.
|
|
338
|
+
*/
|
|
339
|
+
rotate(newId: string, newSecret: Uint8Array): void;
|
|
340
|
+
/**
|
|
341
|
+
* Removes a key from the store and zeros its memory.
|
|
342
|
+
*
|
|
343
|
+
* @param id - The key identifier to remove.
|
|
344
|
+
* @throws {KeyError} If no key exists with the given identifier.
|
|
345
|
+
* @throws {KeyError} If attempting to remove the current active key.
|
|
346
|
+
*
|
|
347
|
+
* @security The key buffer is filled with zeros before being deleted
|
|
348
|
+
* from the map. This is a best-effort measure — the garbage collector
|
|
349
|
+
* may have already copied the buffer. For maximum security, rotate
|
|
350
|
+
* keys frequently and keep rotation windows short.
|
|
351
|
+
*/
|
|
352
|
+
removeKey(id: string): void;
|
|
353
|
+
/**
|
|
354
|
+
* Returns the identifier of the current active key used for encryption.
|
|
355
|
+
*
|
|
356
|
+
* @returns The current key identifier.
|
|
357
|
+
* @throws {KeyError} If no keys have been added to the store.
|
|
358
|
+
*/
|
|
359
|
+
getCurrentKeyId(): string;
|
|
360
|
+
/**
|
|
361
|
+
* Returns the total number of keys currently in the store.
|
|
362
|
+
*/
|
|
363
|
+
get size(): number;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* TECTO Security Utilities
|
|
368
|
+
*
|
|
369
|
+
* Low-level cryptographic helpers for key generation, constant-time
|
|
370
|
+
* comparison, and entropy validation. These are the foundation of
|
|
371
|
+
* the entire security model.
|
|
372
|
+
*
|
|
373
|
+
* @security Every function in this module is designed to be safe
|
|
374
|
+
* against timing attacks, weak key injection, and other side-channel vectors.
|
|
375
|
+
*
|
|
376
|
+
* @module
|
|
377
|
+
*/
|
|
378
|
+
/**
|
|
379
|
+
* Generates a cryptographically secure 32-byte (256-bit) random key
|
|
380
|
+
* suitable for use with XChaCha20-Poly1305.
|
|
381
|
+
*
|
|
382
|
+
* @returns A new `Uint8Array` of 32 cryptographically random bytes.
|
|
383
|
+
*
|
|
384
|
+
* @security Uses the platform's CSPRNG (`crypto.getRandomValues`), which
|
|
385
|
+
* draws from the OS entropy pool. The returned key is never converted
|
|
386
|
+
* to a string to prevent it from being interned by the JavaScript engine
|
|
387
|
+
* or appearing in heap snapshots.
|
|
388
|
+
*
|
|
389
|
+
* @example
|
|
390
|
+
* ```ts
|
|
391
|
+
* const key = generateSecureKey();
|
|
392
|
+
* // key is a Uint8Array of 32 random bytes
|
|
393
|
+
* ```
|
|
394
|
+
*/
|
|
395
|
+
declare function generateSecureKey(): Uint8Array;
|
|
396
|
+
/**
|
|
397
|
+
* Performs a constant-time comparison of two byte arrays.
|
|
398
|
+
*
|
|
399
|
+
* @param a - First byte array.
|
|
400
|
+
* @param b - Second byte array.
|
|
401
|
+
* @returns `true` if both arrays are identical, `false` otherwise.
|
|
402
|
+
*
|
|
403
|
+
* @security This function MUST be used instead of `===` or `Buffer.equals()`
|
|
404
|
+
* when comparing secrets, MACs, or key material. A naive comparison
|
|
405
|
+
* short-circuits on the first differing byte, leaking the position of
|
|
406
|
+
* the mismatch via timing. This implementation XORs all bytes and
|
|
407
|
+
* accumulates differences, ensuring the execution time is constant
|
|
408
|
+
* regardless of where (or whether) the arrays differ.
|
|
409
|
+
*
|
|
410
|
+
* @example
|
|
411
|
+
* ```ts
|
|
412
|
+
* const isValid = constantTimeCompare(receivedMac, computedMac);
|
|
413
|
+
* ```
|
|
414
|
+
*/
|
|
415
|
+
declare function constantTimeCompare(a: Uint8Array, b: Uint8Array): boolean;
|
|
416
|
+
/**
|
|
417
|
+
* Validates that a key meets minimum entropy requirements for
|
|
418
|
+
* use with TECTO's XChaCha20-Poly1305 cipher.
|
|
419
|
+
*
|
|
420
|
+
* @param key - The key material to validate.
|
|
421
|
+
* @throws {KeyError} If the key is not exactly 32 bytes, is all zeros,
|
|
422
|
+
* is a repeating single byte, or has fewer than 8 unique byte values.
|
|
423
|
+
*
|
|
424
|
+
* @security This function prevents common mistakes:
|
|
425
|
+
* - **All-zeros key:** Equivalent to no encryption at all.
|
|
426
|
+
* - **Repeating byte:** Patterns like `0xAA` repeated 32 times are trivially guessable.
|
|
427
|
+
* - **Low diversity:** Keys with fewer than 8 unique byte values lack
|
|
428
|
+
* sufficient entropy for 256-bit security.
|
|
429
|
+
* - **Wrong length:** XChaCha20-Poly1305 requires exactly 32 bytes.
|
|
430
|
+
*
|
|
431
|
+
* This is a heuristic check, not a formal entropy measurement. For
|
|
432
|
+
* production use, always generate keys with {@link generateSecureKey}.
|
|
433
|
+
*
|
|
434
|
+
* @example
|
|
435
|
+
* ```ts
|
|
436
|
+
* assertEntropy(myKey); // throws KeyError if key is weak
|
|
437
|
+
* ```
|
|
438
|
+
*/
|
|
439
|
+
declare function assertEntropy(key: Uint8Array): void;
|
|
440
|
+
|
|
441
|
+
export { InvalidSignatureError, KeyError, type KeyStoreAdapter, MemoryKeyStore, type SignOptions, TectoCoder, TectoError, type TectoPayload, type TectoRegisteredClaims, TokenExpiredError, TokenNotActiveError, assertEntropy, constantTimeCompare, generateSecureKey };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";var pe=Object.defineProperty;var qe=Object.getOwnPropertyDescriptor;var Xe=Object.getOwnPropertyNames;var Qe=Object.prototype.hasOwnProperty;var Ze=(t,e)=>{for(var r in e)pe(t,r,{get:e[r],enumerable:!0})},et=(t,e,r,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of Xe(e))!Qe.call(t,s)&&s!==r&&pe(t,s,{get:()=>e[s],enumerable:!(o=qe(e,s))||o.enumerable});return t};var tt=t=>et(pe({},"__esModule",{value:!0}),t);var Lt={};Ze(Lt,{InvalidSignatureError:()=>V,KeyError:()=>P,MemoryKeyStore:()=>de,TectoCoder:()=>he,TectoError:()=>z,TokenExpiredError:()=>ee,TokenNotActiveError:()=>te,assertEntropy:()=>ue,constantTimeCompare:()=>Ye,generateSecureKey:()=>Je});module.exports=tt(Lt);function ne(t){if(!Number.isSafeInteger(t)||t<0)throw new Error("positive integer expected, got "+t)}function ye(t){return t instanceof Uint8Array||ArrayBuffer.isView(t)&&t.constructor.name==="Uint8Array"}function M(t,...e){if(!ye(t))throw new Error("Uint8Array expected");if(e.length>0&&!e.includes(t.length))throw new Error("Uint8Array expected of length "+e+", got length="+t.length)}function we(t,e=!0){if(t.destroyed)throw new Error("Hash instance has been destroyed");if(e&&t.finished)throw new Error("Hash#digest() has already been called")}function Oe(t,e){M(t);let r=e.outputLen;if(t.length<r)throw new Error("digestInto() expects output buffer of length at least "+r)}function xe(t){if(typeof t!="boolean")throw new Error(`boolean expected, not ${t}`)}var G=t=>new Uint32Array(t.buffer,t.byteOffset,Math.floor(t.byteLength/4)),Se=t=>new DataView(t.buffer,t.byteOffset,t.byteLength),rt=new Uint8Array(new Uint32Array([287454020]).buffer)[0]===68;if(!rt)throw new Error("Non little-endian hardware is not supported");function nt(t){if(typeof t!="string")throw new Error("string expected");return new Uint8Array(new TextEncoder().encode(t))}function oe(t){if(typeof t=="string")t=nt(t);else if(ye(t))t=se(t);else throw new Error("Uint8Array expected, got "+typeof t);return t}function Ce(t,e){if(e==null||typeof e!="object")throw new Error("options must be defined");return Object.assign(t,e)}function Ke(t,e){if(t.length!==e.length)return!1;let r=0;for(let o=0;o<t.length;o++)r|=t[o]^e[o];return r===0}var ge=(t,e)=>{function r(o,...s){if(M(o),t.nonceLength!==void 0){let h=s[0];if(!h)throw new Error("nonce / iv required");t.varSizeNonce?M(h):M(h,t.nonceLength)}let n=t.tagLength;n&&s[1]!==void 0&&M(s[1]);let i=e(o,...s),c=(h,d)=>{if(d!==void 0){if(h!==2)throw new Error("cipher output not supported");M(d)}},f=!1;return{encrypt(h,d){if(f)throw new Error("cannot encrypt() twice with same key + nonce");return f=!0,M(h),c(i.encrypt.length,d),i.encrypt(h,d)},decrypt(h,d){if(M(h),n&&h.length<n)throw new Error("invalid ciphertext length: smaller than tagLength="+n);return c(i.decrypt.length,d),i.decrypt(h,d)}}}return Object.assign(r,t),r};function be(t,e,r=!0){if(e===void 0)return new Uint8Array(t);if(e.length!==t)throw new Error("invalid output length, expected "+t+", got: "+e.length);if(r&&!ot(e))throw new Error("invalid output, must be aligned");return e}function me(t,e,r,o){if(typeof t.setBigUint64=="function")return t.setBigUint64(e,r,o);let s=BigInt(32),n=BigInt(4294967295),i=Number(r>>s&n),c=Number(r&n),f=o?4:0,l=o?0:4;t.setUint32(e+f,i,o),t.setUint32(e+l,c,o)}function ot(t){return t.byteOffset%4===0}function se(t){return Uint8Array.from(t)}function W(...t){for(let e=0;e<t.length;e++)t[e].fill(0)}var Ne=t=>Uint8Array.from(t.split("").map(e=>e.charCodeAt(0))),st=Ne("expand 16-byte k"),it=Ne("expand 32-byte k"),ct=G(st),ft=G(it);function a(t,e){return t<<e|t>>>32-e}function Ee(t){return t.byteOffset%4===0}var ie=64,at=16,Re=2**32-1,Ie=new Uint32Array;function ht(t,e,r,o,s,n,i,c){let f=s.length,l=new Uint8Array(ie),h=G(l),d=Ee(s)&&Ee(n),p=d?G(s):Ie,x=d?G(n):Ie;for(let y=0;y<f;i++){if(t(e,r,o,h,i,c),i>=Re)throw new Error("arx: counter overflow");let g=Math.min(ie,f-y);if(d&&g===ie){let w=y/4;if(y%4!==0)throw new Error("arx: invalid block position");for(let b=0,m;b<at;b++)m=w+b,x[m]=p[m]^h[b];y+=ie;continue}for(let w=0,b;w<g;w++)b=y+w,n[b]=s[b]^l[w];y+=g}}function Ae(t,e){let{allowShortKeys:r,extendNonceFn:o,counterLength:s,counterRight:n,rounds:i}=Ce({allowShortKeys:!1,counterLength:8,counterRight:!1,rounds:20},e);if(typeof t!="function")throw new Error("core must be a function");return ne(s),ne(i),xe(n),xe(r),(c,f,l,h,d=0)=>{M(c),M(f),M(l);let p=l.length;if(h===void 0&&(h=new Uint8Array(p)),M(h),ne(d),d<0||d>=Re)throw new Error("arx: counter overflow");if(h.length<p)throw new Error(`arx: output (${h.length}) is shorter than data (${p})`);let x=[],y=c.length,g,w;if(y===32)x.push(g=se(c)),w=ft;else if(y===16&&r)g=new Uint8Array(32),g.set(c),g.set(c,16),w=ct,x.push(g);else throw new Error(`arx: invalid 32-byte key, got length=${y}`);Ee(f)||x.push(f=se(f));let b=G(g);if(o){if(f.length!==24)throw new Error("arx: extended nonce must be 24 bytes");o(w,b,G(f.subarray(0,16)),b),f=f.subarray(16)}let m=16-s;if(m!==f.length)throw new Error(`arx: nonce must be ${m} or 16 bytes`);if(m!==12){let $=new Uint8Array(12);$.set(f,n?0:12-f.length),f=$,x.push(f)}let N=G(f);return ht(t,w,b,N,l,h,d,i),W(...x),h}}var k=(t,e)=>t[e++]&255|(t[e++]&255)<<8,Te=class{constructor(e){this.blockLen=16,this.outputLen=16,this.buffer=new Uint8Array(16),this.r=new Uint16Array(10),this.h=new Uint16Array(10),this.pad=new Uint16Array(8),this.pos=0,this.finished=!1,e=oe(e),M(e,32);let r=k(e,0),o=k(e,2),s=k(e,4),n=k(e,6),i=k(e,8),c=k(e,10),f=k(e,12),l=k(e,14);this.r[0]=r&8191,this.r[1]=(r>>>13|o<<3)&8191,this.r[2]=(o>>>10|s<<6)&7939,this.r[3]=(s>>>7|n<<9)&8191,this.r[4]=(n>>>4|i<<12)&255,this.r[5]=i>>>1&8190,this.r[6]=(i>>>14|c<<2)&8191,this.r[7]=(c>>>11|f<<5)&8065,this.r[8]=(f>>>8|l<<8)&8191,this.r[9]=l>>>5&127;for(let h=0;h<8;h++)this.pad[h]=k(e,16+2*h)}process(e,r,o=!1){let s=o?0:2048,{h:n,r:i}=this,c=i[0],f=i[1],l=i[2],h=i[3],d=i[4],p=i[5],x=i[6],y=i[7],g=i[8],w=i[9],b=k(e,r+0),m=k(e,r+2),N=k(e,r+4),$=k(e,r+6),H=k(e,r+8),_=k(e,r+10),D=k(e,r+12),j=k(e,r+14),E=n[0]+(b&8191),A=n[1]+((b>>>13|m<<3)&8191),T=n[2]+((m>>>10|N<<6)&8191),U=n[3]+((N>>>7|$<<9)&8191),v=n[4]+(($>>>4|H<<12)&8191),L=n[5]+(H>>>1&8191),B=n[6]+((H>>>14|_<<2)&8191),O=n[7]+((_>>>11|D<<5)&8191),S=n[8]+((D>>>8|j<<8)&8191),C=n[9]+(j>>>5|s),u=0,I=u+E*c+A*(5*w)+T*(5*g)+U*(5*y)+v*(5*x);u=I>>>13,I&=8191,I+=L*(5*p)+B*(5*d)+O*(5*h)+S*(5*l)+C*(5*f),u+=I>>>13,I&=8191;let R=u+E*f+A*c+T*(5*w)+U*(5*g)+v*(5*y);u=R>>>13,R&=8191,R+=L*(5*x)+B*(5*p)+O*(5*d)+S*(5*h)+C*(5*l),u+=R>>>13,R&=8191;let K=u+E*l+A*f+T*c+U*(5*w)+v*(5*g);u=K>>>13,K&=8191,K+=L*(5*y)+B*(5*x)+O*(5*p)+S*(5*d)+C*(5*h),u+=K>>>13,K&=8191;let F=u+E*h+A*l+T*f+U*c+v*(5*w);u=F>>>13,F&=8191,F+=L*(5*g)+B*(5*y)+O*(5*x)+S*(5*p)+C*(5*d),u+=F>>>13,F&=8191;let J=u+E*d+A*h+T*l+U*f+v*c;u=J>>>13,J&=8191,J+=L*(5*w)+B*(5*g)+O*(5*y)+S*(5*x)+C*(5*p),u+=J>>>13,J&=8191;let Y=u+E*p+A*d+T*h+U*l+v*f;u=Y>>>13,Y&=8191,Y+=L*c+B*(5*w)+O*(5*g)+S*(5*y)+C*(5*x),u+=Y>>>13,Y&=8191;let q=u+E*x+A*p+T*d+U*h+v*l;u=q>>>13,q&=8191,q+=L*f+B*c+O*(5*w)+S*(5*g)+C*(5*y),u+=q>>>13,q&=8191;let X=u+E*y+A*x+T*p+U*d+v*h;u=X>>>13,X&=8191,X+=L*l+B*f+O*c+S*(5*w)+C*(5*g),u+=X>>>13,X&=8191;let Q=u+E*g+A*y+T*x+U*p+v*d;u=Q>>>13,Q&=8191,Q+=L*h+B*l+O*f+S*c+C*(5*w),u+=Q>>>13,Q&=8191;let Z=u+E*w+A*g+T*y+U*x+v*p;u=Z>>>13,Z&=8191,Z+=L*d+B*h+O*l+S*f+C*c,u+=Z>>>13,Z&=8191,u=(u<<2)+u|0,u=u+I|0,I=u&8191,u=u>>>13,R+=u,n[0]=I,n[1]=R,n[2]=K,n[3]=F,n[4]=J,n[5]=Y,n[6]=q,n[7]=X,n[8]=Q,n[9]=Z}finalize(){let{h:e,pad:r}=this,o=new Uint16Array(10),s=e[1]>>>13;e[1]&=8191;for(let c=2;c<10;c++)e[c]+=s,s=e[c]>>>13,e[c]&=8191;e[0]+=s*5,s=e[0]>>>13,e[0]&=8191,e[1]+=s,s=e[1]>>>13,e[1]&=8191,e[2]+=s,o[0]=e[0]+5,s=o[0]>>>13,o[0]&=8191;for(let c=1;c<10;c++)o[c]=e[c]+s,s=o[c]>>>13,o[c]&=8191;o[9]-=8192;let n=(s^1)-1;for(let c=0;c<10;c++)o[c]&=n;n=~n;for(let c=0;c<10;c++)e[c]=e[c]&n|o[c];e[0]=(e[0]|e[1]<<13)&65535,e[1]=(e[1]>>>3|e[2]<<10)&65535,e[2]=(e[2]>>>6|e[3]<<7)&65535,e[3]=(e[3]>>>9|e[4]<<4)&65535,e[4]=(e[4]>>>12|e[5]<<1|e[6]<<14)&65535,e[5]=(e[6]>>>2|e[7]<<11)&65535,e[6]=(e[7]>>>5|e[8]<<8)&65535,e[7]=(e[8]>>>8|e[9]<<5)&65535;let i=e[0]+r[0];e[0]=i&65535;for(let c=1;c<8;c++)i=(e[c]+r[c]|0)+(i>>>16)|0,e[c]=i&65535;W(o)}update(e){we(this);let{buffer:r,blockLen:o}=this;e=oe(e);let s=e.length;for(let n=0;n<s;){let i=Math.min(o-this.pos,s-n);if(i===o){for(;o<=s-n;n+=o)this.process(e,n);continue}r.set(e.subarray(n,n+i),this.pos),this.pos+=i,n+=i,this.pos===o&&(this.process(r,0,!1),this.pos=0)}return this}destroy(){W(this.h,this.r,this.buffer,this.pad)}digestInto(e){we(this),Oe(e,this),this.finished=!0;let{buffer:r,h:o}=this,{pos:s}=this;if(s){for(r[s++]=1;s<16;s++)r[s]=0;this.process(r,0,!0)}this.finalize();let n=0;for(let i=0;i<8;i++)e[n++]=o[i]>>>0,e[n++]=o[i]>>>8;return e}digest(){let{buffer:e,outputLen:r}=this;this.digestInto(e);let o=e.slice(0,r);return this.destroy(),o}};function lt(t){let e=(o,s)=>t(s).update(oe(o)).digest(),r=t(new Uint8Array(32));return e.outputLen=r.outputLen,e.blockLen=r.blockLen,e.create=o=>t(o),e}var $e=lt(t=>new Te(t));function De(t,e,r,o,s,n=20){let i=t[0],c=t[1],f=t[2],l=t[3],h=e[0],d=e[1],p=e[2],x=e[3],y=e[4],g=e[5],w=e[6],b=e[7],m=s,N=r[0],$=r[1],H=r[2],_=i,D=c,j=f,E=l,A=h,T=d,U=p,v=x,L=y,B=g,O=w,S=b,C=m,u=N,I=$,R=H;for(let F=0;F<n;F+=2)_=_+A|0,C=a(C^_,16),L=L+C|0,A=a(A^L,12),_=_+A|0,C=a(C^_,8),L=L+C|0,A=a(A^L,7),D=D+T|0,u=a(u^D,16),B=B+u|0,T=a(T^B,12),D=D+T|0,u=a(u^D,8),B=B+u|0,T=a(T^B,7),j=j+U|0,I=a(I^j,16),O=O+I|0,U=a(U^O,12),j=j+U|0,I=a(I^j,8),O=O+I|0,U=a(U^O,7),E=E+v|0,R=a(R^E,16),S=S+R|0,v=a(v^S,12),E=E+v|0,R=a(R^E,8),S=S+R|0,v=a(v^S,7),_=_+T|0,R=a(R^_,16),O=O+R|0,T=a(T^O,12),_=_+T|0,R=a(R^_,8),O=O+R|0,T=a(T^O,7),D=D+U|0,C=a(C^D,16),S=S+C|0,U=a(U^S,12),D=D+U|0,C=a(C^D,8),S=S+C|0,U=a(U^S,7),j=j+v|0,u=a(u^j,16),L=L+u|0,v=a(v^L,12),j=j+v|0,u=a(u^j,8),L=L+u|0,v=a(v^L,7),E=E+A|0,I=a(I^E,16),B=B+I|0,A=a(A^B,12),E=E+A|0,I=a(I^E,8),B=B+I|0,A=a(A^B,7);let K=0;o[K++]=i+_|0,o[K++]=c+D|0,o[K++]=f+j|0,o[K++]=l+E|0,o[K++]=h+A|0,o[K++]=d+T|0,o[K++]=p+U|0,o[K++]=x+v|0,o[K++]=y+L|0,o[K++]=g+B|0,o[K++]=w+O|0,o[K++]=b+S|0,o[K++]=m+C|0,o[K++]=N+u|0,o[K++]=$+I|0,o[K++]=H+R|0}function ut(t,e,r,o){let s=t[0],n=t[1],i=t[2],c=t[3],f=e[0],l=e[1],h=e[2],d=e[3],p=e[4],x=e[5],y=e[6],g=e[7],w=r[0],b=r[1],m=r[2],N=r[3];for(let H=0;H<20;H+=2)s=s+f|0,w=a(w^s,16),p=p+w|0,f=a(f^p,12),s=s+f|0,w=a(w^s,8),p=p+w|0,f=a(f^p,7),n=n+l|0,b=a(b^n,16),x=x+b|0,l=a(l^x,12),n=n+l|0,b=a(b^n,8),x=x+b|0,l=a(l^x,7),i=i+h|0,m=a(m^i,16),y=y+m|0,h=a(h^y,12),i=i+h|0,m=a(m^i,8),y=y+m|0,h=a(h^y,7),c=c+d|0,N=a(N^c,16),g=g+N|0,d=a(d^g,12),c=c+d|0,N=a(N^c,8),g=g+N|0,d=a(d^g,7),s=s+l|0,N=a(N^s,16),y=y+N|0,l=a(l^y,12),s=s+l|0,N=a(N^s,8),y=y+N|0,l=a(l^y,7),n=n+h|0,w=a(w^n,16),g=g+w|0,h=a(h^g,12),n=n+h|0,w=a(w^n,8),g=g+w|0,h=a(h^g,7),i=i+d|0,b=a(b^i,16),p=p+b|0,d=a(d^p,12),i=i+d|0,b=a(b^i,8),p=p+b|0,d=a(d^p,7),c=c+f|0,m=a(m^c,16),x=x+m|0,f=a(f^x,12),c=c+f|0,m=a(m^c,8),x=x+m|0,f=a(f^x,7);let $=0;o[$++]=s,o[$++]=n,o[$++]=i,o[$++]=c,o[$++]=w,o[$++]=b,o[$++]=m,o[$++]=N}var dt=Ae(De,{counterRight:!1,counterLength:4,allowShortKeys:!1}),pt=Ae(De,{counterRight:!1,counterLength:8,extendNonceFn:ut,allowShortKeys:!1});var yt=new Uint8Array(16),ke=(t,e)=>{t.update(e);let r=e.length%16;r&&t.update(yt.subarray(r))},wt=new Uint8Array(32);function _e(t,e,r,o,s){let n=t(e,r,wt),i=$e.create(n);s&&ke(i,s),ke(i,o);let c=new Uint8Array(16),f=Se(c);me(f,0,BigInt(s?s.length:0),!0),me(f,8,BigInt(o.length),!0),i.update(c);let l=i.digest();return W(n,c),l}var je=t=>(e,r,o)=>({encrypt(n,i){let c=n.length;i=be(c+16,i,!1),i.set(n);let f=i.subarray(0,-16);t(e,r,f,f,1);let l=_e(t,e,r,f,o);return i.set(l,c),W(l),i},decrypt(n,i){i=be(n.length-16,i,!1);let c=n.subarray(0,-16),f=n.subarray(-16),l=_e(t,e,r,c,o);if(!Ke(f,l))throw new Error("invalid tag");return i.set(n.subarray(0,-16)),t(e,r,i,i,1),W(l),i}}),Pt=ge({blockSize:64,nonceLength:12,tagLength:16},je(dt)),Ue=ge({blockSize:64,nonceLength:24,tagLength:16},je(pt));function xt(t){return t instanceof Uint8Array||ArrayBuffer.isView(t)&&t.constructor.name==="Uint8Array"}function Pe(t,e){return Array.isArray(e)?e.length===0?!0:t?e.every(r=>typeof r=="string"):e.every(r=>Number.isSafeInteger(r)):!1}function ce(t,e){if(typeof e!="string")throw new Error(`${t}: string expected`);return!0}function Be(t){if(!Number.isSafeInteger(t))throw new Error(`invalid integer: ${t}`)}function Le(t){if(!Array.isArray(t))throw new Error("array expected")}function fe(t,e){if(!Pe(!0,e))throw new Error(`${t}: array of strings expected`)}function gt(t,e){if(!Pe(!1,e))throw new Error(`${t}: array of numbers expected`)}function bt(...t){let e=n=>n,r=(n,i)=>c=>n(i(c)),o=t.map(n=>n.encode).reduceRight(r,e),s=t.map(n=>n.decode).reduce(r,e);return{encode:o,decode:s}}function mt(t){let e=typeof t=="string"?t.split(""):t,r=e.length;fe("alphabet",e);let o=new Map(e.map((s,n)=>[s,n]));return{encode:s=>(Le(s),s.map(n=>{if(!Number.isSafeInteger(n)||n<0||n>=r)throw new Error(`alphabet.encode: digit index outside alphabet "${n}". Allowed: ${t}`);return e[n]})),decode:s=>(Le(s),s.map(n=>{ce("alphabet.decode",n);let i=o.get(n);if(i===void 0)throw new Error(`Unknown letter: "${n}". Allowed: ${t}`);return i}))}}function Et(t=""){return ce("join",t),{encode:e=>(fe("join.decode",e),e.join(t)),decode:e=>(ce("join.decode",e),e.split(t))}}function At(t,e="="){return Be(t),ce("padding",e),{encode(r){for(fe("padding.encode",r);r.length*t%8;)r.push(e);return r},decode(r){fe("padding.decode",r);let o=r.length;if(o*t%8)throw new Error("padding: invalid, string should have whole number of bytes");for(;o>0&&r[o-1]===e;o--)if((o-1)*t%8===0)throw new Error("padding: invalid, string has too much padding");return r.slice(0,o)}}}var Ve=(t,e)=>e===0?t:Ve(e,t%e),ae=(t,e)=>t+(e-Ve(t,e)),ve=(()=>{let t=[];for(let e=0;e<40;e++)t.push(2**e);return t})();function Me(t,e,r,o){if(Le(t),e<=0||e>32)throw new Error(`convertRadix2: wrong from=${e}`);if(r<=0||r>32)throw new Error(`convertRadix2: wrong to=${r}`);if(ae(e,r)>32)throw new Error(`convertRadix2: carry overflow from=${e} to=${r} carryBits=${ae(e,r)}`);let s=0,n=0,i=ve[e],c=ve[r]-1,f=[];for(let l of t){if(Be(l),l>=i)throw new Error(`convertRadix2: invalid data word=${l} from=${e}`);if(s=s<<e|l,n+e>32)throw new Error(`convertRadix2: carry overflow pos=${n} from=${e}`);for(n+=e;n>=r;n-=r)f.push((s>>n-r&c)>>>0);let h=ve[n];if(h===void 0)throw new Error("invalid carry");s&=h-1}if(s=s<<r-n&c,!o&&n>=e)throw new Error("Excess padding");if(!o&&s>0)throw new Error(`Non-zero padding: ${s}`);return o&&n>0&&f.push(s>>>0),f}function Tt(t,e=!1){if(Be(t),t<=0||t>32)throw new Error("radix2: bits should be in (0..32]");if(ae(8,t)>32||ae(t,8)>32)throw new Error("radix2: carry overflow");return{encode:r=>{if(!xt(r))throw new Error("radix2.encode input should be Uint8Array");return Me(Array.from(r),8,t,!e)},decode:r=>(gt("radix2.decode",r),Uint8Array.from(Me(r,t,8,e)))}}var re=bt(Tt(6),mt("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"),At(6),Et(""));var z=class extends Error{code;constructor(e,r){super(e),this.name="TectoError",this.code=r,Object.setPrototypeOf(this,new.target.prototype)}},ee=class extends z{expiredAt;constructor(e){super("Token has expired","TECTO_TOKEN_EXPIRED"),this.name="TokenExpiredError",this.expiredAt=e}},V=class extends z{constructor(){super("Invalid token","TECTO_INVALID_TOKEN"),this.name="InvalidSignatureError"}},P=class extends z{constructor(e){super(e,"TECTO_KEY_ERROR"),this.name="KeyError"}},te=class extends z{activeAt;constructor(e){super("Token is not yet active","TECTO_TOKEN_NOT_ACTIVE"),this.name="TokenNotActiveError",this.activeAt=e}};var ze="tecto",He="v1",Fe=24,Ut=5;function Ge(t){let e=/^(\d+)\s*(s|m|h|d)$/i.exec(t);if(!e)throw new z(`Invalid duration format: "${t}". Expected format: <number><unit> where unit is s, m, h, or d.`,"TECTO_INVALID_DURATION");let r=Number.parseInt(e[1],10),o=e[2].toLowerCase(),n={s:1,m:60,h:3600,d:86400}[o];if(n===void 0)throw new z(`Unknown duration unit: "${o}"`,"TECTO_INVALID_DURATION");return r*n}function vt(){let t=new Uint8Array(16);return crypto.getRandomValues(t),Array.from(t).map(e=>e.toString(16).padStart(2,"0")).join("")}var he=class{keyStore;constructor(e){this.keyStore=e}encrypt(e,r){let o=this.keyStore.getCurrentKeyId(),s=this.keyStore.getKey(o),n=Math.floor(Date.now()/1e3),i={...e,iat:n,jti:r?.jti??vt()};r?.expiresIn&&(i.exp=n+Ge(r.expiresIn)),r?.notBefore&&(i.nbf=n+Ge(r.notBefore)),r?.issuer&&(i.iss=r.issuer),r?.audience&&(i.aud=r.audience);let c=new TextEncoder().encode(JSON.stringify(i)),f=new Uint8Array(Fe);crypto.getRandomValues(f);let h=Ue(s,f).encrypt(c),d=re.encode(f),p=re.encode(h);return`${ze}.${He}.${o}.${d}.${p}`}decrypt(e){let r;try{r=e.split(".")}catch{throw new V}if(r.length!==Ut)throw new V;let[o,s,n,i,c]=r;if(o!==ze||s!==He)throw new V;let f;try{f=this.keyStore.getKey(n)}catch{throw new V}let l,h;try{l=re.decode(i),h=re.decode(c)}catch{throw new V}if(l.byteLength!==Fe)throw new V;let d;try{d=Ue(f,l).decrypt(h)}catch{throw new V}let p;try{let y=new TextDecoder().decode(d);p=JSON.parse(y)}catch{throw new V}let x=Math.floor(Date.now()/1e3);if(typeof p.exp=="number"&&p.exp<=x)throw new ee(new Date(p.exp*1e3));if(typeof p.nbf=="number"&&p.nbf>x)throw new te(new Date(p.nbf*1e3));return p}};var le=32,We=8;function Je(){let t=new Uint8Array(le);return crypto.getRandomValues(t),t}function Ye(t,e){if(t.byteLength!==e.byteLength)return!1;let r=0;for(let o=0;o<t.byteLength;o++)r|=(t[o]??0)^(e[o]??0);return r===0}function ue(t){if(!(t instanceof Uint8Array))throw new P("Key must be a Uint8Array. String keys are forbidden to prevent internalization attacks.");if(t.byteLength!==le)throw new P(`Key must be exactly ${le} bytes (${le*8}-bit). Received ${t.byteLength} bytes.`);let e=!0;for(let n=0;n<t.byteLength;n++)if((t[n]??0)!==0){e=!1;break}if(e)throw new P("Key must not be all zeros. Use generateSecureKey() for safe key generation.");let r=t[0]??0,o=!0;for(let n=1;n<t.byteLength;n++)if((t[n]??0)!==r){o=!1;break}if(o)throw new P("Key must not be a repeating single byte. Use generateSecureKey() for safe key generation.");let s=new Set;for(let n=0;n<t.byteLength;n++)s.add(t[n]??0);if(s.size<We)throw new P(`Key has insufficient entropy: only ${s.size} unique byte values. A minimum of ${We} is required. Use generateSecureKey() for safe key generation.`)}var de=class{keys=new Map;currentKeyId=null;addKey(e,r){ue(r);let o=new Uint8Array(r.byteLength);o.set(r),this.keys.set(e,o),this.currentKeyId===null&&(this.currentKeyId=e)}getKey(e){let r=this.keys.get(e);if(!r)throw new P(`Key not found: "${e}"`);return r}rotate(e,r){this.addKey(e,r),this.currentKeyId=e}removeKey(e){let r=this.keys.get(e);if(!r)throw new P(`Key not found: "${e}"`);if(this.currentKeyId===e)throw new P("Cannot remove the current active key. Rotate to a new key first.");r.fill(0),this.keys.delete(e)}getCurrentKeyId(){if(this.currentKeyId===null)throw new P("No keys in the store. Add a key with addKey() first.");return this.currentKeyId}get size(){return this.keys.size}};0&&(module.exports={InvalidSignatureError,KeyError,MemoryKeyStore,TectoCoder,TectoError,TokenExpiredError,TokenNotActiveError,assertEntropy,constantTimeCompare,generateSecureKey});
|
|
2
|
+
/*! Bundled license information:
|
|
3
|
+
|
|
4
|
+
@noble/ciphers/esm/utils.js:
|
|
5
|
+
(*! noble-ciphers - MIT License (c) 2023 Paul Miller (paulmillr.com) *)
|
|
6
|
+
|
|
7
|
+
@scure/base/lib/esm/index.js:
|
|
8
|
+
(*! scure-base - MIT License (c) 2022 Paul Miller (paulmillr.com) *)
|
|
9
|
+
*/
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tecto",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Transport Encrypted Compact Token Object — an opaque, XChaCha20-Poly1305 encrypted token protocol",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup",
|
|
19
|
+
"test": "bun test"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@biomejs/biome": "2.3.14",
|
|
23
|
+
"@noble/ciphers": "1.2.1",
|
|
24
|
+
"@scure/base": "1.2.4",
|
|
25
|
+
"@types/bun": "1.2.4",
|
|
26
|
+
"tsup": "8.4.0",
|
|
27
|
+
"typescript": "5.9.3"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"tecto",
|
|
31
|
+
"token",
|
|
32
|
+
"encryption",
|
|
33
|
+
"xchacha20",
|
|
34
|
+
"chacha20-poly1305",
|
|
35
|
+
"aead",
|
|
36
|
+
"opaque-token",
|
|
37
|
+
"cryptography",
|
|
38
|
+
"security",
|
|
39
|
+
"auth",
|
|
40
|
+
"session",
|
|
41
|
+
"keystore"
|
|
42
|
+
],
|
|
43
|
+
"license": "MIT"
|
|
44
|
+
}
|