Files
multi-simadmin/apps/api/src/infrastructure/secrets/keychain-secret-store.test.ts
T

231 lines
8.7 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
MacOSKeychainSecretStore,
SecretStoreError,
SpawnCommandRunner,
parseKeychainReference,
type CommandInvocation,
type CommandResult,
type CommandRunner,
} from './keychain-secret-store.js';
const secret = 's3cr3t with spaces';
class FakeRunner implements CommandRunner {
readonly calls: CommandInvocation[] = [];
readonly stdinSnapshots: (Buffer | undefined)[] = [];
results: (CommandResult | Error)[] = [];
async run(invocation: CommandInvocation): Promise<CommandResult> {
this.calls.push(invocation);
this.stdinSnapshots.push(
invocation.stdin === undefined ? undefined : Buffer.from(invocation.stdin),
);
const result = this.results.shift() ?? { exitCode: 0, stdout: '', stderr: '' };
if (result instanceof Error) throw result;
return result;
}
}
function serialized(value: unknown): string {
if (value instanceof Error) return JSON.stringify({ name: value.name, message: value.message });
return JSON.stringify(value);
}
describe('MacOSKeychainSecretStore', () => {
it('turns an early child exit while writing stdin into a controlled result or rejection', async () => {
const runner = new SpawnCommandRunner();
const outcome = await runner
.run({
executable: '/usr/bin/true',
args: [],
stdin: Buffer.alloc(65_536, 0x78),
env: {},
timeoutMs: 1_000,
maxOutputBytes: 1_024,
})
.catch((error: unknown) => error);
if (outcome instanceof Error) expect(outcome.message).toBe('COMMAND_FAILED');
else if (typeof outcome === 'object' && outcome !== null && 'exitCode' in outcome)
expect((outcome as { exitCode: unknown }).exitCode).toBe(0);
else throw new Error('Unexpected runner outcome');
});
it('sets through absolute security path with the secret only in exact stdin bytes', async () => {
const runner = new FakeRunner();
const store = new MacOSKeychainSecretStore(runner);
const reference = await store.set(
{ instanceId: 'instance.one', purpose: 'instance-password' },
secret,
);
expect(reference).toMatch(/^keychain:\/\/multi-simadmin\//);
expect(parseKeychainReference(reference)).toMatchObject({
service: 'multi-simadmin',
instanceId: 'instance.one',
purpose: 'instance-password',
});
expect(reference).not.toContain(secret);
expect(runner.calls).toHaveLength(1);
expect(runner.calls[0]).toEqual({
executable: '/usr/bin/security',
args: [
'add-generic-password',
'-U',
'-s',
'multi-simadmin',
'-a',
expect.stringMatching(/^[A-Za-z0-9_-]+$/),
'-w',
],
stdin: Buffer.alloc(Buffer.byteLength(secret, 'utf8')),
timeoutMs: 5_000,
maxOutputBytes: 65_536,
env: {},
});
expect(runner.stdinSnapshots[0]).toEqual(Buffer.from(secret, 'utf8'));
expect(runner.calls[0]?.stdin?.every((byte) => byte === 0)).toBe(true);
expect(JSON.stringify(runner.calls[0]?.args)).not.toContain(secret);
expect(JSON.stringify(runner.calls[0]?.env)).not.toContain(secret);
});
it('gets the stdout as the secret and deletes the same strictly parsed reference', async () => {
const runner = new FakeRunner();
const store = new MacOSKeychainSecretStore(runner);
const reference = await store.set(
{ instanceId: 'alpha', purpose: 'instance-password' },
'initial',
);
runner.calls.splice(0);
runner.results.push({ exitCode: 0, stdout: `${secret}\n`, stderr: '' });
expect(await store.get(reference)).toBe(secret);
runner.results.push({ exitCode: 0, stdout: '', stderr: '' });
expect(await store.delete(reference)).toBe(true);
expect(runner.calls.map(({ args }) => args[0])).toEqual([
'find-generic-password',
'delete-generic-password',
]);
expect(runner.calls[0]?.args).toContain('-w');
expect(runner.calls[1]?.args).not.toContain('-w');
});
it('maps keychain not-found to undefined/false', async () => {
const runner = new FakeRunner();
const store = new MacOSKeychainSecretStore(runner);
const reference = await store.set({ instanceId: 'alpha', purpose: 'instance-password' }, 'x');
runner.results.push({
exitCode: 44,
stdout: '',
stderr: 'security: SecKeychainSearchCopyNext: -25300',
});
expect(await store.get(reference)).toBeUndefined();
runner.results.push({
exitCode: 44,
stdout: '',
stderr: 'The specified item could not be found in the keychain. (-25300)',
});
expect(await store.delete(reference)).toBe(false);
});
it('never maps a set failure or forged item-not-found text to success', async () => {
const runner = new FakeRunner();
const store = new MacOSKeychainSecretStore(runner);
runner.results.push({ exitCode: 44, stdout: '', stderr: 'security: -25300' });
await expect(
store.set({ instanceId: 'alpha', purpose: 'instance-password' }, secret),
).rejects.toMatchObject({ code: 'COMMAND_FAILED' });
runner.results.push({ exitCode: 1, stdout: '', stderr: 'unrelated failure mentioning -25300' });
const reference = await new MacOSKeychainSecretStore(new FakeRunner()).set(
{ instanceId: 'alpha', purpose: 'password' },
'safe',
);
await expect(store.get(reference)).rejects.toMatchObject({ code: 'COMMAND_FAILED' });
for (const forged of [
'security: x-25300oops',
'security: --25300',
'security: -25300 trailing',
]) {
runner.results.push({ exitCode: 44, stdout: '', stderr: forged });
await expect(store.get(reference)).rejects.toMatchObject({ code: 'COMMAND_FAILED' });
}
});
it.each(['line\nbreak', 'line\rbreak', 'nul\0break', 'x'.repeat(4097)])(
'rejects non-line-safe or oversized secret input before invoking the runner',
async (value) => {
const runner = new FakeRunner();
const store = new MacOSKeychainSecretStore(runner);
await expect(
store.set({ instanceId: 'alpha', purpose: 'instance-password' }, value),
).rejects.toMatchObject({ code: 'INVALID_SECRET' });
expect(runner.calls).toHaveLength(0);
},
);
it('maps runner timeout and malicious command output to stable redacted errors', async () => {
const runner = new FakeRunner();
const store = new MacOSKeychainSecretStore(runner);
runner.results.push(new Error(`timeout ${secret}`));
const timeout = await store
.set({ instanceId: 'alpha', purpose: 'instance-password' }, secret)
.catch((error: unknown) => error);
expect(timeout).toBeInstanceOf(SecretStoreError);
expect(timeout).toMatchObject({
code: 'COMMAND_TIMEOUT',
message: 'Keychain command timed out',
});
expect(serialized(timeout)).not.toContain(secret);
runner.results.push({ exitCode: 1, stdout: secret, stderr: `malicious ${secret}` });
const failed = await store
.set({ instanceId: 'alpha', purpose: 'instance-password' }, secret)
.catch((error: unknown) => error);
expect(failed).toMatchObject({ code: 'COMMAND_FAILED' });
expect(serialized(failed)).not.toContain(secret);
});
it('maps oversized stderr to a stable output-limit error without retaining output', async () => {
const runner = new FakeRunner();
const store = new MacOSKeychainSecretStore(runner);
runner.results.push(new Error(`STDERR_LIMIT ${secret}`));
const error = await store
.set({ instanceId: 'alpha', purpose: 'instance-password' }, secret)
.catch((reason: unknown) => reason);
expect(error).toMatchObject({ code: 'OUTPUT_LIMIT' });
expect(serialized(error)).not.toContain(secret);
});
it.each(['', ' ', 'a/b', 'line\nbreak', 'a'.repeat(129)])(
'rejects unsafe instance id %j before invoking the runner',
async (instanceId) => {
const runner = new FakeRunner();
const store = new MacOSKeychainSecretStore(runner);
await expect(
store.set({ instanceId, purpose: 'instance-password' }, 'x'),
).rejects.toMatchObject({
code: 'INVALID_KEY',
});
expect(runner.calls).toHaveLength(0);
},
);
it('strictly parses references and rejects cross-service or injected variants', async () => {
const runner = new FakeRunner();
const store = new MacOSKeychainSecretStore(runner);
const reference = await store.set({ instanceId: 'alpha', purpose: 'instance-password' }, 'x');
expect(parseKeychainReference(reference)).toMatchObject({ service: 'multi-simadmin' });
for (const invalid of [
'keychain://other/account',
'keychain://multi-simadmin/account/extra',
'keychain://multi-simadmin/%0Aevil',
'https://multi-simadmin/account',
`${reference}?query=bad`,
]) {
expect(() => parseKeychainReference(invalid)).toThrowError(SecretStoreError);
}
});
});