@techbulls/encrypted-s3-store 1.0.3 → 1.0.6

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
@@ -5,6 +5,7 @@ A TypeScript library that provides transparent end-to-end encryption for AWS S3
5
5
  ## Features
6
6
 
7
7
  - **AES-256-GCM Encryption** - Military-grade encryption for all stored objects
8
+ - **Custom Encryption Support** - Bring your own encryption/decryption functions
8
9
  - **Configurable Modes** - Choose between `encrypt` (with encryption) or `passthrough` (without encryption)
9
10
  - **Scrypt Key Derivation** - Secure memory-hard key derivation using Node.js built-in crypto
10
11
  - **Transparent Encryption/Decryption** - Automatic handling during upload and download
@@ -64,11 +65,12 @@ const result = await store.download({
64
65
 
65
66
  The `S3ObjectStore` constructor accepts the following parameters:
66
67
 
67
- | Parameter | Type | Required | Description |
68
- | --------- | ---------- | -------- | -------------------------------------------------------------- |
69
- | `client` | `S3Client` | Yes | An initialized AWS S3Client instance |
70
- | `key` | `string` | Yes\* | Encryption key for AES-256-GCM |
71
- | `mode` | `ModeType` | No | Operation mode (`'encrypt'` or `'passthrough'`). Default: `'encrypt'` |
68
+ | Parameter | Type | Required | Description |
69
+ | ------------------ | ------------------ | -------- | --------------------------------------------------------------------- |
70
+ | `client` | `S3Client` | Yes | An initialized AWS S3Client instance |
71
+ | `key` | `string` | Yes\* | Encryption key for AES-256-GCM or custom encryption |
72
+ | `mode` | `ModeType` | No | Operation mode (`'encrypt'` or `'passthrough'`). Default: `'encrypt'` |
73
+ | `customEncryption` | `CustomEncryption` | No | Custom encrypt/decrypt functions to replace default AES-256-GCM |
72
74
 
73
75
  \*Required when using `encrypt` mode. Can also be provided via `ENCRYPTION_KEY` environment variable.
74
76
 
@@ -250,6 +252,77 @@ const result = await store.download({
250
252
  });
251
253
  ```
252
254
 
255
+ ### Custom Encryption
256
+
257
+ You can provide your own encryption/decryption functions to replace the default AES-256-GCM implementation:
258
+
259
+ ```typescript
260
+ import { S3ObjectStore, CustomEncryption } from '@techbulls/encrypted-s3-store';
261
+ import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
262
+
263
+ const customEncryption: CustomEncryption = {
264
+ encrypt: (data: Buffer, key: string) => {
265
+ // Your custom encryption logic
266
+ const iv = randomBytes(16);
267
+ const cipher = createCipheriv('aes-256-cbc', Buffer.from(key.padEnd(32)), iv);
268
+ const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
269
+
270
+ return {
271
+ data: encrypted,
272
+ metadata: {
273
+ iv: iv.toString('base64'),
274
+ algorithm: 'aes-256-cbc',
275
+ },
276
+ };
277
+ },
278
+ decrypt: (data: Buffer, key: string, metadata) => {
279
+ // Your custom decryption logic using the metadata
280
+ const iv = Buffer.from(metadata.iv, 'base64');
281
+ const decipher = createDecipheriv('aes-256-cbc', Buffer.from(key.padEnd(32)), iv);
282
+ return Buffer.concat([decipher.update(data), decipher.final()]);
283
+ },
284
+ };
285
+
286
+ const store = new S3ObjectStore(s3Client, 'my-secret-key', 'encrypt', customEncryption);
287
+
288
+ // Upload using custom encryption
289
+ await store.upload({
290
+ Bucket: 'my-bucket',
291
+ Key: 'file.txt',
292
+ Body: 'Sensitive data',
293
+ });
294
+
295
+ // Download using custom decryption
296
+ const result = await store.download({
297
+ Bucket: 'my-bucket',
298
+ Key: 'file.txt',
299
+ });
300
+ ```
301
+
302
+ **Custom Encryption Interface:**
303
+
304
+ ```typescript
305
+ interface CustomEncryption {
306
+ encrypt: (data: Buffer, key: string) => EncryptResult | Promise<EncryptResult>;
307
+ decrypt: (data: Buffer, key: string, metadata: EncryptionMetadata) => Buffer | Promise<Buffer>;
308
+ }
309
+
310
+ interface EncryptResult {
311
+ data: Buffer; // The encrypted data
312
+ metadata: EncryptionMetadata; // Metadata to store for decryption
313
+ }
314
+
315
+ interface EncryptionMetadata {
316
+ [key: string]: string; // Key-value pairs stored in S3 object metadata
317
+ }
318
+ ```
319
+
320
+ **Notes:**
321
+ - Both `encrypt` and `decrypt` functions must be provided together
322
+ - Metadata keys are automatically prefixed with `x-amz-custom-` when stored in S3
323
+ - Custom encryption uses `x-amz-encryption: 'custom'` as the marker in S3 metadata
324
+ - Functions can be synchronous or asynchronous (return Promise)
325
+
253
326
  ### Environment-based Configuration
254
327
 
255
328
  ```typescript
@@ -282,10 +355,15 @@ Uses scrypt with the following parameters:
282
355
 
283
356
  Encryption metadata is stored in S3 object metadata:
284
357
 
358
+ **Default AES-256-GCM:**
285
359
  - `x-amz-iv` - Base64-encoded initialization vector
286
360
  - `x-amz-salt` - Base64-encoded salt for key derivation
287
361
  - `x-amz-auth-tag` - Base64-encoded authentication tag
288
- - `x-amz-encryption` - Encryption algorithm identifier
362
+ - `x-amz-encryption` - Set to `aes-256-gcm`
363
+
364
+ **Custom Encryption:**
365
+ - `x-amz-custom-*` - Custom metadata keys (prefixed automatically)
366
+ - `x-amz-encryption` - Set to `custom`
289
367
 
290
368
  ## Error Handling
291
369
 
@@ -308,7 +386,16 @@ The library is written in TypeScript and exports all types:
308
386
 
309
387
  ```typescript
310
388
  import { S3ObjectStore } from '@techbulls/encrypted-s3-store';
311
- import type { ModeType, IUpload, IDownload } from '@techbulls/encrypted-s3-store';
389
+ import type {
390
+ ModeType,
391
+ IUpload,
392
+ IDownload,
393
+ CustomEncryption,
394
+ EncryptFunction,
395
+ DecryptFunction,
396
+ EncryptResult,
397
+ EncryptionMetadata,
398
+ } from '@techbulls/encrypted-s3-store';
312
399
  import type { PutObjectCommandInput, GetObjectCommandInput } from '@techbulls/encrypted-s3-store';
313
400
  ```
314
401
 
@@ -350,4 +437,4 @@ await store.client.send(
350
437
 
351
438
  ## License
352
439
 
353
- Licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for details.
440
+ Licensed under the MIT License. See [LICENSE.md](LICENSE.md) for details.
package/dist/client.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * @license Apache-2.0
7
7
  */
8
8
  import { S3Client } from '@aws-sdk/client-s3';
9
- import { IDownload, IUpload, ModeType } from './types.js';
9
+ import { CustomEncryption, IDownload, IUpload, ModeType } from './types.js';
10
10
  import { Readable } from 'node:stream';
11
11
  /**
12
12
  * S3 client wrapper with transparent end-to-end encryption support.
@@ -40,12 +40,15 @@ export declare class S3ObjectStore {
40
40
  algorithm: string;
41
41
  /** @type {S3Client} The underlying AWS S3 client instance */
42
42
  client: S3Client;
43
+ /** @type {CustomEncryption | undefined} Optional custom encryption/decryption functions */
44
+ customEncryption?: CustomEncryption;
43
45
  /**
44
46
  * Creates a new S3ObjectStore instance.
45
47
  *
46
48
  * @param client - An initialized AWS S3Client instance
47
49
  * @param key - Encryption key for AES-256-GCM. Falls back to ENCRYPTION_KEY environment variable if not provided.
48
50
  * @param mode - Operation mode ('encrypt' or 'passthrough'). Defaults to 'encrypt'.
51
+ * @param customEncryption - Optional custom encryption/decryption functions to replace the default AES-256-GCM implementation.
49
52
  * @throws {Error} If no encryption key is provided and ENCRYPTION_KEY environment variable is not set
50
53
  *
51
54
  * @example
@@ -58,13 +61,24 @@ export declare class S3ObjectStore {
58
61
  *
59
62
  * // With passthrough mode (no encryption)
60
63
  * const store = new S3ObjectStore(s3Client, 'key', 'passthrough');
64
+ *
65
+ * // With custom encryption
66
+ * const store = new S3ObjectStore(s3Client, 'key', 'encrypt', {
67
+ * encrypt: (data, key) => ({ data: myEncrypt(data, key), metadata: { myParam: '...' } }),
68
+ * decrypt: (data, key, metadata) => myDecrypt(data, key, metadata.myParam)
69
+ * });
61
70
  * ```
62
71
  */
63
- constructor(client: S3Client, key?: string, mode?: ModeType);
72
+ constructor(client: S3Client, key?: string, mode?: ModeType, customEncryption?: CustomEncryption);
64
73
  /**
65
74
  * Uploads data to S3 with optional encryption.
66
75
  *
67
- * In 'encrypt' mode:
76
+ * In 'encrypt' mode with custom encryption:
77
+ * - Calls the custom encrypt function with data and key
78
+ * - Stores returned metadata with 'x-amz-custom-' prefix in S3 object metadata
79
+ * - Sets 'x-amz-encryption' to 'custom'
80
+ *
81
+ * In 'encrypt' mode (default AES-256-GCM):
68
82
  * - Generates a random 12-byte IV (Initialization Vector)
69
83
  * - Generates a random 16-byte salt for key derivation
70
84
  * - Derives a 256-bit key using scrypt
@@ -109,7 +123,12 @@ export declare class S3ObjectStore {
109
123
  /**
110
124
  * Downloads data from S3 with optional decryption.
111
125
  *
112
- * In 'encrypt' mode:
126
+ * In 'encrypt' mode with custom encryption (x-amz-encryption: 'custom'):
127
+ * - Extracts custom metadata (removes 'x-amz-custom-' prefix)
128
+ * - Buffers the encrypted stream
129
+ * - Calls the custom decrypt function with data, key, and metadata
130
+ *
131
+ * In 'encrypt' mode (default AES-256-GCM):
113
132
  * - Retrieves encryption metadata (IV, salt, auth tag) from S3 object metadata
114
133
  * - Derives the decryption key using scrypt with the stored salt
115
134
  * - Decrypts the data using AES-256-GCM
package/dist/client.js CHANGED
@@ -37,6 +37,7 @@ export class S3ObjectStore {
37
37
  * @param client - An initialized AWS S3Client instance
38
38
  * @param key - Encryption key for AES-256-GCM. Falls back to ENCRYPTION_KEY environment variable if not provided.
39
39
  * @param mode - Operation mode ('encrypt' or 'passthrough'). Defaults to 'encrypt'.
40
+ * @param customEncryption - Optional custom encryption/decryption functions to replace the default AES-256-GCM implementation.
40
41
  * @throws {Error} If no encryption key is provided and ENCRYPTION_KEY environment variable is not set
41
42
  *
42
43
  * @example
@@ -49,9 +50,15 @@ export class S3ObjectStore {
49
50
  *
50
51
  * // With passthrough mode (no encryption)
51
52
  * const store = new S3ObjectStore(s3Client, 'key', 'passthrough');
53
+ *
54
+ * // With custom encryption
55
+ * const store = new S3ObjectStore(s3Client, 'key', 'encrypt', {
56
+ * encrypt: (data, key) => ({ data: myEncrypt(data, key), metadata: { myParam: '...' } }),
57
+ * decrypt: (data, key, metadata) => myDecrypt(data, key, metadata.myParam)
58
+ * });
52
59
  * ```
53
60
  */
54
- constructor(client, key, mode) {
61
+ constructor(client, key, mode, customEncryption) {
55
62
  /** @type {string} Encryption algorithm identifier (always 'aes-256-gcm') */
56
63
  this.algorithm = 'aes-256-gcm';
57
64
  this.client = client;
@@ -61,11 +68,17 @@ export class S3ObjectStore {
61
68
  }
62
69
  this.key = (key || envEncryptionKey);
63
70
  this.mode = mode ?? 'encrypt';
71
+ this.customEncryption = customEncryption;
64
72
  }
65
73
  /**
66
74
  * Uploads data to S3 with optional encryption.
67
75
  *
68
- * In 'encrypt' mode:
76
+ * In 'encrypt' mode with custom encryption:
77
+ * - Calls the custom encrypt function with data and key
78
+ * - Stores returned metadata with 'x-amz-custom-' prefix in S3 object metadata
79
+ * - Sets 'x-amz-encryption' to 'custom'
80
+ *
81
+ * In 'encrypt' mode (default AES-256-GCM):
69
82
  * - Generates a random 12-byte IV (Initialization Vector)
70
83
  * - Generates a random 16-byte salt for key derivation
71
84
  * - Derives a 256-bit key using scrypt
@@ -120,14 +133,6 @@ export class S3ObjectStore {
120
133
  }));
121
134
  }
122
135
  /** Encrypt mode: encrypt data before uploading */
123
- /** @type {Buffer} iv - 96-bit IV for GCM mode */
124
- const iv = randomBytes(12);
125
- /** @type {Buffer} salt - 128-bit salt for scrypt key derivation */
126
- const salt = randomBytes(16);
127
- /** Derive encryption key using scrypt (memory-hard, resistant to GPU attacks) */
128
- const secretKey = await deriveKey(this.key, salt);
129
- /** Create AES-256-GCM cipher */
130
- const cipher = createCipheriv('aes-256-gcm', secretKey, iv);
131
136
  /** Convert input body to Buffer for encryption */
132
137
  let data;
133
138
  if (typeof input.Body === 'string') {
@@ -142,6 +147,36 @@ export class S3ObjectStore {
142
147
  else {
143
148
  throw new Error('Unsupported Body type: only string, Buffer, or Uint8Array supported');
144
149
  }
150
+ /** Use custom encryption if provided */
151
+ if (this.customEncryption) {
152
+ const result = await this.customEncryption.encrypt(data, this.key);
153
+ /** Prefix custom metadata keys to avoid conflicts */
154
+ const customMetadata = {};
155
+ for (const [key, value] of Object.entries(result.metadata)) {
156
+ customMetadata[`x-amz-custom-${key}`] = value;
157
+ }
158
+ return this.client.send(new PutObjectCommand({
159
+ ...input,
160
+ Bucket: input.Bucket || this.bucket,
161
+ Body: result.data,
162
+ ContentLength: result.data.length,
163
+ Metadata: {
164
+ ...(input.Metadata || {}),
165
+ ...customMetadata,
166
+ /** @description Marker indicating custom encryption was used */
167
+ 'x-amz-encryption': 'custom',
168
+ },
169
+ }));
170
+ }
171
+ /** Default AES-256-GCM encryption */
172
+ /** @type {Buffer} iv - 96-bit IV for GCM mode */
173
+ const iv = randomBytes(12);
174
+ /** @type {Buffer} salt - 128-bit salt for scrypt key derivation */
175
+ const salt = randomBytes(16);
176
+ /** Derive encryption key using scrypt (memory-hard, resistant to GPU attacks) */
177
+ const secretKey = await deriveKey(this.key, salt);
178
+ /** Create AES-256-GCM cipher */
179
+ const cipher = createCipheriv('aes-256-gcm', secretKey, iv);
145
180
  /** Encrypt the data */
146
181
  const encryptedBody = Buffer.concat([cipher.update(data), cipher.final()]);
147
182
  /** Get the GCM authentication tag (ensures data integrity and authenticity) */
@@ -168,7 +203,12 @@ export class S3ObjectStore {
168
203
  /**
169
204
  * Downloads data from S3 with optional decryption.
170
205
  *
171
- * In 'encrypt' mode:
206
+ * In 'encrypt' mode with custom encryption (x-amz-encryption: 'custom'):
207
+ * - Extracts custom metadata (removes 'x-amz-custom-' prefix)
208
+ * - Buffers the encrypted stream
209
+ * - Calls the custom decrypt function with data, key, and metadata
210
+ *
211
+ * In 'encrypt' mode (default AES-256-GCM):
172
212
  * - Retrieves encryption metadata (IV, salt, auth tag) from S3 object metadata
173
213
  * - Derives the decryption key using scrypt with the stored salt
174
214
  * - Decrypts the data using AES-256-GCM
@@ -214,10 +254,35 @@ export class S3ObjectStore {
214
254
  /** Encrypt mode: decrypt the response */
215
255
  /** Extract encryption metadata from S3 object */
216
256
  const metadata = response.Metadata || {};
257
+ const encryption = metadata['x-amz-encryption'];
258
+ /** Handle custom encryption */
259
+ if (encryption === 'custom' && this.customEncryption) {
260
+ /** Extract custom metadata (remove x-amz-custom- prefix) */
261
+ const customMetadata = {};
262
+ for (const [key, value] of Object.entries(metadata)) {
263
+ if (key.startsWith('x-amz-custom-')) {
264
+ customMetadata[key.replace('x-amz-custom-', '')] = value;
265
+ }
266
+ }
267
+ /** Buffer the stream to get the encrypted data */
268
+ const chunks = [];
269
+ for await (const chunk of response.Body) {
270
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
271
+ }
272
+ const encryptedData = Buffer.concat(chunks);
273
+ /** Decrypt using custom decryption function */
274
+ const decryptedData = await this.customEncryption.decrypt(encryptedData, this.key, customMetadata);
275
+ return {
276
+ Body: Readable.from(decryptedData),
277
+ ContentType: response.ContentType,
278
+ ContentLength: response.ContentLength,
279
+ Metadata: response.Metadata,
280
+ };
281
+ }
282
+ /** Default AES-256-GCM decryption */
217
283
  const ivBase64 = metadata['x-amz-iv'];
218
284
  const saltBase64 = metadata['x-amz-salt'];
219
285
  const authTagBase64 = metadata['x-amz-auth-tag'];
220
- const encryption = metadata['x-amz-encryption'];
221
286
  /** If encryption metadata is missing, return unencrypted stream */
222
287
  if (!ivBase64 || !saltBase64 || !authTagBase64 || encryption !== 'aes-256-gcm') {
223
288
  return {
package/dist/types.d.ts CHANGED
@@ -53,3 +53,62 @@ export interface IUpload extends PutObjectCommandInput {
53
53
  export interface IDownload extends GetObjectCommandInput {
54
54
  Mode?: ModeType;
55
55
  }
56
+ /**
57
+ * Metadata returned by custom encrypt function, stored in S3 object metadata.
58
+ * All values must be strings as they are stored in S3 metadata headers.
59
+ */
60
+ export interface EncryptionMetadata {
61
+ [key: string]: string;
62
+ }
63
+ /**
64
+ * Result returned by custom encrypt function.
65
+ *
66
+ * @property data - The encrypted data as a Buffer
67
+ * @property metadata - Key-value pairs to be stored in S3 object metadata for decryption
68
+ */
69
+ export interface EncryptResult {
70
+ data: Buffer;
71
+ metadata: EncryptionMetadata;
72
+ }
73
+ /**
74
+ * Custom encryption function signature.
75
+ *
76
+ * @param data - The plaintext data to encrypt
77
+ * @param key - The encryption key provided to S3ObjectStore
78
+ * @returns The encrypted data and metadata (can be sync or async)
79
+ */
80
+ export type EncryptFunction = (data: Buffer, key: string) => Promise<EncryptResult> | EncryptResult;
81
+ /**
82
+ * Custom decryption function signature.
83
+ *
84
+ * @param data - The encrypted data to decrypt
85
+ * @param key - The encryption key provided to S3ObjectStore
86
+ * @param metadata - The metadata that was stored during encryption
87
+ * @returns The decrypted plaintext data (can be sync or async)
88
+ */
89
+ export type DecryptFunction = (data: Buffer, key: string, metadata: EncryptionMetadata) => Promise<Buffer> | Buffer;
90
+ /**
91
+ * Custom encryption handler interface.
92
+ * Both encrypt and decrypt functions must be provided together.
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * const customEncryption: CustomEncryption = {
97
+ * encrypt: (data, key) => {
98
+ * const iv = crypto.randomBytes(16);
99
+ * const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
100
+ * const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
101
+ * return { data: encrypted, metadata: { iv: iv.toString('base64') } };
102
+ * },
103
+ * decrypt: (data, key, metadata) => {
104
+ * const iv = Buffer.from(metadata.iv, 'base64');
105
+ * const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
106
+ * return Buffer.concat([decipher.update(data), decipher.final()]);
107
+ * }
108
+ * };
109
+ * ```
110
+ */
111
+ export interface CustomEncryption {
112
+ encrypt: EncryptFunction;
113
+ decrypt: DecryptFunction;
114
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@techbulls/encrypted-s3-store",
3
- "version": "1.0.3",
4
- "description": "",
3
+ "version": "1.0.6",
4
+ "description": "Encrypt and decrypt S3 objects with ease",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "type": "module",
@@ -11,7 +11,24 @@
11
11
  "types": "./dist/index.d.ts"
12
12
  }
13
13
  },
14
- "keywords": [],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "clean": "rm -rf dist",
17
+ "lint": "eslint .",
18
+ "lint:fix": "eslint . --fix",
19
+ "format": "prettier --write .",
20
+ "format:check": "prettier --check .",
21
+ "prepublishOnly": "npm run clean && npm run lint && npm run build"
22
+ },
23
+ "keywords": [
24
+ "s3",
25
+ "encryption",
26
+ "aws",
27
+ "aes",
28
+ "security",
29
+ "encrypted",
30
+ "storage"
31
+ ],
15
32
  "author": "Aditya Chaphekar <aditya.chaphekar@techbulls.com>",
16
33
  "license": "MIT",
17
34
  "files": [
@@ -19,7 +36,24 @@
19
36
  "README.md",
20
37
  "LICENSE.md"
21
38
  ],
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/techbulls/encrypted-s3-store.git"
42
+ },
43
+ "bugs": {
44
+ "url": "https://github.com/techbulls/encrypted-s3-store/issues"
45
+ },
46
+ "homepage": "https://github.com/techbulls/encrypted-s3-store#readme",
47
+ "engines": {
48
+ "node": ">=18.0.0"
49
+ },
50
+ "packageManager": "pnpm@10.19.0",
51
+ "peerDependencies": {
52
+ "@aws-sdk/client-s3": "^3.0.0"
53
+ },
22
54
  "devDependencies": {
55
+ "@aws-sdk/client-s3": "^3.958.0",
56
+ "@aws-sdk/types": "^3.957.0",
23
57
  "@eslint/js": "^9.39.2",
24
58
  "@types/node": "^25.0.3",
25
59
  "@typescript-eslint/eslint-plugin": "^8.50.1",
@@ -30,16 +64,5 @@
30
64
  "prettier": "^3.7.4",
31
65
  "typescript": "^5.9.3",
32
66
  "typescript-eslint": "^8.50.1"
33
- },
34
- "dependencies": {
35
- "@aws-sdk/client-s3": "^3.958.0",
36
- "@aws-sdk/types": "^3.957.0"
37
- },
38
- "scripts": {
39
- "build": "tsc",
40
- "lint": "eslint .",
41
- "lint:fix": "eslint . --fix",
42
- "format": "prettier --write .",
43
- "format:check": "prettier --check ."
44
67
  }
45
- }
68
+ }