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:
@@ -8,6 +8,8 @@ import {
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { type EncryptedString } from 'src/engine/core-modules/secret-encryption/branded-strings/encrypted-string.type';
|
||||
|
||||
@Entity({ name: 'signingKey', schema: 'core' })
|
||||
@Index('IDX_SIGNING_KEY_IS_CURRENT_UNIQUE', ['isCurrent'], {
|
||||
unique: true,
|
||||
@@ -27,7 +29,7 @@ export class SigningKeyEntity {
|
||||
publicKey: string;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
privateKey: string | null;
|
||||
privateKey: EncryptedString | null;
|
||||
|
||||
@Column({ type: 'boolean', default: false })
|
||||
isCurrent: boolean;
|
||||
|
||||
+5
-3
@@ -13,6 +13,8 @@ import {
|
||||
JwtKeyManagerException,
|
||||
JwtKeyManagerExceptionCode,
|
||||
} from 'src/engine/core-modules/jwt/jwt-key-manager.exception';
|
||||
import { type EncryptedString } from 'src/engine/core-modules/secret-encryption/branded-strings/encrypted-string.type';
|
||||
import { type PlaintextString } from 'src/engine/core-modules/secret-encryption/branded-strings/plaintext-string.type';
|
||||
import { SecretEncryptionService } from 'src/engine/core-modules/secret-encryption/secret-encryption.service';
|
||||
|
||||
export type CurrentSigningKey = {
|
||||
@@ -179,7 +181,7 @@ export class JwtKeyManagerService {
|
||||
}
|
||||
|
||||
private decryptPrivateKey(
|
||||
encryptedPrivateKey: string | null,
|
||||
encryptedPrivateKey: EncryptedString | null,
|
||||
id: string,
|
||||
): string {
|
||||
if (!isDefined(encryptedPrivateKey)) {
|
||||
@@ -230,7 +232,7 @@ export class JwtKeyManagerService {
|
||||
}
|
||||
|
||||
private generateEcP256KeyPair(): {
|
||||
privateKeyPem: string;
|
||||
privateKeyPem: PlaintextString;
|
||||
publicKeyPem: string;
|
||||
} {
|
||||
const { privateKey, publicKey } = generateKeyPairSync('ec', {
|
||||
@@ -239,7 +241,7 @@ export class JwtKeyManagerService {
|
||||
|
||||
const privateKeyPem = privateKey
|
||||
.export({ format: 'pem', type: 'pkcs8' })
|
||||
.toString();
|
||||
.toString() as PlaintextString;
|
||||
const publicKeyPem = publicKey
|
||||
.export({ format: 'pem', type: 'spki' })
|
||||
.toString();
|
||||
|
||||
Reference in New Issue
Block a user