@arikajs/encryption 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  `@arikajs/encryption` provides secure, application-level encryption for the ArikaJS framework.
4
4
 
5
- It is responsible for encrypting and decrypting sensitive data such as sessions, cookies, signed payloads, and internal framework values — similar in spirit to Laravel’s `Illuminate\Encryption`.
5
+ It is responsible for encrypting and decrypting sensitive data such as sessions, cookies, signed payloads, and internal framework values — providing a clean, secure API.
6
6
 
7
7
  This package is framework-agnostic at runtime, but designed to integrate seamlessly with `@arikajs/foundation` via service providers.
8
8
 
@@ -11,9 +11,10 @@ This package is framework-agnostic at runtime, but designed to integrate seamles
11
11
  ## ✨ Features
12
12
 
13
13
  - 🔐 **AES-256-GCM encryption** (modern & secure)
14
- - 🔑 **Single application key** (`APP_KEY`)
14
+ - 🔑 **Multiple application keys** (support for key rotation)
15
15
  - 🧾 **Authenticated encryption** (tamper detection)
16
16
  - 🔄 **Encrypt / decrypt strings & JSON**
17
+ - ✍️ **Signed-only payloads** (non-encrypted but authenticated)
17
18
  - 🧠 **Stateless design** (safe for queues & workers)
18
19
  - 🧩 **Framework service friendly**
19
20
  - 🟦 **TypeScript-first**
@@ -75,6 +76,72 @@ const data = encrypter.decrypt(token);
75
76
 
76
77
  Internally, objects are JSON-serialized automatically.
77
78
 
79
+ ### 🗜️ Optional Compression
80
+
81
+ For large data (like long session strings or queue jobs), you can compress the payload before encryption to save space.
82
+
83
+ ```ts
84
+ const encrypted = encrypter.encrypt(veryLargeObject, { compress: true });
85
+ ```
86
+
87
+ ### ⏳ Payload Expiration (TTL)
88
+
89
+ You can create encrypted data that is only valid for a certain number of seconds.
90
+
91
+ ```ts
92
+ // Valid for 5 minutes (300 seconds)
93
+ const token = encrypter.encrypt('secret', { ttl: 300 });
94
+
95
+ // After 5 minutes, decrypt() will throw a DecryptionException
96
+ const data = encrypter.decrypt(token);
97
+ ```
98
+
99
+ ### 🔐 Contextual Encryption (AAD)
100
+
101
+ Ensure the encrypted data is only used in the correct context (e.g., tying a token to a specific user). This uses **Additional Authenticated Data** to prevent payload reuse.
102
+
103
+ ```ts
104
+ const token = encrypter.encrypt('sensitive', { context: 'user-id-123' });
105
+
106
+ // This will succeed
107
+ encrypter.decrypt(token, { context: 'user-id-123' });
108
+
109
+ // This will throw even with the correct key!
110
+ encrypter.decrypt(token, { context: 'user-id-456' });
111
+ ```
112
+
113
+ ---
114
+
115
+ ### ✍️ Signed Payloads (Non-encrypted)
116
+
117
+ Sometimes you want to ensure data isn't tampered with but don't need to keep it secret (e.g., verification tokens).
118
+
119
+ ```ts
120
+ const signed = encrypter.sign({ email: 'user@example.com' });
121
+
122
+ // "eyJ2YWx1ZSI6InsiZW1haWwiOiJ1c2VyQGV4YW1..."
123
+
124
+ const data = encrypter.verify(signed);
125
+ // { email: 'user@example.com' }
126
+ ```
127
+
128
+ ---
129
+
130
+ ## 🔄 Key Rotation
131
+
132
+ You can pass an array of keys to the `Encrypter`. The first key will be used for encryption/signing, but all keys will be tried during decryption/verification.
133
+
134
+ ```ts
135
+ const encrypter = new Encrypter([
136
+ 'base64:new-primary-key...',
137
+ 'base64:old-key-1...',
138
+ 'base64:old-key-2...'
139
+ ]);
140
+
141
+ // Decrypts data created with any of the 3 keys
142
+ const data = encrypter.decrypt(oldPayload);
143
+ ```
144
+
78
145
  ---
79
146
 
80
147
  ## 🧠 Integration with ArikaJS
@@ -85,13 +152,13 @@ Internally, objects are JSON-serialized automatically.
85
152
  import { Encrypter } from '@arikajs/encryption';
86
153
 
87
154
  this.app.singleton('encrypter', () => {
88
- const key = config('app.key');
155
+ const keys = config('app.keys'); // Array of keys from config
89
156
 
90
- if (!key) {
157
+ if (!keys || keys.length === 0) {
91
158
  throw new Error('APP_KEY is not defined.');
92
159
  }
93
160
 
94
- return new Encrypter(key);
161
+ return new Encrypter(keys);
95
162
  });
96
163
  ```
97
164
 
@@ -107,13 +174,15 @@ const value = encrypter.encrypt('hello');
107
174
 
108
175
  ## 🔒 Security Guarantees
109
176
 
110
- - Uses **AES-256-GCM**
111
- - Every payload includes:
177
+ - Uses **AES-256-GCM** for encryption
178
+ - Uses **HMAC-SHA256** for signing
179
+ - Every encrypted payload includes:
112
180
  - Random IV (Initialization Vector)
113
181
  - Authentication tag
182
+ - Version identifier
114
183
  - Any tampering → automatic decryption failure
115
184
  - No weak or legacy algorithms
116
- - If data is modified, `decrypt()` will throw.
185
+ - Uses timing-safe comparisons for signatures
117
186
 
118
187
  ---
119
188
 
@@ -124,7 +193,7 @@ This package is a core dependency for:
124
193
  | Package | Usage |
125
194
  | :--- | :--- |
126
195
  | `@arikajs/session` | Encrypted sessions |
127
- | `@arikajs/http` | Encrypted cookies |
196
+ | `@arikajs/http` | Encrypted cookies / Signed URLs |
128
197
  | `@arikajs/queue` | Secure job payloads |
129
198
  | `@arikajs/auth` | Token encryption |
130
199
  | `@arikajs/mail` | Signed mail data |
@@ -133,51 +202,42 @@ This package is a core dependency for:
133
202
 
134
203
  ## 🧠 API Reference
135
204
 
136
- ### `new Encrypter(key: string)`
137
- Creates a new encrypter instance.
138
-
139
- ### `encrypt(value: string | object): string`
140
- Encrypts a value and returns a string payload.
205
+ ### `new Encrypter(key: string | string[])`
206
+ Creates a new encrypter instance. Accepts a single key or an array for rotation.
141
207
 
142
- ### `decrypt(payload: string): any`
143
- Decrypts a payload and returns the original value.
208
+ ### `encrypt(value: any, options?: object): string`
209
+ Encrypts a value. Options include:
210
+ - `serialize`: (default: true)
211
+ - `compress`: (default: false) uses zlib deflation
212
+ - `ttl`: seconds until expiration
213
+ - `context`: Additional Authenticated Data (AAD)
144
214
 
145
- Throws if:
146
- - Payload is invalid
147
- - Payload is tampered
148
- - Key is incorrect
215
+ ### `decrypt(payload: string, options?: object): any`
216
+ Decrypts a payload. Options include:
217
+ - `unserialize`: (default: true)
218
+ - `context`: Must match the context used during encryption
149
219
 
150
220
  ---
151
221
 
152
222
  ## 🏗 Architecture
153
223
 
154
- ```
224
+ ```text
155
225
  encryption/
156
226
  ├── src/
157
- │ ├── Encrypter.ts
158
- │ ├── Contracts/
227
+ │ ├── Contracts
159
228
  │ │ └── Encrypter.ts
160
- │ ├── Exceptions/
229
+ │ ├── Exceptions
161
230
  │ │ └── DecryptionException.ts
231
+ │ ├── Encrypter.ts
162
232
  │ └── index.ts
163
233
  ├── tests/
164
234
  ├── package.json
165
235
  ├── tsconfig.json
166
- ├── README.md
167
- └── LICENSE
236
+ └── README.md
168
237
  ```
169
238
 
170
239
  ---
171
240
 
172
- ## 🚧 Planned (v1.x+)
173
-
174
- - 🔄 Key rotation support
175
- - 🧪 Encrypted payload versioning
176
- - 🔑 Multiple key support (fallback decryption)
177
- - 🧷 Signed-only (non-encrypted) payloads
178
-
179
- ---
180
-
181
241
  ## 🧭 Philosophy
182
242
 
183
243
  > "Encryption should be invisible, mandatory, and impossible to misuse."
@@ -2,9 +2,25 @@ export interface Encrypter {
2
2
  /**
3
3
  * Encrypt the given value.
4
4
  */
5
- encrypt(value: any): string;
5
+ encrypt(value: any, options?: {
6
+ serialize?: boolean;
7
+ compress?: boolean;
8
+ ttl?: number;
9
+ context?: string;
10
+ }): string;
6
11
  /**
7
12
  * Decrypt the given value.
8
13
  */
9
- decrypt(payload: string): any;
14
+ decrypt(payload: string, options?: {
15
+ unserialize?: boolean;
16
+ context?: string;
17
+ }): any;
18
+ /**
19
+ * Sign the given value.
20
+ */
21
+ sign(value: any): string;
22
+ /**
23
+ * Verify the given signed payload.
24
+ */
25
+ verify(payload: string): any;
10
26
  }
@@ -1,8 +1,8 @@
1
1
  import { Encrypter as EncrypterContract } from './Contracts/Encrypter';
2
2
  export declare class Encrypter implements EncrypterContract {
3
- private readonly key;
3
+ private readonly keys;
4
4
  private readonly cipher;
5
- constructor(key: string);
5
+ constructor(key: string | string[]);
6
6
  /**
7
7
  * Parse the encryption key.
8
8
  */
@@ -10,11 +10,31 @@ export declare class Encrypter implements EncrypterContract {
10
10
  /**
11
11
  * Encrypt the given value.
12
12
  */
13
- encrypt(value: any): string;
13
+ encrypt(value: any, options?: {
14
+ serialize?: boolean;
15
+ compress?: boolean;
16
+ ttl?: number;
17
+ context?: string;
18
+ }): string;
14
19
  /**
15
20
  * Decrypt the given value.
16
21
  */
17
- decrypt(payload: string): any;
22
+ decrypt(payload: string, options?: {
23
+ unserialize?: boolean;
24
+ context?: string;
25
+ }): any;
26
+ /**
27
+ * Sign the given value (non-encrypted but authenticated).
28
+ */
29
+ sign(value: any): string;
30
+ /**
31
+ * Verify the given signed payload.
32
+ */
33
+ verify(payload: string): any;
34
+ /**
35
+ * Create a hash (HMAC) of the given value.
36
+ */
37
+ protected hash(value: string): string;
18
38
  /**
19
39
  * Get the JSON payload from the given string.
20
40
  */
package/dist/Encrypter.js CHANGED
@@ -2,14 +2,21 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Encrypter = void 0;
4
4
  const crypto_1 = require("crypto");
5
+ const zlib_1 = require("zlib");
5
6
  const DecryptionException_1 = require("./Exceptions/DecryptionException");
6
7
  class Encrypter {
7
- key;
8
+ keys;
8
9
  cipher = 'aes-256-gcm';
9
10
  constructor(key) {
10
- this.key = this.parseKey(key);
11
- if (this.key.length !== 32) {
12
- throw new Error('The encryption key must be 32 bytes.');
11
+ const keys = Array.isArray(key) ? key : [key];
12
+ this.keys = keys.map(k => this.parseKey(k));
13
+ if (this.keys.length === 0) {
14
+ throw new Error('At least one encryption key must be provided.');
15
+ }
16
+ for (const k of this.keys) {
17
+ if (k.length !== 32) {
18
+ throw new Error('Each encryption key must be 32 bytes.');
19
+ }
13
20
  }
14
21
  }
15
22
  /**
@@ -24,38 +31,124 @@ class Encrypter {
24
31
  /**
25
32
  * Encrypt the given value.
26
33
  */
27
- encrypt(value) {
34
+ encrypt(value, options = {}) {
28
35
  const iv = (0, crypto_1.randomBytes)(16);
29
- const cipher = (0, crypto_1.createCipheriv)(this.cipher, this.key, iv);
30
- const jsonValue = JSON.stringify(value);
31
- let encrypted = cipher.update(jsonValue, 'utf8', 'base64');
32
- encrypted += cipher.final('base64');
36
+ const cipher = (0, crypto_1.createCipheriv)(this.cipher, this.keys[0], iv);
37
+ if (options.context) {
38
+ cipher.setAAD(Buffer.from(options.context));
39
+ }
40
+ const serialize = options.serialize !== false;
41
+ let data;
42
+ if (serialize) {
43
+ data = JSON.stringify(value);
44
+ }
45
+ else {
46
+ data = Buffer.isBuffer(value) ? value : String(value);
47
+ }
48
+ if (options.compress === true) {
49
+ data = (0, zlib_1.deflateSync)(Buffer.from(data));
50
+ }
51
+ let encrypted = cipher.update(data);
52
+ encrypted = Buffer.concat([encrypted, cipher.final()]);
33
53
  const tag = cipher.getAuthTag();
34
54
  const payload = {
35
55
  iv: iv.toString('base64'),
36
- value: encrypted,
56
+ value: encrypted.toString('base64'),
37
57
  tag: tag.toString('base64'),
58
+ v: 1,
38
59
  };
60
+ if (options.compress)
61
+ payload.c = true;
62
+ if (options.ttl) {
63
+ payload.exp = Date.now() + (options.ttl * 1000);
64
+ }
65
+ if (options.context)
66
+ payload.aad = true;
39
67
  return Buffer.from(JSON.stringify(payload)).toString('base64');
40
68
  }
41
69
  /**
42
70
  * Decrypt the given value.
43
71
  */
44
- decrypt(payload) {
72
+ decrypt(payload, options = {}) {
45
73
  const jsonPayload = this.getJsonPayload(payload);
74
+ // Check for TTL expiration
75
+ if (jsonPayload.exp && Date.now() > jsonPayload.exp) {
76
+ throw new DecryptionException_1.DecryptionException('The payload has expired.');
77
+ }
78
+ // Verify context requirements
79
+ if (jsonPayload.aad && !options.context) {
80
+ throw new DecryptionException_1.DecryptionException('The payload requires a context for decryption.');
81
+ }
46
82
  const iv = Buffer.from(jsonPayload.iv, 'base64');
47
83
  const tag = Buffer.from(jsonPayload.tag, 'base64');
48
84
  const value = jsonPayload.value;
85
+ const compressed = jsonPayload.c === true;
86
+ // Try decrypting with all available keys (support rotation)
87
+ for (const key of this.keys) {
88
+ try {
89
+ const decipher = (0, crypto_1.createDecipheriv)(this.cipher, key, iv);
90
+ if (options.context) {
91
+ decipher.setAAD(Buffer.from(options.context));
92
+ }
93
+ decipher.setAuthTag(tag);
94
+ let decrypted = decipher.update(value, 'base64');
95
+ decrypted = Buffer.concat([decrypted, decipher.final()]);
96
+ let result = decrypted;
97
+ if (compressed) {
98
+ result = (0, zlib_1.inflateSync)(result);
99
+ }
100
+ const finalData = result.toString('utf8');
101
+ return options.unserialize === false ? finalData : JSON.parse(finalData);
102
+ }
103
+ catch (error) {
104
+ // Try next key
105
+ continue;
106
+ }
107
+ }
108
+ throw new DecryptionException_1.DecryptionException();
109
+ }
110
+ /**
111
+ * Sign the given value (non-encrypted but authenticated).
112
+ */
113
+ sign(value) {
114
+ const jsonValue = JSON.stringify(value);
115
+ const signature = this.hash(jsonValue);
116
+ return Buffer.from(JSON.stringify({
117
+ value: jsonValue,
118
+ signature: signature
119
+ })).toString('base64');
120
+ }
121
+ /**
122
+ * Verify the given signed payload.
123
+ */
124
+ verify(payload) {
49
125
  try {
50
- const decipher = (0, crypto_1.createDecipheriv)(this.cipher, this.key, iv);
51
- decipher.setAuthTag(tag);
52
- let decrypted = decipher.update(value, 'base64', 'utf8');
53
- decrypted += decipher.final('utf8');
54
- return JSON.parse(decrypted);
126
+ const decoded = JSON.parse(Buffer.from(payload, 'base64').toString('utf8'));
127
+ if (!decoded.value || !decoded.signature) {
128
+ throw new DecryptionException_1.DecryptionException('Invalid signed payload.');
129
+ }
130
+ // Verify with all available keys
131
+ for (const key of this.keys) {
132
+ const signature = (0, crypto_1.createHmac)('sha256', key)
133
+ .update(decoded.value)
134
+ .digest('hex');
135
+ if ((0, crypto_1.timingSafeEqual)(Buffer.from(signature), Buffer.from(decoded.signature))) {
136
+ return JSON.parse(decoded.value);
137
+ }
138
+ }
55
139
  }
56
- catch (error) {
57
- throw new DecryptionException_1.DecryptionException();
140
+ catch (e) {
141
+ // Fall through
58
142
  }
143
+ throw new DecryptionException_1.DecryptionException('Signature verification failed.');
144
+ }
145
+ /**
146
+ * Create a hash (HMAC) of the given value.
147
+ */
148
+ hash(value) {
149
+ return (0, crypto_1.createHmac)('sha256', this.keys[0])
150
+ .update(value)
151
+ .digest('hex');
59
152
  }
60
153
  /**
61
154
  * Get the JSON payload from the given string.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arikajs/encryption",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Secure, application-level encryption for the ArikaJS framework.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -8,7 +8,10 @@
8
8
  "scripts": {
9
9
  "build": "tsc -p tsconfig.json",
10
10
  "clean": "rm -rf dist",
11
- "prepare": "echo skip"
11
+ "prepare": "echo skip",
12
+ "test": "npm run build && node --test 'dist/tests/**/*.test.js'",
13
+ "test:watch": "npm run build && node --test --watch 'dist/tests/**/*.test.js'",
14
+ "dev": "tsc -p tsconfig.json --watch"
12
15
  },
13
16
  "files": [
14
17
  "dist"
@@ -18,8 +21,18 @@
18
21
  },
19
22
  "dependencies": {},
20
23
  "devDependencies": {
24
+ "tsx": "^4.7.1",
21
25
  "@types/node": "^20.11.24",
22
26
  "typescript": "^5.3.3"
23
27
  },
24
- "author": "Prakash Tank"
28
+ "author": "Prakash Tank",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/ArikaJs/arikajs.git",
32
+ "directory": "packages/encryption"
33
+ },
34
+ "bugs": {
35
+ "url": "https://github.com/ArikaJs/arikajs/issues"
36
+ },
37
+ "homepage": "https://github.com/ArikaJs/arikajs/tree/main/packages/encryption#readme"
25
38
  }