EncryptedString PlaintextString branded string types (#21001)
## Summary closes https://github.com/twentyhq/core-team-issues/issues/2464 Introduces compile-time branded types to distinguish encrypted ciphertext from plaintext strings, preventing mix-ups like the one fixed in #20819 — but at the type level rather in addition to the one existing at runtime. ### Branded string primitives - Created `EncryptedString` and `PlaintextString` as hard nominal brands using `z.string().brand(...)`, making them non-assignable to each other or to raw `string` - Created `isEncryptedString` type predicate to narrow `string` to `EncryptedString` based on the `enc:v2:` envelope prefix - Retyped `SecretEncryptionService`: `encryptVersioned` accepts `PlaintextString`, `decryptVersioned` returns `PlaintextString` ### Entity typing - Typed encrypted columns across entities: `SigningKeyEntity.privateKey`, `TwoFactorAuthenticationMethodEntity.secret`, `ApplicationRegistrationVariableEntity.encryptedValue`, `ApplicationVariableEntity.value` - Parameterized JSONB types for connected account connection parameters (`ImapSmtpCaldavParams<Pwd>`) with reusable aliases `EncryptedImapSmtpCaldavParams` / `DecryptedImapSmtpCaldavParams` - Typed DTOs (`CreateApplicationRegistrationVariableInput`, `UpdateApplicationRegistrationVariablePayload`, `UpdateApplicationVariableEntityInput`) with `PlaintextString` ### ApplicationVariable always-encrypt uniformization - Retyped `ApplicationVariableEntity.value` to `EncryptedString | ''` — all values are now encrypted regardless of `isSecret` - Updated `ApplicationVariableEntityService` to always encrypt on write and always decrypt on read - Simplified `UpdateApplicationVariableActionHandlerService` by removing conditional encrypt/decrypt-on-isSecret-toggle logic - Added slow instance command (`2.9.0`) to backfill-encrypt existing `isSecret=false` plaintext rows and tighten the `CHECK` constraint ### ConfigStorageService refactor - Split `convertAndSecureValue` (which used `any`) into two well-typed methods: `convertAndDecrypt` and `convertAndEncrypt` - Introduced `isSensitiveStringValue` type predicate to narrow values before encryption/decryption ### What's next - Typeorm entity derivation to strictly type sitemap configuration as code + handler logic for encryption rotation - https://github.com/twentyhq/core-team-issues/issues/2465
This commit is contained in:
+3
-32
@@ -3,7 +3,6 @@ import { Injectable } from '@nestjs/common';
|
||||
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
|
||||
|
||||
import { ApplicationVariableEntity } from 'src/engine/core-modules/application/application-variable/application-variable.entity';
|
||||
import { SecretEncryptionService } from 'src/engine/core-modules/secret-encryption/secret-encryption.service';
|
||||
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
|
||||
import { resolveUniversalUpdateRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-update-relation-identifiers-to-ids.util';
|
||||
import {
|
||||
@@ -20,9 +19,7 @@ export class UpdateApplicationVariableActionHandlerService extends WorkspaceMigr
|
||||
'update',
|
||||
'applicationVariable',
|
||||
) {
|
||||
constructor(
|
||||
private readonly secretEncryptionService: SecretEncryptionService,
|
||||
) {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -50,6 +47,8 @@ export class UpdateApplicationVariableActionHandlerService extends WorkspaceMigr
|
||||
};
|
||||
}
|
||||
|
||||
// Value is always encrypted regardless of isSecret, so toggling
|
||||
// isSecret does not require re-encrypting or decrypting the stored value.
|
||||
async executeForMetadata(
|
||||
context: WorkspaceMigrationActionRunnerContext<FlatUpdateApplicationVariableAction>,
|
||||
): Promise<void> {
|
||||
@@ -60,34 +59,6 @@ export class UpdateApplicationVariableActionHandlerService extends WorkspaceMigr
|
||||
ApplicationVariableEntity,
|
||||
);
|
||||
|
||||
const existing = await applicationVariableRepository.findOne({
|
||||
where: { id: entityId, workspaceId },
|
||||
});
|
||||
|
||||
if (
|
||||
update.isSecret !== undefined &&
|
||||
update.isSecret &&
|
||||
existing &&
|
||||
!existing.isSecret
|
||||
) {
|
||||
(update as Record<string, unknown>).value =
|
||||
this.secretEncryptionService.encryptVersioned(existing.value, {
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
update.isSecret !== undefined &&
|
||||
!update.isSecret &&
|
||||
existing &&
|
||||
existing.isSecret
|
||||
) {
|
||||
(update as Record<string, unknown>).value =
|
||||
this.secretEncryptionService.decryptVersioned(existing.value, {
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
await applicationVariableRepository.update(
|
||||
{ id: entityId, workspaceId },
|
||||
update,
|
||||
|
||||
Reference in New Issue
Block a user