feat(security): integrate @push.rocks/smartsecret for keychain-based token storage
Connection tokens are now stored in OS keychain (or encrypted file fallback) instead of plaintext JSON. Existing plaintext tokens auto-migrate on first load.
This commit is contained in:
@@ -6,17 +6,21 @@ import type { StorageManager } from '../storage/index.ts';
|
||||
|
||||
const LEGACY_CONNECTIONS_FILE = './.nogit/connections.json';
|
||||
const CONNECTIONS_PREFIX = '/connections/';
|
||||
const KEYCHAIN_PREFIX = 'keychain:';
|
||||
|
||||
/**
|
||||
* Manages provider connections — persists each connection as an
|
||||
* individual JSON file via StorageManager.
|
||||
* individual JSON file via StorageManager. Tokens are stored in
|
||||
* the OS keychain (or encrypted file fallback) via SmartSecret.
|
||||
*/
|
||||
export class ConnectionManager {
|
||||
private connections: interfaces.data.IProviderConnection[] = [];
|
||||
private storageManager: StorageManager;
|
||||
private smartSecret: plugins.smartsecret.SmartSecret;
|
||||
|
||||
constructor(storageManager: StorageManager) {
|
||||
constructor(storageManager: StorageManager, smartSecret: plugins.smartsecret.SmartSecret) {
|
||||
this.storageManager = storageManager;
|
||||
this.smartSecret = smartSecret;
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
@@ -51,6 +55,18 @@ export class ConnectionManager {
|
||||
for (const key of keys) {
|
||||
const conn = await this.storageManager.getJSON<interfaces.data.IProviderConnection>(key);
|
||||
if (conn) {
|
||||
if (conn.token.startsWith(KEYCHAIN_PREFIX)) {
|
||||
// Token is in keychain — retrieve it
|
||||
const realToken = await this.smartSecret.getSecret(conn.id);
|
||||
if (realToken) {
|
||||
conn.token = realToken;
|
||||
} else {
|
||||
logger.warn(`Could not retrieve token for connection ${conn.id} from keychain`);
|
||||
}
|
||||
} else if (conn.token && conn.token !== '***') {
|
||||
// Plaintext token found — auto-migrate to keychain
|
||||
await this.migrateTokenToKeychain(conn);
|
||||
}
|
||||
this.connections.push(conn);
|
||||
}
|
||||
}
|
||||
@@ -61,11 +77,33 @@ export class ConnectionManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates a plaintext token to keychain storage.
|
||||
*/
|
||||
private async migrateTokenToKeychain(
|
||||
conn: interfaces.data.IProviderConnection,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.smartSecret.setSecret(conn.id, conn.token);
|
||||
// Save sentinel to JSON file
|
||||
const jsonConn = { ...conn, token: `${KEYCHAIN_PREFIX}${conn.id}` };
|
||||
await this.storageManager.setJSON(`${CONNECTIONS_PREFIX}${conn.id}.json`, jsonConn);
|
||||
logger.info(`Migrated token for connection "${conn.name}" to keychain`);
|
||||
} catch (err) {
|
||||
logger.warn(`Failed to migrate token for ${conn.id} to keychain: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async persistConnection(conn: interfaces.data.IProviderConnection): Promise<void> {
|
||||
await this.storageManager.setJSON(`${CONNECTIONS_PREFIX}${conn.id}.json`, conn);
|
||||
// Store real token in keychain
|
||||
await this.smartSecret.setSecret(conn.id, conn.token);
|
||||
// Save JSON with sentinel value
|
||||
const jsonConn = { ...conn, token: `${KEYCHAIN_PREFIX}${conn.id}` };
|
||||
await this.storageManager.setJSON(`${CONNECTIONS_PREFIX}${conn.id}.json`, jsonConn);
|
||||
}
|
||||
|
||||
private async removeConnection(id: string): Promise<void> {
|
||||
await this.smartSecret.deleteSecret(id);
|
||||
await this.storageManager.delete(`${CONNECTIONS_PREFIX}${id}.json`);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as plugins from '../plugins.ts';
|
||||
import { logger } from '../logging.ts';
|
||||
import { ConnectionManager } from './connectionmanager.ts';
|
||||
import { OpsServer } from '../opsserver/index.ts';
|
||||
@@ -10,6 +11,7 @@ import { resolvePaths } from '../paths.ts';
|
||||
*/
|
||||
export class GitopsApp {
|
||||
public storageManager: StorageManager;
|
||||
public smartSecret: plugins.smartsecret.SmartSecret;
|
||||
public connectionManager: ConnectionManager;
|
||||
public opsServer: OpsServer;
|
||||
public cacheDb: CacheDb;
|
||||
@@ -21,7 +23,8 @@ export class GitopsApp {
|
||||
backend: 'filesystem',
|
||||
fsPath: paths.defaultStoragePath,
|
||||
});
|
||||
this.connectionManager = new ConnectionManager(this.storageManager);
|
||||
this.smartSecret = new plugins.smartsecret.SmartSecret({ service: 'gitops' });
|
||||
this.connectionManager = new ConnectionManager(this.storageManager, this.smartSecret);
|
||||
|
||||
this.cacheDb = CacheDb.getInstance({
|
||||
storagePath: paths.defaultTsmDbPath,
|
||||
|
||||
Reference in New Issue
Block a user