Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ dpapi_vault.dat
dpapi_user.key
.secret-broker*

# Unignore source directories that match global gitignore patterns
!broker/src/credentials/
!broker/src/credentials/**
!src/secrets/
!src/secrets/**
!tests/secrets/
!tests/secrets/**

# IDE & OS Files
.DS_Store
Thumbs.db
Expand Down
5 changes: 5 additions & 0 deletions broker/src/credentials/credential-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface CredentialStore {
get(name: string): Promise<string | null>;
set(name: string, value: string): Promise<void>;
delete(name: string): Promise<void>;
}
126 changes: 126 additions & 0 deletions broker/src/credentials/hardware-bound-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { CredentialStore } from './credential-store';
import { HardwareSecurityProvider } from '../security/hardware-provider';
import { HardwareSecurityFactory } from '../security/hardware-factory';
import { BrokerError } from '../protocol';
import * as crypto from 'crypto';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';

export class HardwareBoundStore implements CredentialStore {
private memoryStore = new Map<string, string>();
private readonly hardwareProvider: HardwareSecurityProvider;
private readonly storageFile: string;
private readonly sealedKeyFile: string;

constructor(
customStorageDir?: string,
hardwareProvider?: HardwareSecurityProvider,
) {
this.hardwareProvider =
hardwareProvider || HardwareSecurityFactory.createHardwareProvider();

const baseDir =
customStorageDir || path.join(os.homedir(), '.nest-secret-broker-hsm');
if (!fs.existsSync(baseDir)) {
fs.mkdirSync(baseDir, { recursive: true, mode: 0o700 });
}

this.storageFile = path.join(baseDir, 'hsm_vault.enc');
this.sealedKeyFile = path.join(baseDir, 'hsm_master.sealed');
}

private async getUnsealedMasterKey(): Promise<Buffer> {
if (fs.existsSync(this.sealedKeyFile)) {
const sealedBlob = fs.readFileSync(this.sealedKeyFile);
return await this.hardwareProvider.unsealKey(sealedBlob);
} else {
const rawMasterKey = crypto.randomBytes(32);
const sealedBlob = await this.hardwareProvider.sealKey(rawMasterKey);
fs.writeFileSync(this.sealedKeyFile, sealedBlob, { mode: 0o600 });
return rawMasterKey;
}
}

private async loadFromDisk() {
if (!fs.existsSync(this.storageFile)) return;

try {
const masterKey = await this.getUnsealedMasterKey();
const data = fs.readFileSync(this.storageFile);
if (data.length < 28) return;

const iv = data.subarray(0, 12);
const tag = data.subarray(12, 28);
const ciphertext = data.subarray(28);

const decipher = crypto.createDecipheriv('aes-256-gcm', masterKey, iv);
decipher.setAuthTag(tag);

const decrypted = Buffer.concat([
decipher.update(ciphertext),
decipher.final(),
]);
const json = JSON.parse(decrypted.toString('utf8')) as Record<
string,
string
>;

for (const [k, v] of Object.entries(json)) {
this.memoryStore.set(k, v);
}
} catch (err: unknown) {
const error = err as Error;
throw new BrokerError(
'CREDENTIAL_STORE_ERROR',
`Hardware unseal/decrypt failed: ${error.message}`,
);
}
}

private async saveToDisk() {
try {
const masterKey = await this.getUnsealedMasterKey();
const obj: Record<string, string> = {};
for (const [k, v] of this.memoryStore.entries()) {
obj[k] = v;
}

const plaintext = Buffer.from(JSON.stringify(obj), 'utf8');
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv('aes-256-gcm', masterKey, iv);

const ciphertext = Buffer.concat([
cipher.update(plaintext),
cipher.final(),
]);
const tag = cipher.getAuthTag();

const combined = Buffer.concat([iv, tag, ciphertext]);
fs.writeFileSync(this.storageFile, combined, { mode: 0o600 });
} catch (err: unknown) {
const error = err as Error;
throw new BrokerError(
'CREDENTIAL_STORE_ERROR',
`Failed to write to hardware bound store: ${error.message}`,
);
}
}

async get(name: string): Promise<string | null> {
await this.loadFromDisk();
return this.memoryStore.get(name) || null;
}

async set(name: string, value: string): Promise<void> {
await this.loadFromDisk();
this.memoryStore.set(name, value);
await this.saveToDisk();
}

async delete(name: string): Promise<void> {
await this.loadFromDisk();
this.memoryStore.delete(name);
await this.saveToDisk();
}
}
139 changes: 139 additions & 0 deletions broker/src/credentials/platform-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { CredentialStore } from './credential-store';
import { BrokerError } from '../protocol';
import * as crypto from 'crypto';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';

export class PlatformStore implements CredentialStore {
private memoryStore = new Map<string, string>();
private masterKey: Buffer;
private storageFile: string;

constructor(customStorageDir?: string) {
const runtimeDir =
customStorageDir ||
process.env.XDG_RUNTIME_DIR ||
path.join(
os.tmpdir(),
`.secret-broker-${process.getuid ? process.getuid() : 1000}`,
);

if (!fs.existsSync(runtimeDir)) {
try {
fs.mkdirSync(runtimeDir, { recursive: true, mode: 0o700 });
} catch (err: unknown) {
const error = err as Error;
throw new BrokerError(
'CREDENTIAL_STORE_ERROR',
`Failed to create secure credential storage directory: ${error.message}`,
);
}
}

try {
fs.chmodSync(runtimeDir, 0o700);
} catch {
// Ignore permission errors if not supported
}

this.storageFile = path.join(runtimeDir, 'vault.enc');

const keyFile = path.join(runtimeDir, '.master.key');
if (fs.existsSync(keyFile)) {
try {
this.masterKey = fs.readFileSync(keyFile);
} catch {
this.masterKey = crypto.randomBytes(32);
fs.writeFileSync(keyFile, this.masterKey, { mode: 0o600 });
}
} else {
this.masterKey = crypto.randomBytes(32);
fs.writeFileSync(keyFile, this.masterKey, { mode: 0o600 });
}

this.loadFromDisk();
}

private loadFromDisk() {
if (!fs.existsSync(this.storageFile)) {
return;
}

try {
const data = fs.readFileSync(this.storageFile);
if (data.length < 28) return;
const iv = data.subarray(0, 12);
const tag = data.subarray(12, 28);
const ciphertext = data.subarray(28);

const decipher = crypto.createDecipheriv(
'aes-256-gcm',
this.masterKey,
iv,
);
decipher.setAuthTag(tag);

const decrypted = Buffer.concat([
decipher.update(ciphertext),
decipher.final(),
]);
const json = JSON.parse(decrypted.toString('utf8')) as Record<
string,
string
>;

for (const [k, v] of Object.entries(json)) {
this.memoryStore.set(k, v);
}
} catch {
throw new BrokerError(
'CREDENTIAL_STORE_ERROR',
'Failed to decrypt OS credential store file',
);
}
}

private saveToDisk() {
try {
const obj: Record<string, string> = {};
for (const [k, v] of this.memoryStore.entries()) {
obj[k] = v;
}
const plaintext = Buffer.from(JSON.stringify(obj), 'utf8');
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv('aes-256-gcm', this.masterKey, iv);

const ciphertext = Buffer.concat([
cipher.update(plaintext),
cipher.final(),
]);
const tag = cipher.getAuthTag();

const combined = Buffer.concat([iv, tag, ciphertext]);
fs.writeFileSync(this.storageFile, combined, { mode: 0o600 });
} catch (err: unknown) {
const error = err as Error;
throw new BrokerError(
'CREDENTIAL_STORE_ERROR',
`Failed to write to encrypted credential store: ${error.message}`,
);
}
}

get(name: string): Promise<string | null> {
return Promise.resolve(this.memoryStore.get(name) || null);
}

set(name: string, value: string): Promise<void> {
this.memoryStore.set(name, value);
this.saveToDisk();
return Promise.resolve();
}

delete(name: string): Promise<void> {
this.memoryStore.delete(name);
this.saveToDisk();
return Promise.resolve();
}
}
13 changes: 13 additions & 0 deletions broker/src/credentials/store-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as os from 'os';
import { CredentialStore } from './credential-store';
import { PlatformStore } from './platform-store';
import { WindowsDpapiStore } from './windows-dpapi-store';

export class StoreFactory {
public static createStore(customStorageDir?: string): CredentialStore {
if (os.platform() === 'win32') {
return new WindowsDpapiStore(customStorageDir);
}
return new PlatformStore(customStorageDir);
}
}
Loading
Loading