Skip to content

feat: implement V2 encryption and decryption methods in Encryptor class#1560

Merged
Artuomka merged 2 commits intomainfrom
backend_security_report
Feb 4, 2026
Merged

feat: implement V2 encryption and decryption methods in Encryptor class#1560
Artuomka merged 2 commits intomainfrom
backend_security_report

Conversation

@Artuomka
Copy link
Collaborator

@Artuomka Artuomka commented Feb 4, 2026

No description provided.

Copilot AI review requested due to automatic review settings February 4, 2026 12:38
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements V2 encryption in the Encryptor class, upgrading from CryptoJS AES to Node.js native crypto with AES-256-GCM authenticated encryption. The changes maintain backward compatibility with the existing V1 (CryptoJS) format through automatic format detection and lazy migration.

Changes:

  • Implemented V2 encryption using AES-256-GCM with PBKDF2 key derivation (100,000 iterations)
  • Added version prefix '$v2:k1$' to identify the encryption format
  • Maintained backward compatibility by detecting and decrypting V1 format data

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 20 comments.

File Description
rocketadmin-agent/src/helpers/encryption/encryptor.ts Implements V2 encryption/decryption methods for master password operations with backward compatibility
backend/src/helpers/encryption/encryptor.ts Implements V2 encryption/decryption for both PRIVATE_KEY and master password operations with backward compatibility
backend/test/ava-tests/encryptor.test.ts Comprehensive test suite covering V2 encryption, backward compatibility, edge cases, and utility functions

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

const salt = Buffer.from(saltB64, 'base64');
const iv = Buffer.from(ivB64, 'base64');
const authTag = Buffer.from(authTagB64, 'base64');
const encrypted = Buffer.from(encryptedB64, 'base64');
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After converting the base64 strings to buffers, the buffer lengths should be validated to match expected sizes (salt should be 32 bytes, IV should be 16 bytes, authTag should be 16 bytes for GCM). Without validation, corrupted or malicious data could pass through with incorrect buffer sizes and cause unexpected behavior or security issues during decryption. Add validation: if (salt.length !== SALT_LENGTH || iv.length !== IV_LENGTH || authTag.length !== 16) { throw new Error('Invalid V2 encrypted data format: incorrect buffer sizes'); }

Suggested change
const encrypted = Buffer.from(encryptedB64, 'base64');
const encrypted = Buffer.from(encryptedB64, 'base64');
if (salt.length !== SALT_LENGTH || iv.length !== IV_LENGTH || authTag.length !== 16) {
throw new Error('Invalid V2 encrypted data format: incorrect buffer sizes');
}

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +104
test('decryptData: should throw error for invalid V2 format', (t) => {
const invalidV2 = '$v2:k1$invaliddata';

const error = t.throws(() => {
Encryptor.decryptData(invalidV2);
});

t.true(error.message.includes('Invalid V2 encrypted data format') || error.message.includes('decryption failed'));
});
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test should verify that data with a different version prefix (e.g., '$v2:k2$someciphertext') is rejected or handled correctly. Currently, due to a bug in the isV2Format method (which only checks for '$v2:' instead of the full '$v2:k1$'), such data would incorrectly be treated as valid V2 format and fail during decryption. Add a test case to verify proper handling of incompatible version prefixes.

Copilot uses AI. Check for mistakes.
Comment on lines +101 to +103
if (data === null || data === undefined) {
return data;
}
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning null or undefined as-is when data is null/undefined can lead to type safety issues. The function signature returns 'string', but this code returns null or undefined. This could cause runtime errors in calling code that expects a string. Consider either: 1) changing the return type to 'string | null | undefined', 2) throwing an error for invalid inputs, or 3) returning an empty string. The same issue exists in encryptData and decryptData methods in this file.

Copilot uses AI. Check for mistakes.
Comment on lines +71 to +73
if (data === null || data === undefined) {
return data;
}
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning null or undefined as-is when data is null/undefined can lead to type safety issues. The function signature returns 'string', but this code returns null or undefined. This could cause runtime errors in calling code that expects a string. Consider either: 1) changing the return type to 'string | null | undefined', 2) throwing an error for invalid inputs, or 3) returning an empty string.

Copilot uses AI. Check for mistakes.
}

