mirror of
https://github.com/LogicLabs-OU/OpenArchiver.git
synced 2026-04-06 00:31:57 +02:00
File encryption support
This commit is contained in:
@@ -55,6 +55,13 @@ STORAGE_S3_REGION=
|
||||
# Set to 'true' for MinIO and other non-AWS S3 services
|
||||
STORAGE_S3_FORCE_PATH_STYLE=false
|
||||
|
||||
# --- Storage Encryption ---
|
||||
# IMPORTANT: Generate a secure, random 32-byte hex string for this key.
|
||||
# You can use `openssl rand -hex 32` to generate a key.
|
||||
# This key is used for AES-256 encryption of files at rest.
|
||||
# This is an optional variable, if not set, files will not be encrypted.
|
||||
STORAGE_ENCRYPTION_KEY=
|
||||
|
||||
# --- Security & Authentication ---
|
||||
|
||||
# Rate Limiting
|
||||
@@ -79,5 +86,3 @@ ENCRYPTION_KEY=
|
||||
# Apache Tika Integration
|
||||
# ONLY active if TIKA_URL is set
|
||||
TIKA_URL=http://tika:9998
|
||||
|
||||
|
||||
|
||||
@@ -62,6 +62,10 @@ Now, open the `.env` file in a text editor and customize the settings.
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
```
|
||||
- `STORAGE_ENCRYPTION_KEY`: **(Optional but Recommended)** A 32-byte hex string for encrypting emails and attachments at rest. If this key is not provided, storage encryption will be disabled. You can generate one with:
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
### Storage Configuration
|
||||
|
||||
@@ -117,13 +121,14 @@ These variables are used by `docker-compose.yml` to configure the services.
|
||||
| ------------------------------ | ----------------------------------------------------------------------------------------------------------- | ------------------------- |
|
||||
| `STORAGE_TYPE` | The storage backend to use (`local` or `s3`). | `local` |
|
||||
| `BODY_SIZE_LIMIT` | The maximum request body size for uploads. Can be a number in bytes or a string with a unit (e.g., `100M`). | `100M` |
|
||||
| `STORAGE_LOCAL_ROOT_PATH` | The root path for local file storage. | `/var/data/open-archiver` |
|
||||
| `STORAGE_LOCAL_ROOT_PATH` | The root path for Open Archiver app data. | `/var/data/open-archiver` |
|
||||
| `STORAGE_S3_ENDPOINT` | The endpoint for S3-compatible storage (required if `STORAGE_TYPE` is `s3`). | |
|
||||
| `STORAGE_S3_BUCKET` | The bucket name for S3-compatible storage (required if `STORAGE_TYPE` is `s3`). | |
|
||||
| `STORAGE_S3_ACCESS_KEY_ID` | The access key ID for S3-compatible storage (required if `STORAGE_TYPE` is `s3`). | |
|
||||
| `STORAGE_S3_SECRET_ACCESS_KEY` | The secret access key for S3-compatible storage (required if `STORAGE_TYPE` is `s3`). | |
|
||||
| `STORAGE_S3_REGION` | The region for S3-compatible storage (required if `STORAGE_TYPE` is `s3`). | |
|
||||
| `STORAGE_S3_FORCE_PATH_STYLE` | Force path-style addressing for S3 (optional). | `false` |
|
||||
| `STORAGE_ENCRYPTION_KEY` | A 32-byte hex string for AES-256 encryption of files at rest. If not set, files will not be encrypted. | |
|
||||
|
||||
#### Security & Authentication
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ export const rateLimiter = rateLimit({
|
||||
max: config.api.rateLimit.max,
|
||||
keyGenerator: (req, res) => {
|
||||
// Use the real IP address of the client, even if it's behind a proxy.
|
||||
// This is safe because we have `app.set('trust proxy', true)` in `server.ts`.
|
||||
// `app.set('trust proxy', true)` in `server.ts`.
|
||||
return ipKeyGenerator(req.ip || 'unknown');
|
||||
},
|
||||
message: {
|
||||
|
||||
@@ -2,9 +2,14 @@ import { StorageConfig } from '@open-archiver/types';
|
||||
import 'dotenv/config';
|
||||
|
||||
const storageType = process.env.STORAGE_TYPE;
|
||||
const encryptionKey = process.env.STORAGE_ENCRYPTION_KEY;
|
||||
const openArchiverFolderName = 'open-archiver';
|
||||
let storageConfig: StorageConfig;
|
||||
|
||||
if (encryptionKey && !/^[a-fA-F0-9]{64}$/.test(encryptionKey)) {
|
||||
throw new Error('STORAGE_ENCRYPTION_KEY must be a 64-character hex string (32 bytes)');
|
||||
}
|
||||
|
||||
if (storageType === 'local') {
|
||||
if (!process.env.STORAGE_LOCAL_ROOT_PATH) {
|
||||
throw new Error('STORAGE_LOCAL_ROOT_PATH is not defined in the environment variables');
|
||||
@@ -13,6 +18,7 @@ if (storageType === 'local') {
|
||||
type: 'local',
|
||||
rootPath: process.env.STORAGE_LOCAL_ROOT_PATH,
|
||||
openArchiverFolderName: openArchiverFolderName,
|
||||
encryptionKey: encryptionKey,
|
||||
};
|
||||
} else if (storageType === 's3') {
|
||||
if (
|
||||
@@ -32,6 +38,7 @@ if (storageType === 'local') {
|
||||
region: process.env.STORAGE_S3_REGION,
|
||||
forcePathStyle: process.env.STORAGE_S3_FORCE_PATH_STYLE === 'true',
|
||||
openArchiverFolderName: openArchiverFolderName,
|
||||
encryptionKey: encryptionKey,
|
||||
};
|
||||
} else {
|
||||
throw new Error(`Invalid STORAGE_TYPE: ${storageType}`);
|
||||
|
||||
@@ -2,11 +2,22 @@ import { IStorageProvider, StorageConfig } from '@open-archiver/types';
|
||||
import { LocalFileSystemProvider } from './storage/LocalFileSystemProvider';
|
||||
import { S3StorageProvider } from './storage/S3StorageProvider';
|
||||
import { config } from '../config/index';
|
||||
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
|
||||
import { streamToBuffer } from '../helpers/streamToBuffer';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
const ENCRYPTION_PREFIX = Buffer.from('oa_enc_v1::');
|
||||
|
||||
export class StorageService implements IStorageProvider {
|
||||
private provider: IStorageProvider;
|
||||
private encryptionKey: Buffer | null = null;
|
||||
private readonly algorithm = 'aes-256-cbc';
|
||||
|
||||
constructor(storageConfig: StorageConfig = config.storage) {
|
||||
if (storageConfig.encryptionKey) {
|
||||
this.encryptionKey = Buffer.from(storageConfig.encryptionKey, 'hex');
|
||||
}
|
||||
|
||||
switch (storageConfig.type) {
|
||||
case 'local':
|
||||
this.provider = new LocalFileSystemProvider(storageConfig);
|
||||
@@ -19,12 +30,50 @@ export class StorageService implements IStorageProvider {
|
||||
}
|
||||
}
|
||||
|
||||
put(path: string, content: Buffer | NodeJS.ReadableStream): Promise<void> {
|
||||
return this.provider.put(path, content);
|
||||
private async encrypt(content: Buffer): Promise<Buffer> {
|
||||
if (!this.encryptionKey) {
|
||||
return content;
|
||||
}
|
||||
const iv = randomBytes(16);
|
||||
const cipher = createCipheriv(this.algorithm, this.encryptionKey, iv);
|
||||
const encrypted = Buffer.concat([cipher.update(content), cipher.final()]);
|
||||
return Buffer.concat([ENCRYPTION_PREFIX, iv, encrypted]);
|
||||
}
|
||||
|
||||
get(path: string): Promise<NodeJS.ReadableStream> {
|
||||
return this.provider.get(path);
|
||||
private async decrypt(content: Buffer): Promise<Buffer> {
|
||||
if (!this.encryptionKey) {
|
||||
return content;
|
||||
}
|
||||
const prefix = content.subarray(0, ENCRYPTION_PREFIX.length);
|
||||
if (!prefix.equals(ENCRYPTION_PREFIX)) {
|
||||
// File is not encrypted, return as is
|
||||
return content;
|
||||
}
|
||||
|
||||
try {
|
||||
const iv = content.subarray(ENCRYPTION_PREFIX.length, ENCRYPTION_PREFIX.length + 16);
|
||||
const encrypted = content.subarray(ENCRYPTION_PREFIX.length + 16);
|
||||
const decipher = createDecipheriv(this.algorithm, this.encryptionKey, iv);
|
||||
return Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
||||
} catch (error) {
|
||||
// Decryption failed for a file that has the prefix.
|
||||
// This indicates a corrupted file or a wrong key.
|
||||
throw new Error('Failed to decrypt file. It may be corrupted or the key is incorrect.');
|
||||
}
|
||||
}
|
||||
|
||||
async put(path: string, content: Buffer | NodeJS.ReadableStream): Promise<void> {
|
||||
const buffer =
|
||||
content instanceof Buffer ? content : await streamToBuffer(content as NodeJS.ReadableStream);
|
||||
const encryptedContent = await this.encrypt(buffer);
|
||||
return this.provider.put(path, encryptedContent);
|
||||
}
|
||||
|
||||
async get(path: string): Promise<NodeJS.ReadableStream> {
|
||||
const stream = await this.provider.get(path);
|
||||
const buffer = await streamToBuffer(stream);
|
||||
const decryptedContent = await this.decrypt(buffer);
|
||||
return Readable.from(decryptedContent);
|
||||
}
|
||||
|
||||
delete(path: string): Promise<void> {
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -45,6 +45,7 @@ export interface LocalStorageConfig {
|
||||
// The absolute root path on the server where the archive will be stored.
|
||||
rootPath: string;
|
||||
openArchiverFolderName: string;
|
||||
encryptionKey?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,6 +67,7 @@ export interface S3StorageConfig {
|
||||
// Force path-style addressing, required for MinIO.
|
||||
forcePathStyle?: boolean;
|
||||
openArchiverFolderName: string;
|
||||
encryptionKey?: string;
|
||||
}
|
||||
|
||||
export type StorageConfig = LocalStorageConfig | S3StorageConfig;
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user