Conversation
Greptile OverviewGreptile Summaryintroduces encryption for application variables marked as secrets, using AES-256-CTR with a random IV. The implementation follows the existing pattern from PR #15283, creating a centralized Key changes:
Issues found:
Confidence Score: 3/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant Client
participant Resolver as ApplicationVariableEntityResolver
participant Service as ApplicationVariableEntityService
participant Encryption as SecretEncryptionService
participant EnvDriver as EnvironmentConfigDriver
participant AuthUtil as auth.util
participant DB as Database
Note over Client,DB: Update Application Variable Flow
Client->>Resolver: updateOneApplicationVariable(key, value, appId)
Resolver->>Service: update({key, plainTextValue, appId, workspaceId})
Service->>DB: findOne({key, appId})
DB-->>Service: existingVariable (with isSecret flag)
alt isSecret = true
Service->>Encryption: encrypt(plainTextValue)
Encryption->>EnvDriver: get('APP_SECRET')
EnvDriver-->>Encryption: appSecret
Encryption->>AuthUtil: encryptText(value, appSecret)
AuthUtil-->>Encryption: encryptedValue
Encryption-->>Service: encryptedValue
Service->>DB: update({key, appId}, {value: encryptedValue})
else isSecret = false
Service->>DB: update({key, appId}, {value: plainTextValue})
end
Service->>DB: invalidateAndRecompute cache
Service-->>Resolver: success
Resolver-->>Client: true
Note over Client,DB: Query Application Variable Flow (with masking)
Client->>Resolver: query applicationVariable
DB-->>Resolver: variable (encrypted if isSecret)
Resolver->>Service: getDisplayValue(variable)
alt isSecret = false
Service-->>Resolver: plainValue
else isSecret = true
Service->>Encryption: decrypt(variable.value)
Encryption->>EnvDriver: get('APP_SECRET')
EnvDriver-->>Encryption: appSecret
Encryption->>AuthUtil: decryptText(value, appSecret)
AuthUtil-->>Encryption: decryptedValue
Encryption-->>Service: decryptedValue
Service->>Service: calculate visibleChars = min(5, floor(length/10))
Service->>Service: mask = decrypted.slice(0, visibleChars) + "********"
Service-->>Resolver: maskedValue
end
Resolver-->>Client: displayValue
Note over Client,DB: Serverless Function Execution Flow
participant SLS as ServerlessFunctionService
participant BuildEnv as buildEnvVar
SLS->>DB: fetch flatApplicationVariables
DB-->>SLS: variables (encrypted if isSecret)
SLS->>BuildEnv: buildEnvVar(variables, encryptionService)
loop for each variable
alt isSecret = true
BuildEnv->>Encryption: decrypt(variable.value)
Encryption-->>BuildEnv: decryptedValue
else isSecret = false
BuildEnv->>BuildEnv: use value as-is
end
end
BuildEnv-->>SLS: envVars (all decrypted)
SLS->>SLS: execute function with plaintext env vars
|
...es/twenty-server/src/engine/core-modules/applicationVariable/application-variable.service.ts
Show resolved
Hide resolved
| try { | ||
| const appSecret = this.getAppSecret(); | ||
|
|
||
| return encryptText(value, appSecret); | ||
| } catch (error) { | ||
| this.logger.debug( | ||
| `Encryption failed: ${error.message}. Using original value.`, | ||
| ); | ||
|
|
||
| return value; | ||
| } |
There was a problem hiding this comment.
silently returns the original value when encryption fails. If APP_SECRET is missing or invalid, this could lead to secrets being stored unencrypted without the caller knowing. Consider throwing an error or logging at a higher level than debug.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/twenty-server/src/engine/core-modules/secret-encryption/secret-encryption.service.ts
Line: 28:38
Comment:
silently returns the original value when encryption fails. If `APP_SECRET` is missing or invalid, this could lead to secrets being stored unencrypted without the caller knowing. Consider throwing an error or logging at a higher level than debug.
How can I resolve this? If you propose a fix, please make it concise.|
🚀 Preview Environment Ready! Your preview environment is available at: http://bore.pub:50978 This environment will automatically shut down when the PR is closed or after 5 hours. |
|
|
||
| return convertedValue; | ||
| } | ||
| return isDecrypt |
There was a problem hiding this comment.
I dont understand why we had that try / catch behaviour before, swallowing the encryption/decryption error with a fallback which seems problematic to me - if we mean to encrypt a value but leave it decrypted, we will have errors when trying to decrypt again, won't we ? And getting the encrypted value when we try to fetch the decrypted value does not help either - if i understood correctly whats happening
There was a problem hiding this comment.
2 issues found across 19 files
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them.
<file name="packages/twenty-server/src/engine/core-modules/secret-encryption/secret-encryption.service.ts">
<violation number="1" location="packages/twenty-server/src/engine/core-modules/secret-encryption/secret-encryption.service.ts:25">
P1: Encryption failures should not silently fall back to storing plaintext; this can leave secrets unencrypted at rest if APP_SECRET is misconfigured. Consider failing hard so callers can handle the error.</violation>
</file>
<file name="packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.service.ts">
<violation number="1" location="packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.service.ts:125">
P2: When updating an existing variable, the value isn’t updated/encrypted. If isSecret becomes true, the stored plaintext value will later be decrypted and fail. Include value: encryptedValue in the update payload.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
packages/twenty-server/src/engine/core-modules/secret-encryption/secret-encryption.service.ts
Show resolved
Hide resolved
...es/twenty-server/src/engine/core-modules/applicationVariable/application-variable.service.ts
Show resolved
Hide resolved
packages/twenty-server/src/engine/core-modules/secret-encryption/secret-encryption.service.ts
Outdated
Show resolved
Hide resolved
| encryptText: jest.fn((text) => `encrypted:${text}`), | ||
| decryptText: jest.fn((text) => text.replace('encrypted:', '')), | ||
| encryptText: jest.fn((text) => `${text}`), | ||
| decryptText: jest.fn((text) => text.replace('', '')), |
Check warning
Code scanning / CodeQL
Replacement of a substring with itself Medium test
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 2 months ago
In general, this kind of problem is fixed by removing or correcting the ineffective string replacement. If the goal is to transform the string (e.g., strip certain characters, unescape sequences), the search and replacement arguments to replace must be adjusted so the replacement is not identical to the matched substring. If the goal is simply to return the input unchanged, the replace call should be removed and the argument returned directly.
Here, the mock implementation lives in config-storage.service.spec.ts:
jest.mock('src/engine/core-modules/auth/auth.util', () => ({
encryptText: jest.fn((text) => `${text}`),
decryptText: jest.fn((text) => text.replace('', '')),
}));The intended behavior for decryptText in tests is almost certainly to be an identity function, just like encryptText. The best fix, preserving existing test behavior but removing the problematic pattern, is to change the implementation of decryptText to simply return its input. That can be done by replacing text.replace('', '') with text, or, for consistency with encryptText, with a template literal such as `${text}`. No new imports or helper methods are required; it's a one-line change in this file only.
Concretely:
- Edit line 27 in
packages/twenty-server/src/engine/core-modules/twenty-config/storage/__tests__/config-storage.service.spec.ts. - Replace
decryptText: jest.fn((text) => text.replace('', '')),withdecryptText: jest.fn((text) => text),(or the template literal variant). - This preserves the identity behavior while removing the ineffective replacement.
| @@ -24,7 +24,7 @@ | ||
|
|
||
| jest.mock('src/engine/core-modules/auth/auth.util', () => ({ | ||
| encryptText: jest.fn((text) => `${text}`), | ||
| decryptText: jest.fn((text) => text.replace('', '')), | ||
| decryptText: jest.fn((text) => text), | ||
| })); | ||
|
|
||
| describe('ConfigStorageService', () => { |
|
|
||
| return encryptText(value, appSecret); | ||
| } catch (error) { | ||
| this.logger.debug( |
There was a problem hiding this comment.
I think we should not swallow here as this is not expected to happen. If this fails we will save non-encrypted value and we won't know (this should not happen but still)
There was a problem hiding this comment.
totally agree, i have changed the behavior in config-storage (see my comment - would like some validation on this btw :)) but did not think to do here too
packages/twenty-server/src/engine/core-modules/secret-encryption/secret-encryption.service.ts
Outdated
Show resolved
Hide resolved
| import { EnvironmentConfigDriver } from 'src/engine/core-modules/twenty-config/drivers/environment-config.driver'; | ||
|
|
||
| @Injectable() | ||
| export class SecretEncryptionService { |
There was a problem hiding this comment.
I know I originally introduced this service in my PR but I'm wondering if it's bringing much value 🤔
There was a problem hiding this comment.
kept it and added decryptAndMask in it

Closes twentyhq/core-team-issues#1724
From PR #15283, followed same implementation
******