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

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 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. 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:

{
  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 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 and typed in 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:

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

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

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

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 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.
  • 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 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 and typed in 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, 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, 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →