# How PicList Encrypts Upload Data: AES-256-CBC Implementation Explained

> Discover how PicList encrypts upload data with AES-256-CBC. Learn about the IV, key derivation using PBKDF2, and secure `ivHex:cipherHex` output for protected client-server communication.

- Repository: [Kuingsmile/piclist](https://github.com/kuingsmile/piclist)
- Tags: internals
- Published: 2026-03-05

---

**PicList encrypts upload result metadata using AES-256-CBC with a per-request random 16-byte IV and a PBKDF2-derived 256-bit key, formatting the output as `ivHex:cipherHex` for secure transmission between the server and client.**

When handling image uploads in the [kuingsmile/piclist](https://github.com/kuingsmile/piclist) repository, the application implements a robust encryption layer to protect upload result data in transit. This encryption occurs server-side within the Node.js backend after successful file uploads, ensuring that sensitive metadata—such as remote URLs and configuration details—remains confidential during the response phase.

## Encryption Entry Points in routerManager.ts

The encryption process triggers immediately after file processing completes in [`src/main/server/routerManager.ts`](https://github.com/kuingsmile/piclist/blob/main/src/main/server/routerManager.ts). When the server finishes uploading images—whether from clipboard or batch file lists—it wraps the result objects before sending them to the client.

### Single Upload Encryption

For individual uploads, the code constructs a `treatedFullResult` object that marks the payload as encrypted:

```typescript
{
  isEncrypted: 1,
  EncryptedData: new AESHelper().encrypt(JSON.stringify(fullResult)),
  ...fullResult,
}

```

The `isEncrypted: 1` flag signals to the client that the `EncryptedData` field contains ciphertext rather than plaintext. The server strips internal configuration objects (via `delete treated.config`) before transmission to minimize exposure.

### Batch Upload Handling

For multiple files, the server iterates through upload results and applies the same encryption pattern to each `treatedItem`. During deletion operations, the server reverses this process: it checks for `isEncrypted` flags and decrypts payloads using the same `AESHelper` class before forwarding items to the deletion logic.

## AESHelper Core Implementation

The encryption engine resides in [`src/main/utils/aesHelper.ts`](https://github.com/kuingsmile/piclist/blob/main/src/main/utils/aesHelper.ts) within the `AESHelper` class. This utility manages symmetric encryption using **AES-256-CBC** mode through Node.js's `crypto` module.

### PBKDF2 Key Derivation

Before encryption begins, the class derives a 256-bit key from the user's password using **PBKDF2** (Password-Based Key Derivation Function 2) with the following hardened parameters:

- **Salt**: `a8b3c4d2e4f5098712345678feedc0de` (16-byte hex)
- **Iterations**: 100,000
- **Digest**: SHA-512
- **Key length**: 32 bytes (256 bits)

The password source follows a hierarchy: it first checks `settings.aesPassword` from PicList's configuration, falling back to the literal string `"aesPassword"` if undefined. The implementation caches derived keys in a static `Map` to avoid recomputing PBKDF2 hashes for repeated operations with the same password.

### AES-256-CBC Encryption Flow

For each encryption call, the system generates cryptographically secure randomness:

1. **IV Generation**: `crypto.randomBytes(16)` creates a fresh 16-byte initialization vector for every request, ensuring non-deterministic output even for identical plaintexts.
2. **Cipher Creation**: `crypto.createCipheriv('aes-256-cbc', key, iv)` instantiates the CBC-mode cipher.
3. **Encryption**: The plaintext (typically a JSON-stringified upload result) is processed through `cipher.update()` and `cipher.final()`.
4. **Output Format**: The final string concatenates the hex-encoded IV and ciphertext with a colon separator: `${ivHex}:${cipherHex}`.

### Decryption and Validation

The decryption method validates input format by checking for the colon separator and verifying the IV length matches 16 bytes. It then creates a decipher instance with the identical key and IV, returning the parsed JSON string. Malformed or tampered data triggers error handling that returns `"{}"` to prevent application crashes.

## Configuration and Password Management

Users control encryption keys through PicList's settings interface. The configuration path `settings.aesPassword` is defined in [`src/renderer/utils/configPaths.ts`](https://github.com/kuingsmile/piclist/blob/main/src/renderer/utils/configPaths.ts) and typed in [`src/universal/types/view.d.ts`](https://github.com/kuingsmile/piclist/blob/main/src/universal/types/view.d.ts), exposing the setting to both the renderer process and TypeScript compiler.

When instantiating `AESHelper` without explicit arguments, the constructor queries the PicList configuration store:

```typescript
const pwd = password ?? picgo.getConfig<string>(configPaths.settings.aesPassword) ?? 'aesPassword'
this.key = AESHelper.#deriveKey(pwd)

```

This design allows users to set custom passwords through the UI while maintaining encryption functionality with a default fallback.

## Practical Code Examples

### Encrypting Upload Results Server-Side

```typescript
import { AESHelper } from '~/utils/aesHelper'

async function handleUpload(files: string[]) {
  const fullResult = await uploadChoosedFiles(files)
  
  // Wrap with encryption layer
  const treated = {
    isEncrypted: 1,
    EncryptedData: new AESHelper().encrypt(JSON.stringify(fullResult)),
    ...fullResult,
  }
  
  // Remove sensitive internal config
  delete (treated as any).config
  
  return treated
}

```

### Decrypting During Delete Operations

```typescript
import { AESHelper } from '~/utils/aesHelper'

const aesHelper = new AESHelper()

function processDeleteList(list: any[]) {
  return list.map(item => {
    if (item.isEncrypted && item.EncryptedData) {
      return JSON.parse(aesHelper.decrypt(item.EncryptedData))
    }
    return item
  })
}

```

### Simplified AESHelper Class Structure

```typescript
export class AESHelper {
  static readonly #SALT = Buffer.from('a8b3c4d2e4f5098712345678feedc0de', 'hex')
  static readonly #ITERATIONS = 100_000
  static readonly #KEYLEN = 32
  static readonly #DIGEST = 'sha512' as const
  static readonly #ALGO = 'aes-256-cbc'
  static readonly #IV_LENGTH = 16
  static readonly #SEP = ':'

  constructor(password?: string) {
    const pwd = password ?? picgo.getConfig<string>(configPaths.settings.aesPassword) ?? 'aesPassword'
    this.key = AESHelper.#deriveKey(pwd)
  }

  static #deriveKey(pwd: string): Buffer {
    const cached = this.#keyCache.get(pwd)
    if (cached) return cached
    const key = crypto.pbkdf2Sync(pwd, this.#SALT, this.#ITERATIONS, this.#KEYLEN, this.#DIGEST)
    this.#keyCache.set(pwd, key)
    return key
  }

  encrypt(plain: string): string {
    const iv = crypto.randomBytes(AESHelper.#IV_LENGTH)
    const cipher = crypto.createCipheriv(AESHelper.#ALGO, this.key, iv)
    const encrypted = Buffer.concat([cipher.update(plain, 'utf8'), cipher.final()])
    return `${iv.toString('hex')}${AESHelper.#SEP}${encrypted.toString('hex')}`
  }

  decrypt(data: string): string {
    // Validates format, extracts IV, creates decipher, returns plaintext
    // Returns "{}" on malformed input
  }
}

```

## Testing and Security Validation

The test suite in [`tests/aeshelper.test.ts`](https://github.com/kuingsmile/piclist/blob/main/tests/aeshelper.test.ts) verifies cryptographic correctness through multiple assertions:

- **Round-trip integrity**: Encryption followed by decryption returns the original plaintext.
- **Randomized IVs**: Multiple encryptions of identical data produce different ciphertexts due to unique IVs.
- **Key caching**: PBKDF2 executes only once per unique password, improving performance for batch operations.
- **Graceful degradation**: Invalid hex strings or missing separators return empty JSON objects rather than throwing unhandled exceptions.

## Summary

- **PicList encrypts upload metadata**—not the image files themselves—using AES-256-CBC implemented in [`src/main/utils/aesHelper.ts`](https://github.com/kuingsmile/piclist/blob/main/src/main/utils/aesHelper.ts).
- **PBKDF2 derivation** uses 100,000 iterations of SHA-512 with a fixed salt to generate 256-bit keys from user passwords.
- **Per-request random IVs** ensure ciphertext uniqueness; outputs follow the `ivHex:cipherHex` format.
- **Server routes** in [`src/main/server/routerManager.ts`](https://github.com/kuingsmile/piclist/blob/main/src/main/server/routerManager.ts) automatically wrap upload results and unwrap deletion requests using the `AESHelper` class.
- **User-configurable passwords** are stored via `settings.aesPassword` with a fallback default, allowing customizable security levels.

## Frequently Asked Questions

### What encryption algorithm does PicList use for uploads?

PicList implements **AES-256-CBC** (Advanced Encryption Standard with 256-bit keys in Cipher Block Chaining mode) through Node.js's native `crypto` module. The system derives keys using PBKDF2 with SHA-512 and 100,000 iterations, then encrypts JSON-stringified upload results using random 16-byte IVs for each operation.

### Where is the encryption password stored in PicList?

The encryption password is stored in PicList's configuration store under the key `settings.aesPassword`, defined in [`src/renderer/utils/configPaths.ts`](https://github.com/kuingsmile/piclist/blob/main/src/renderer/utils/configPaths.ts) and typed in [`src/universal/types/view.d.ts`](https://github.com/kuingsmile/piclist/blob/main/src/universal/types/view.d.ts). Users can set this password through the PicList settings UI. If no password is configured, the system falls back to the hardcoded string `"aesPassword"`.

### Does PicList encrypt the actual image files or just metadata?

PicList encrypts **upload result metadata**—such as remote URLs, file names, and server responses—not the actual binary image content. The encryption occurs in the response phase after successful file uploads in [`src/main/server/routerManager.ts`](https://github.com/kuingsmile/piclist/blob/main/src/main/server/routerManager.ts), protecting transmission of sensitive result data between the PicList server and client interface.

### How does PicList handle decryption when deleting files?

When processing deletion requests in [`src/main/server/routerManager.ts`](https://github.com/kuingsmile/piclist/blob/main/src/main/server/routerManager.ts), PicList checks for the `isEncrypted` flag on incoming items. If present, it instantiates `AESHelper` and calls `decrypt()` on the `EncryptedData` field, parsing the result back into a JavaScript object. This decrypted payload is then forwarded to the platform-specific deletion logic to remove the remote files.