Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export abstract class AbstractEncryptedStorage {
abstract initialize(): Promise<void>;

abstract save<T extends Record<string, any>>(
key: string,
data: T
): Promise<void>;

abstract load<T extends Record<string, any>>(key: string): Promise<T>;

abstract delete(key: string): Promise<void>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { safeStorage } from "electron";
import { IEncryptedStorage } from "./IEncryptedStorage";

export class EncryptedFsStorageService implements IEncryptedStorage {
constructor(private readonly baseFolderPath: string) {}

async initialize(): Promise<void> {
if (!safeStorage.isEncryptionAvailable()) {
// Show trouble shooting steps to user
throw new Error("Encryption is not available on this system. ");
}

// initialize directories
}

async save<T extends Record<string, any>>(
key: string,
data: T
): Promise<void> {
// encrypted
}

async load<T extends Record<string, any>>(key: string): Promise<T> {}

async delete(key: string): Promise<void> {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { SecretProviderConfig, SecretProviderType } from "../types";
import { AbstractEncryptedStorage } from "../encryptedStorage/AbstractEncryptedStorage";
import { AbstractSecretProvider } from "../providerService/AbstractSecretProvider";

export interface ProvidersManifest {
version: string;
providers: {
id: string;
storagePath: string;
type: SecretProviderType;
}[];
}

export abstract class AbstractProviderRegistry {
protected encryptedStorage: AbstractEncryptedStorage;

protected providers: Map<string, AbstractSecretProvider> = new Map();

constructor(encryptedStorage: AbstractEncryptedStorage) {
this.encryptedStorage = encryptedStorage;
}

abstract initialize(): Promise<void>;

protected abstract createProviderInstance(
config: SecretProviderConfig
): AbstractSecretProvider;

protected abstract loadManifest(): Promise<ProvidersManifest>;

protected abstract saveManifest(manifest: ProvidersManifest): Promise<void>;

abstract getAllProviderConfigs(): Promise<SecretProviderConfig[]>;

abstract getProviderConfig(id: string): Promise<SecretProviderConfig | null>;

abstract setProviderConfig(config: SecretProviderConfig): Promise<void>;

abstract deleteProviderConfig(id: string): Promise<void>;

abstract getProvider(providerId: string): AbstractSecretProvider | null;
}
112 changes: 112 additions & 0 deletions src/lib/secretsManager/providerRegistry/FsProviderRegistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import * as fs from "fs";
import * as path from "path";
import { SecretProviderConfig, SecretProviderType } from "../types";
import { createProvider } from "../providerService/providerFactory";
import { AbstractProviderRegistry } from "./AbstractProviderRegistry";
import { AbstractEncryptedStorage } from "../encryptedStorage/AbstractEncryptedStorage";
import { AbstractSecretProvider } from "../providerService/AbstractSecretProvider";

const MANIFEST_FILENAME = "providers.json";



// Functions
// 1. initialize registry (create config dir if not exists)
// 2. list providers
// 3.

export class FileBasedProviderRegistry extends AbstractProviderRegistry {
private manifestPath: string;

private configDir: string;

protected providers: Map<string, AbstractSecretProvider> = new Map();

constructor(encryptedStorage: AbstractEncryptedStorage, configDir: string) {
super(encryptedStorage);
this.configDir = configDir;
this.manifestPath = path.join(configDir, MANIFEST_FILENAME);
}

getProvider(providerId: string): AbstractSecretProvider | null {
return this.providers.get(providerId) || null;
}

async initialize(): Promise<void> {
await this.ensureConfigDir();
this.initProvidersFromManifest();
}

private async initProvidersFromManifest() {
const configs = await this.getAllProviderConfigs();
configs.forEach((config) => {
this.providers.set(config.id, this.createProviderInstance(config));
});
}

async getAllProviderConfigs(): Promise<SecretProviderConfig[]> {
const manifest = await this.loadManifest();
const configs: SecretProviderConfig[] = [];

for (const entry of manifest.providers) {
const config = await this.encryptedStorage.load<SecretProviderConfig>(
entry.id
);
configs.push(config);
}

return configs;
}

async setProviderConfig(config: SecretProviderConfig): Promise<void> {
const manifest = await this.loadManifest();
const storageKey = config.id;

// do atomic write
await this.encryptedStorage.save(storageKey, config);

// Update manifest

await this.saveManifest(manifest);
this.providers.set(config.id, this.createProviderInstance(config));
}

async deleteProviderConfig(id: string): Promise<void> {
const manifest = await this.loadManifest();
const entry = manifest.providers.find((p) => p.id === id);
if (!entry) return;

await this.encryptedStorage.delete(id);

manifest.providers = manifest.providers.filter((p) => p.id !== id);
await this.saveManifest(manifest);
this.providers.delete(id);
}

async getProviderConfig(id: string): Promise<SecretProviderConfig | null> {
const manifest = await this.loadManifest();
const entry = manifest.providers.find((p) => p.id === id);
if (!entry) return null;

return this.encryptedStorage.load<SecretProviderConfig>(id);
}

private async ensureConfigDir(): Promise<void> {
try {
await fs.mkdir(this.configDir, { recursive: true });
} catch (error) {
console.error("Failed to create config directory:", error);
}
}

protected async loadManifest(): Promise<ProvidersManifest> {}

protected async saveManifest(manifest: ProvidersManifest): Promise<void> {}

// eslint-disable-next-line class-methods-use-this
protected createProviderInstance(
config: SecretProviderConfig
): AbstractSecretProvider {
return createProvider(config);
}
}
27 changes: 27 additions & 0 deletions src/lib/secretsManager/providerService/AbstractSecretProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { CachedSecret, SecretProviderType, SecretReference } from "../types";

export abstract class AbstractSecretProvider {
protected cache: Map<string, CachedSecret> = new Map();

abstract readonly type: SecretProviderType;

abstract readonly id: string;

protected config: any;

protected abstract getSecretIdentfier(ref: SecretReference): string;

abstract testConnection(): Promise<boolean>;

abstract getSecret(ref: SecretReference): Promise<string>;

abstract getSecrets(): Promise<string[]>;

abstract setSecret(): Promise<void>;

abstract setSecrets(): Promise<void>;

static validateConfig(config: any): boolean {
throw new Error("Not implemented");
}
}
82 changes: 82 additions & 0 deletions src/lib/secretsManager/providerService/awsSecretManagerProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/* eslint-disable class-methods-use-this */
import {
AwsSecretReference,
AWSSecretsManagerConfig,
CachedSecret,
SecretProviderConfig,
SecretProviderType,
SecretReference,
} from "../types";
import { AbstractSecretProvider } from "./AbstractSecretProvider";

// Functions
// 1. validate config
// 2. test connection
// 3. fetch secret
// 4. list secrets

const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes

export class AWSSecretsManagerProvider extends AbstractSecretProvider {
readonly type = SecretProviderType.AWS_SECRETS_MANAGER;

readonly id: string;

protected config: AWSSecretsManagerConfig;

protected cache: Map<string, CachedSecret> = new Map();

protected getSecretIdentfier(ref: AwsSecretReference): string {
return `name=${ref.nameOrArn};version:${ref.version}`;
}

constructor(providerConfig: SecretProviderConfig) {
super();
this.id = providerConfig.id;
this.config = providerConfig.config as AWSSecretsManagerConfig;
}

async testConnection(): Promise<boolean> {
if (!AWSSecretsManagerProvider.validateConfig(this.config)) {
return false;
}

return true;
}

async getSecret(ref: AwsSecretReference): Promise<string> {
const secretKey = this.getSecretIdentfier(ref);
const cachedSecret = this.cache.get(secretKey);
const now = Date.now();

if (cachedSecret && cachedSecret.expiry > now) {
return cachedSecret.value;
}

// Fetch from AWS Secrets Manager
const secretValue = "fetched-secret-value"; // Placeholder for actual fetch logic

this.cache.set(secretKey, {
value: secretValue,
expiry: now + DEFAULT_CACHE_TTL_MS,
});

return secretValue;
}

async getSecrets(): Promise<string[]> {}

async setSecret(): Promise<void> {
throw new Error("Method not implemented.");
}

async setSecrets(): Promise<void> {
throw new Error("Method not implemented.");
}

static validateConfig(config: AWSSecretsManagerConfig): boolean {
return Boolean(
config.accessKeyId && config.secretAccessKey && config.region
);
}
}
14 changes: 14 additions & 0 deletions src/lib/secretsManager/providerService/providerFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { SecretProviderConfig, SecretProviderType } from "../types";
import { AWSSecretsManagerProvider } from "./awsSecretManagerProvider";
import { AbstractSecretProvider } from "./AbstractSecretProvider";

export function createProvider(
config: SecretProviderConfig
): AbstractSecretProvider {
switch (config.type) {
case SecretProviderType.AWS_SECRETS_MANAGER:
return new AWSSecretsManagerProvider(config);
default:
throw new Error(`Unknown provider type: ${config.type}`);
}
}
Loading