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:
- IV Generation:
crypto.randomBytes(16)creates a fresh 16-byte initialization vector for every request, ensuring non-deterministic output even for identical plaintexts. - Cipher Creation:
crypto.createCipheriv('aes-256-cbc', key, iv)instantiates the CBC-mode cipher. - Encryption: The plaintext (typically a JSON-stringified upload result) is processed through
cipher.update()andcipher.final(). - 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:cipherHexformat. - Server routes in
src/main/server/routerManager.tsautomatically wrap upload results and unwrap deletion requests using theAESHelperclass. - User-configurable passwords are stored via
settings.aesPasswordwith 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →