From 7d1974184df9cdedebe88b0f898acc2e7a0ce382 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 21:27:23 +0900 Subject: [PATCH 1/5] test(calendar): specify encrypted credential store boundary --- ...lendar-encrypted-file-secret-store.test.ts | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 apps/integration-calendar-service/src/calendar-encrypted-file-secret-store.test.ts diff --git a/apps/integration-calendar-service/src/calendar-encrypted-file-secret-store.test.ts b/apps/integration-calendar-service/src/calendar-encrypted-file-secret-store.test.ts new file mode 100644 index 00000000..974fba16 --- /dev/null +++ b/apps/integration-calendar-service/src/calendar-encrypted-file-secret-store.test.ts @@ -0,0 +1,174 @@ +import { mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, describe, expect, it } from 'vitest'; +import { + CalendarEncryptedFileSecretStore, + CalendarEncryptedFileSecretStoreError, + createCalendarEncryptedFileSecretStoreFromEnvironment, +} from './calendar-encrypted-file-secret-store'; + +const BASE64_KEY = Buffer.alloc(32, 0x2a).toString('base64'); +const OTHER_BASE64_KEY = Buffer.alloc(32, 0x17).toString('base64'); +const CONNECTION_ID = '11111111-1111-4111-8111-111111111111'; +const WORKSPACE_ID = '22222222-2222-4222-8222-222222222222'; +const USER_ID = '33333333-3333-4333-8333-333333333333'; + +const directories: string[] = []; + +async function temporaryDirectory(): Promise { + const directory = await mkdtemp(join(tmpdir(), 'life-os-calendar-secret-')); + directories.push(directory); + return directory; +} + +afterEach(async () => { + await Promise.all( + directories.splice(0).map(async (directory) => + rm(directory, { recursive: true, force: true }), + ), + ); +}); + +describe('CalendarEncryptedFileSecretStore', () => { + it('encrypts credentials at rest and round-trips only through an opaque handle', async () => { + const directory = await temporaryDirectory(); + const store = new CalendarEncryptedFileSecretStore(directory, BASE64_KEY); + const secret = 'calendar-access-token-value'; + + const handle = await store.writeSecret({ + connectionId: CONNECTION_ID, + workspaceId: WORKSPACE_ID, + userId: USER_ID, + credentialKind: 'access', + secretValue: secret, + }); + + expect(handle).toMatch( + /^lifeos-calendar-secret:\/\/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u, + ); + const secretId = handle.slice('lifeos-calendar-secret://'.length); + const path = join(directory, `${secretId}.json`); + const encoded = await readFile(path, 'utf8'); + expect(encoded).not.toContain(secret); + expect((await stat(path)).mode & 0o077).toBe(0); + await expect(store.readSecret(handle)).resolves.toBe(secret); + }); + + it('binds ciphertext to its opaque handle so file swapping fails closed', async () => { + const directory = await temporaryDirectory(); + const store = new CalendarEncryptedFileSecretStore(directory, BASE64_KEY); + const first = await store.writeSecret({ + connectionId: CONNECTION_ID, + workspaceId: WORKSPACE_ID, + userId: USER_ID, + credentialKind: 'access', + secretValue: 'first-token', + }); + const second = await store.writeSecret({ + connectionId: CONNECTION_ID, + workspaceId: WORKSPACE_ID, + userId: USER_ID, + credentialKind: 'refresh', + secretValue: 'second-token', + }); + const firstId = first.slice('lifeos-calendar-secret://'.length); + const secondId = second.slice('lifeos-calendar-secret://'.length); + const firstPath = join(directory, `${firstId}.json`); + const secondPath = join(directory, `${secondId}.json`); + + await writeFile(firstPath, await readFile(secondPath), { mode: 0o600 }); + + await expect(store.readSecret(first)).rejects.toBeInstanceOf( + CalendarEncryptedFileSecretStoreError, + ); + }); + + it('rejects tampering and a different master key without exposing crypto details', async () => { + const directory = await temporaryDirectory(); + const store = new CalendarEncryptedFileSecretStore(directory, BASE64_KEY); + const handle = await store.writeSecret({ + connectionId: CONNECTION_ID, + workspaceId: WORKSPACE_ID, + userId: USER_ID, + credentialKind: 'access', + secretValue: 'token-to-protect', + }); + const secretId = handle.slice('lifeos-calendar-secret://'.length); + const path = join(directory, `${secretId}.json`); + const payload = JSON.parse(await readFile(path, 'utf8')) as { + ciphertext: string; + }; + payload.ciphertext = `${payload.ciphertext.slice(0, -2)}AA`; + await writeFile(path, JSON.stringify(payload), { mode: 0o600 }); + + await expect(store.readSecret(handle)).rejects.toMatchObject({ + name: 'CalendarEncryptedFileSecretStoreError', + message: 'Calendar encrypted secret storage is unavailable', + }); + + const secondDirectory = await temporaryDirectory(); + const writer = new CalendarEncryptedFileSecretStore( + secondDirectory, + BASE64_KEY, + ); + const secondHandle = await writer.writeSecret({ + connectionId: CONNECTION_ID, + workspaceId: WORKSPACE_ID, + userId: USER_ID, + credentialKind: 'access', + secretValue: 'another-token', + }); + const wrongKeyReader = new CalendarEncryptedFileSecretStore( + secondDirectory, + OTHER_BASE64_KEY, + ); + await expect(wrongKeyReader.readSecret(secondHandle)).rejects.toBeInstanceOf( + CalendarEncryptedFileSecretStoreError, + ); + }); + + it('deletes idempotently and rejects malformed handles without path traversal', async () => { + const directory = await temporaryDirectory(); + const store = new CalendarEncryptedFileSecretStore(directory, BASE64_KEY); + const handle = await store.writeSecret({ + connectionId: CONNECTION_ID, + workspaceId: WORKSPACE_ID, + userId: USER_ID, + credentialKind: 'access', + secretValue: 'deletable-token', + }); + + await expect(store.deleteSecret(handle)).resolves.toBeUndefined(); + await expect(store.deleteSecret(handle)).resolves.toBeUndefined(); + await expect(store.readSecret(handle)).rejects.toBeInstanceOf( + CalendarEncryptedFileSecretStoreError, + ); + await expect( + store.readSecret('lifeos-calendar-secret://../../etc/passwd'), + ).rejects.toBeInstanceOf(CalendarEncryptedFileSecretStoreError); + }); + + it('fails closed on malformed environment configuration and accepts a canonical 256-bit key', async () => { + const directory = await temporaryDirectory(); + expect(() => + createCalendarEncryptedFileSecretStoreFromEnvironment({ + CALENDAR_SECRET_STORE_DIRECTORY: directory, + CALENDAR_SECRET_STORE_KEY: 'not-a-256-bit-key', + }), + ).toThrow(CalendarEncryptedFileSecretStoreError); + + const store = createCalendarEncryptedFileSecretStoreFromEnvironment({ + CALENDAR_SECRET_STORE_DIRECTORY: directory, + CALENDAR_SECRET_STORE_KEY: BASE64_KEY, + }); + const handle = await store.writeSecret({ + connectionId: CONNECTION_ID, + workspaceId: WORKSPACE_ID, + userId: USER_ID, + credentialKind: 'access', + secretValue: 'configured-token', + }); + await expect(store.readSecret(handle)).resolves.toBe('configured-token'); + }); +}); From 0448e1e3feb83029c7b08014a73aa53f0660c946 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 21:56:42 +0900 Subject: [PATCH 2/5] feat(calendar): implement encrypted credential store --- .../calendar-encrypted-file-secret-store.ts | 398 ++++++++++++++++++ 1 file changed, 398 insertions(+) create mode 100644 apps/integration-calendar-service/src/calendar-encrypted-file-secret-store.ts diff --git a/apps/integration-calendar-service/src/calendar-encrypted-file-secret-store.ts b/apps/integration-calendar-service/src/calendar-encrypted-file-secret-store.ts new file mode 100644 index 00000000..ba934bca --- /dev/null +++ b/apps/integration-calendar-service/src/calendar-encrypted-file-secret-store.ts @@ -0,0 +1,398 @@ +import { + createCipheriv, + createDecipheriv, + randomBytes, + randomUUID, +} from 'node:crypto'; +import { chmod, lstat, mkdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import type { + CalendarConnectionCredentialStore, + CalendarConnectionCredentialWrite, +} from './calendar-connection-create'; +import type { CalendarCredentialSecretStore } from './calendar-credential-materializer'; + +const HANDLE_PREFIX = 'lifeos-calendar-secret://'; +const UUID_V4_PATTERN = + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu; +const CANONICAL_256_BIT_KEY_PATTERN = /^[A-Za-z0-9+/]{43}=$/u; +const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/u; +const MAXIMUM_DIRECTORY_LENGTH = 4_096; +const MAXIMUM_SECRET_LENGTH = 16_384; +const MAXIMUM_ENVELOPE_BYTES = 65_536; +const MAXIMUM_WRITE_ATTEMPTS = 4; +const IV_BYTES = 12; +const AUTH_TAG_BYTES = 16; + +interface CalendarEncryptedSecretPayload { + readonly schemaVersion: 1; + readonly connectionId: string; + readonly workspaceId: string; + readonly userId: string; + readonly credentialKind: 'access' | 'refresh'; + readonly secretValue: string; +} + +interface CalendarEncryptedSecretEnvelope { + readonly schemaVersion: 1; + readonly algorithm: 'aes-256-gcm'; + readonly iv: string; + readonly ciphertext: string; + readonly authTag: string; +} + +/** + * Fixed, credential-free failure returned for every encrypted secret-store + * validation, filesystem, parsing, or cryptographic error. + */ +export class CalendarEncryptedFileSecretStoreError extends Error { + /** Creates the only externally observable failure for this storage adapter. */ + constructor() { + super('Calendar encrypted secret storage is unavailable'); + this.name = 'CalendarEncryptedFileSecretStoreError'; + } +} + +function unavailable(): never { + throw new CalendarEncryptedFileSecretStoreError(); +} + +function requireDirectory(value: unknown): string { + if ( + typeof value !== 'string' || + value.length === 0 || + value.length > MAXIMUM_DIRECTORY_LENGTH || + CONTROL_CHARACTER_PATTERN.test(value) + ) { + return unavailable(); + } + return value; +} + +function requireUuidV4(value: unknown): string { + if (typeof value !== 'string' || !UUID_V4_PATTERN.test(value)) { + return unavailable(); + } + return value.toLowerCase(); +} + +function requireSecret(value: unknown): string { + if ( + typeof value !== 'string' || + value.length === 0 || + value.length > MAXIMUM_SECRET_LENGTH || + CONTROL_CHARACTER_PATTERN.test(value) + ) { + return unavailable(); + } + return value; +} + +function requireCredentialKind( + value: unknown, +): 'access' | 'refresh' { + if (value !== 'access' && value !== 'refresh') { + return unavailable(); + } + return value; +} + +function requireMasterKey(value: unknown): Buffer { + if ( + typeof value !== 'string' || + !CANONICAL_256_BIT_KEY_PATTERN.test(value) + ) { + return unavailable(); + } + const decoded = Buffer.from(value, 'base64'); + if (decoded.length !== 32 || decoded.toString('base64') !== value) { + decoded.fill(0); + return unavailable(); + } + return decoded; +} + +function requireCanonicalBase64( + value: unknown, + expectedBytes?: number, +): Buffer { + if ( + typeof value !== 'string' || + value.length === 0 || + value.length > MAXIMUM_ENVELOPE_BYTES * 2 + ) { + return unavailable(); + } + const decoded = Buffer.from(value, 'base64'); + if ( + decoded.length === 0 || + decoded.length > MAXIMUM_ENVELOPE_BYTES || + decoded.toString('base64') !== value || + (expectedBytes !== undefined && decoded.length !== expectedBytes) + ) { + decoded.fill(0); + return unavailable(); + } + return decoded; +} + +function parseHandle(value: unknown): { readonly handle: string; readonly id: string } { + if ( + typeof value !== 'string' || + !value.startsWith(HANDLE_PREFIX) + ) { + return unavailable(); + } + const rawId = value.slice(HANDLE_PREFIX.length); + const id = requireUuidV4(rawId); + const handle = `${HANDLE_PREFIX}${id}`; + if (value.toLowerCase() !== handle) { + return unavailable(); + } + return Object.freeze({ handle, id }); +} + +function normalizeWrite( + input: CalendarConnectionCredentialWrite, +): CalendarEncryptedSecretPayload { + if (!input || typeof input !== 'object') { + return unavailable(); + } + return Object.freeze({ + schemaVersion: 1, + connectionId: requireUuidV4(input.connectionId), + workspaceId: requireUuidV4(input.workspaceId), + userId: requireUuidV4(input.userId), + credentialKind: requireCredentialKind(input.credentialKind), + secretValue: requireSecret(input.secretValue), + }); +} + +function requireEnvelope(value: unknown): CalendarEncryptedSecretEnvelope { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return unavailable(); + } + const candidate = value as Partial; + if ( + candidate.schemaVersion !== 1 || + candidate.algorithm !== 'aes-256-gcm' || + typeof candidate.iv !== 'string' || + typeof candidate.ciphertext !== 'string' || + typeof candidate.authTag !== 'string' + ) { + return unavailable(); + } + return Object.freeze({ + schemaVersion: 1, + algorithm: 'aes-256-gcm', + iv: candidate.iv, + ciphertext: candidate.ciphertext, + authTag: candidate.authTag, + }); +} + +function requirePayload(value: unknown): CalendarEncryptedSecretPayload { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return unavailable(); + } + const candidate = value as Partial; + if (candidate.schemaVersion !== 1) { + return unavailable(); + } + return Object.freeze({ + schemaVersion: 1, + connectionId: requireUuidV4(candidate.connectionId), + workspaceId: requireUuidV4(candidate.workspaceId), + userId: requireUuidV4(candidate.userId), + credentialKind: requireCredentialKind(candidate.credentialKind), + secretValue: requireSecret(candidate.secretValue), + }); +} + +function isErrnoCode(error: unknown, code: string): boolean { + return ( + !!error && + typeof error === 'object' && + 'code' in error && + (error as { readonly code?: unknown }).code === code + ); +} + +/** + * Self-hostable AES-256-GCM credential store for Calendar-owned secrets. + * + * Files contain only authenticated ciphertext envelopes. The opaque UUIDv4 + * handle is authenticated as GCM additional data, so moving ciphertext to a + * different handle fails closed. This adapter implements both the connection + * creation write/delete port and the credential materializer read port. + */ +export class CalendarEncryptedFileSecretStore + implements CalendarConnectionCredentialStore, CalendarCredentialSecretStore +{ + private readonly directory: string; + private readonly masterKey: Buffer; + + /** Creates a store rooted at an operator-owned directory with one 256-bit key. */ + constructor(directory: string, canonicalBase64Key: string) { + this.directory = requireDirectory(directory); + this.masterKey = requireMasterKey(canonicalBase64Key); + } + + private async ensureDirectory(): Promise { + await mkdir(this.directory, { recursive: true, mode: 0o700 }); + await chmod(this.directory, 0o700); + } + + private pathForId(id: string): string { + return join(this.directory, `${id}.json`); + } + + /** + * Encrypts one bounded credential and returns only a newly allocated opaque + * handle. Existing files are never overwritten, including on UUID collision. + */ + async writeSecret(input: CalendarConnectionCredentialWrite): Promise { + try { + const safe = normalizeWrite(input); + await this.ensureDirectory(); + + for (let attempt = 0; attempt < MAXIMUM_WRITE_ATTEMPTS; attempt += 1) { + const id = randomUUID(); + const handle = `${HANDLE_PREFIX}${id}`; + const iv = randomBytes(IV_BYTES); + const cipher = createCipheriv('aes-256-gcm', this.masterKey, iv, { + authTagLength: AUTH_TAG_BYTES, + }); + cipher.setAAD(Buffer.from(handle, 'utf8')); + + const plaintext = Buffer.from(JSON.stringify(safe), 'utf8'); + let ciphertext: Buffer; + let authTag: Buffer; + try { + ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]); + authTag = cipher.getAuthTag(); + } finally { + plaintext.fill(0); + } + + const envelope: CalendarEncryptedSecretEnvelope = Object.freeze({ + schemaVersion: 1, + algorithm: 'aes-256-gcm', + iv: iv.toString('base64'), + ciphertext: ciphertext.toString('base64'), + authTag: authTag.toString('base64'), + }); + iv.fill(0); + ciphertext.fill(0); + authTag.fill(0); + + try { + await writeFile(this.pathForId(id), JSON.stringify(envelope), { + encoding: 'utf8', + flag: 'wx', + mode: 0o600, + }); + return handle; + } catch (error) { + if (isErrnoCode(error, 'EEXIST')) { + continue; + } + return unavailable(); + } + } + return unavailable(); + } catch { + return unavailable(); + } + } + + /** + * Reads and authenticates one exact opaque handle, returning plaintext only + * to the internal credential-materialization boundary. + */ + async readSecret(secretHandle: string): Promise { + try { + const parsed = parseHandle(secretHandle); + const path = this.pathForId(parsed.id); + const file = await lstat(path); + if ( + !file.isFile() || + file.isSymbolicLink() || + file.size <= 0 || + file.size > MAXIMUM_ENVELOPE_BYTES + ) { + return unavailable(); + } + + const encoded = await readFile(path, 'utf8'); + if (Buffer.byteLength(encoded, 'utf8') !== file.size) { + return unavailable(); + } + const envelope = requireEnvelope(JSON.parse(encoded) as unknown); + const iv = requireCanonicalBase64(envelope.iv, IV_BYTES); + const ciphertext = requireCanonicalBase64(envelope.ciphertext); + const authTag = requireCanonicalBase64(envelope.authTag, AUTH_TAG_BYTES); + + const decipher = createDecipheriv('aes-256-gcm', this.masterKey, iv, { + authTagLength: AUTH_TAG_BYTES, + }); + decipher.setAAD(Buffer.from(parsed.handle, 'utf8')); + decipher.setAuthTag(authTag); + let plaintext: Buffer; + try { + plaintext = Buffer.concat([ + decipher.update(ciphertext), + decipher.final(), + ]); + } finally { + iv.fill(0); + ciphertext.fill(0); + authTag.fill(0); + } + + try { + if (plaintext.length === 0 || plaintext.length > MAXIMUM_ENVELOPE_BYTES) { + return unavailable(); + } + const payload = requirePayload( + JSON.parse(plaintext.toString('utf8')) as unknown, + ); + return payload.secretValue; + } finally { + plaintext.fill(0); + } + } catch { + return unavailable(); + } + } + + /** Deletes one exact handle idempotently; malformed handles fail closed. */ + async deleteSecret(secretHandle: string): Promise { + try { + const parsed = parseHandle(secretHandle); + await rm(this.pathForId(parsed.id), { force: true }); + } catch { + return unavailable(); + } + } +} + +/** + * Builds the encrypted Calendar secret store from explicit deployment + * configuration. Missing or malformed configuration never falls back to + * plaintext persistence or a generated process-local key. + */ +export function createCalendarEncryptedFileSecretStoreFromEnvironment( + environment: NodeJS.ProcessEnv = process.env, +): CalendarEncryptedFileSecretStore { + try { + return new CalendarEncryptedFileSecretStore( + requireDirectory(environment.CALENDAR_SECRET_STORE_DIRECTORY), + typeof environment.CALENDAR_SECRET_STORE_KEY === 'string' + ? environment.CALENDAR_SECRET_STORE_KEY + : unavailable(), + ); + } catch { + return unavailable(); + } +} From 63256ff3c40a93ec361366c1fae82a70c2b31cac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 21:59:23 +0900 Subject: [PATCH 3/5] fix(calendar): harden bounded encrypted secret reads --- .../calendar-encrypted-file-secret-store.ts | 53 ++++++++++++------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/apps/integration-calendar-service/src/calendar-encrypted-file-secret-store.ts b/apps/integration-calendar-service/src/calendar-encrypted-file-secret-store.ts index ba934bca..cc9e466b 100644 --- a/apps/integration-calendar-service/src/calendar-encrypted-file-secret-store.ts +++ b/apps/integration-calendar-service/src/calendar-encrypted-file-secret-store.ts @@ -4,7 +4,7 @@ import { randomBytes, randomUUID, } from 'node:crypto'; -import { chmod, lstat, mkdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { chmod, lstat, mkdir, open, rm, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import type { CalendarConnectionCredentialStore, @@ -88,9 +88,7 @@ function requireSecret(value: unknown): string { return value; } -function requireCredentialKind( - value: unknown, -): 'access' | 'refresh' { +function requireCredentialKind(value: unknown): 'access' | 'refresh' { if (value !== 'access' && value !== 'refresh') { return unavailable(); } @@ -136,17 +134,16 @@ function requireCanonicalBase64( return decoded; } -function parseHandle(value: unknown): { readonly handle: string; readonly id: string } { - if ( - typeof value !== 'string' || - !value.startsWith(HANDLE_PREFIX) - ) { +function parseHandle( + value: unknown, +): { readonly handle: string; readonly id: string } { + if (typeof value !== 'string' || !value.startsWith(HANDLE_PREFIX)) { return unavailable(); } const rawId = value.slice(HANDLE_PREFIX.length); const id = requireUuidV4(rawId); const handle = `${HANDLE_PREFIX}${id}`; - if (value.toLowerCase() !== handle) { + if (value !== handle) { return unavailable(); } return Object.freeze({ handle, id }); @@ -308,26 +305,44 @@ export class CalendarEncryptedFileSecretStore /** * Reads and authenticates one exact opaque handle, returning plaintext only - * to the internal credential-materialization boundary. + * to the internal credential-materialization boundary. The opened file is + * identity-checked against its preceding lstat and size-bounded before read. */ async readSecret(secretHandle: string): Promise { try { const parsed = parseHandle(secretHandle); const path = this.pathForId(parsed.id); - const file = await lstat(path); + const before = await lstat(path); if ( - !file.isFile() || - file.isSymbolicLink() || - file.size <= 0 || - file.size > MAXIMUM_ENVELOPE_BYTES + !before.isFile() || + before.isSymbolicLink() || + before.size <= 0 || + before.size > MAXIMUM_ENVELOPE_BYTES ) { return unavailable(); } - const encoded = await readFile(path, 'utf8'); - if (Buffer.byteLength(encoded, 'utf8') !== file.size) { - return unavailable(); + const file = await open(path, 'r'); + let encoded: string; + try { + const opened = await file.stat(); + if ( + !opened.isFile() || + opened.size <= 0 || + opened.size > MAXIMUM_ENVELOPE_BYTES || + opened.dev !== before.dev || + opened.ino !== before.ino + ) { + return unavailable(); + } + encoded = await file.readFile({ encoding: 'utf8' }); + if (Buffer.byteLength(encoded, 'utf8') !== opened.size) { + return unavailable(); + } + } finally { + await file.close(); } + const envelope = requireEnvelope(JSON.parse(encoded) as unknown); const iv = requireCanonicalBase64(envelope.iv, IV_BYTES); const ciphertext = requireCanonicalBase64(envelope.ciphertext); From b3f2a21fa923ff4ee1767d13146b0b22eb2db741 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 23:13:35 +0900 Subject: [PATCH 4/5] test(calendar): exercise authenticated tamper and delete failures --- .../src/calendar-encrypted-file-secret-store.test.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/integration-calendar-service/src/calendar-encrypted-file-secret-store.test.ts b/apps/integration-calendar-service/src/calendar-encrypted-file-secret-store.test.ts index 974fba16..43afcf3a 100644 --- a/apps/integration-calendar-service/src/calendar-encrypted-file-secret-store.test.ts +++ b/apps/integration-calendar-service/src/calendar-encrypted-file-secret-store.test.ts @@ -99,7 +99,9 @@ describe('CalendarEncryptedFileSecretStore', () => { const payload = JSON.parse(await readFile(path, 'utf8')) as { ciphertext: string; }; - payload.ciphertext = `${payload.ciphertext.slice(0, -2)}AA`; + const ciphertextBytes = Buffer.from(payload.ciphertext, 'base64'); + ciphertextBytes[0] ^= 0x01; + payload.ciphertext = ciphertextBytes.toString('base64'); await writeFile(path, JSON.stringify(payload), { mode: 0o600 }); await expect(store.readSecret(handle)).rejects.toMatchObject({ @@ -147,6 +149,12 @@ describe('CalendarEncryptedFileSecretStore', () => { await expect( store.readSecret('lifeos-calendar-secret://../../etc/passwd'), ).rejects.toBeInstanceOf(CalendarEncryptedFileSecretStoreError); + await expect( + store.deleteSecret('lifeos-calendar-secret://../../etc/passwd'), + ).rejects.toBeInstanceOf(CalendarEncryptedFileSecretStoreError); + await expect(store.deleteSecret('not-a-handle')).rejects.toBeInstanceOf( + CalendarEncryptedFileSecretStoreError, + ); }); it('fails closed on malformed environment configuration and accepts a canonical 256-bit key', async () => { From d84cd3c5a522596c9b0c88b2ebd81cf10dff6736 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 23:51:41 +0900 Subject: [PATCH 5/5] fix(calendar): make tamper test type-safe --- .../src/calendar-encrypted-file-secret-store.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/integration-calendar-service/src/calendar-encrypted-file-secret-store.test.ts b/apps/integration-calendar-service/src/calendar-encrypted-file-secret-store.test.ts index 43afcf3a..d857b223 100644 --- a/apps/integration-calendar-service/src/calendar-encrypted-file-secret-store.test.ts +++ b/apps/integration-calendar-service/src/calendar-encrypted-file-secret-store.test.ts @@ -100,7 +100,7 @@ describe('CalendarEncryptedFileSecretStore', () => { ciphertext: string; }; const ciphertextBytes = Buffer.from(payload.ciphertext, 'base64'); - ciphertextBytes[0] ^= 0x01; + ciphertextBytes[0] = ciphertextBytes.readUInt8(0) ^ 0x01; payload.ciphertext = ciphertextBytes.toString('base64'); await writeFile(path, JSON.stringify(payload), { mode: 0o600 });