private static isV2Format(encryptedData: string): boolean {
return encryptedData.startsWith('$v2:');
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version prefix check is inconsistent with the defined constant. The constant ENCRYPTION_VERSION_PREFIX is set to '$v2:k1$' but this method only checks for '$v2:'. This means data encrypted with the full prefix '$v2:k1$' will be correctly identified as V2 format, but if data were prefixed with just '$v2:' followed by something else (like '$v2:k2$'), it would incorrectly be treated as V2 format and fail during decryption when the substring operation removes too few characters. The check should use the full constant: return encryptedData.startsWith(ENCRYPTION_VERSION_PREFIX);

Suggested change
return encryptedData.startsWith('$v2:');
return encryptedData.startsWith(ENCRYPTION_VERSION_PREFIX);

Copilot uses AI. Check for mistakes.
const salt = Buffer.from(saltB64, 'base64');
const iv = Buffer.from(ivB64, 'base64');
const authTag = Buffer.from(authTagB64, 'base64');
const encrypted = Buffer.from(encryptedB64, 'base64');
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After converting the base64 strings to buffers, the buffer lengths should be validated to match expected sizes (salt should be 32 bytes, IV should be 16 bytes, authTag should be 16 bytes for GCM). Without validation, corrupted or malicious data could pass through with incorrect buffer sizes and cause unexpected behavior or security issues during decryption. Add validation: if (salt.length !== SALT_LENGTH || iv.length !== IV_LENGTH || authTag.length !== 16) { throw new Error('Invalid V2 encrypted data format: incorrect buffer sizes'); }

Suggested change
const encrypted = Buffer.from(encryptedB64, 'base64');
const encrypted = Buffer.from(encryptedB64, 'base64');
if (salt.length !== SALT_LENGTH || iv.length !== IV_LENGTH || authTag.length !== 16) {
throw new Error('Invalid V2 encrypted data format: incorrect buffer sizes');
}

Copilot uses AI. Check for mistakes.
@@ -1,3 +1,4 @@
/** biome-ignore-all lint/complexity/noStaticOnlyClass: <explanation> */
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The biome-ignore comment has an empty explanation placeholder. Either provide a meaningful explanation for why the 'noStaticOnlyClass' lint rule is being ignored (e.g., 'This class serves as a utility namespace for encryption methods and doesn't require instantiation'), or consider if the class design could be improved to avoid needing this suppression.

Suggested change
/** biome-ignore-all lint/complexity/noStaticOnlyClass: <explanation> */
/** biome-ignore-all lint/complexity/noStaticOnlyClass: This class intentionally serves as a static utility namespace for encryption and hashing helpers and is never instantiated. */

Copilot uses AI. Check for mistakes.
Comment on lines +77 to +81
if (Encryptor.isV2Format(encryptedData)) {
return Encryptor.decryptDataV2(encryptedData, masterPwd);
}

return Encryptor.decryptDataV1Legacy(encryptedData, masterPwd);
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decryptDataMasterPwd method does not have error handling with try-catch. If decryption fails (e.g., wrong password, corrupted data), the method will throw a raw error from the crypto module which may not provide user-friendly error messages. Consider wrapping the decryption calls in a try-catch block and throwing a more descriptive error, similar to how it's done in the backend version of this file (backend/src/helpers/encryption/encryptor.ts lines 111-119).

Suggested change
if (Encryptor.isV2Format(encryptedData)) {
return Encryptor.decryptDataV2(encryptedData, masterPwd);
}
return Encryptor.decryptDataV1Legacy(encryptedData, masterPwd);
try {
if (Encryptor.isV2Format(encryptedData)) {
return Encryptor.decryptDataV2(encryptedData, masterPwd);
}
return Encryptor.decryptDataV1Legacy(encryptedData, masterPwd);
} catch (_err) {
throw new Error(Messages.CORRUPTED_DATA);
}

Copilot uses AI. Check for mistakes.
Comment on lines +50 to +53
const salt = Buffer.from(saltB64, 'base64');
const iv = Buffer.from(ivB64, 'base64');
const authTag = Buffer.from(authTagB64, 'base64');
const encrypted = Buffer.from(encryptedB64, 'base64');
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The base64 decoding from user-provided input doesn't have error handling. If any of the base64 strings are malformed, Buffer.from() will silently decode what it can and may produce incorrect buffer lengths. This should be caught by subsequent operations (createDecipheriv, setAuthTag), but it would be better to validate the base64 format explicitly or wrap the buffer creation in a try-catch to provide clearer error messages. Consider adding validation or explicit error handling around the Buffer.from() calls.

Copilot uses AI. Check for mistakes.
Comment on lines +66 to +73
public static encryptDataMasterPwd(data: string, masterPwd: string): string {
if (data === null || data === undefined) {
return data;
}
return Encryptor.encryptDataV2(data, masterPwd);
}

public static decryptDataMasterPwd(encryptedData: string, masterPwd: string): string {
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning null or undefined as-is when data is null/undefined can lead to type safety issues. The function signature returns 'string', but this code returns null or undefined. This could cause runtime errors in calling code that expects a string. Consider either: 1) changing the return type to 'string | null | undefined', 2) throwing an error for invalid inputs, or 3) returning an empty string. The same issue exists in decryptDataMasterPwd below.

Suggested change
public static encryptDataMasterPwd(data: string, masterPwd: string): string {
if (data === null || data === undefined) {
return data;
}
return Encryptor.encryptDataV2(data, masterPwd);
}
public static decryptDataMasterPwd(encryptedData: string, masterPwd: string): string {
public static encryptDataMasterPwd(data: string, masterPwd: string): string | null | undefined {
if (data === null || data === undefined) {
return data;
}
return Encryptor.encryptDataV2(data, masterPwd);
}
public static decryptDataMasterPwd(encryptedData: string, masterPwd: string,): string | null | undefined {

Copilot uses AI. Check for mistakes.
@Artuomka Artuomka enabled auto-merge February 4, 2026 12:48
@Artuomka Artuomka merged commit c372167 into main Feb 4, 2026
19 checks passed
@Artuomka Artuomka deleted the backend_security_report branch February 4, 2026 13:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant