import * as fs from 'fs-extra'; import * as path from 'path'; import * as os from 'os'; import { createHash, createCipheriv, createDecipheriv, randomBytes } from 'crypto'; /** * Simple, secure token storage without native dependencies * Stores encrypted tokens in user's home directory */ export interface StoredTokens { accessToken: string; refreshToken: string; expiresAt: string; } export class TokenStorage { private configDir: string; private tokenFile: string; private keyFile: string; constructor(serviceName: string = 'vcsys-cli') { this.configDir = path.join(os.homedir(), `.${serviceName}`); this.tokenFile = path.join(this.configDir, 'tokens.enc'); this.keyFile = path.join(this.configDir, '.key'); } /** * Store tokens securely */ async storeTokens(tokens: StoredTokens): Promise { try { // Ensure config directory exists await fs.ensureDir(this.configDir); // Get or create encryption key const key = await this.getOrCreateKey(); // Encrypt tokens const iv = randomBytes(16); const cipher = createCipheriv('aes-256-gcm', key, iv); const tokenData = JSON.stringify(tokens); let encrypted = cipher.update(tokenData, 'utf8', 'hex'); encrypted += cipher.final('hex'); const authTag = cipher.getAuthTag(); // Store encrypted data const encryptedData = { iv: iv.toString('hex'), authTag: authTag.toString('hex'), data: encrypted }; await fs.writeFile(this.tokenFile, JSON.stringify(encryptedData), { mode: 0o600 // Owner read/write only }); } catch (error) { // Fallback to environment variable process.env.VCSYS_CLI_TOKENS = JSON.stringify(tokens); } } /** * Retrieve stored tokens */ async getStoredTokens(): Promise { try { // Try encrypted file first if (await fs.pathExists(this.tokenFile)) { const key = await this.getOrCreateKey(); const encryptedData = JSON.parse(await fs.readFile(this.tokenFile, 'utf8')); const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(encryptedData.iv, 'hex')); decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex')); let decrypted = decipher.update(encryptedData.data, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return JSON.parse(decrypted); } // Fallback to environment variable if (process.env.VCSYS_CLI_TOKENS) { return JSON.parse(process.env.VCSYS_CLI_TOKENS); } return null; } catch (error) { return null; } } /** * Clear stored tokens */ async clearTokens(): Promise { try { if (await fs.pathExists(this.tokenFile)) { await fs.remove(this.tokenFile); } } catch (error) { // Ignore errors } delete process.env.VCSYS_CLI_TOKENS; } /** * Get or create encryption key */ private async getOrCreateKey(): Promise { try { if (await fs.pathExists(this.keyFile)) { const keyData = await fs.readFile(this.keyFile, 'utf8'); return Buffer.from(keyData, 'hex'); } // Create new key const key = randomBytes(32); await fs.writeFile(this.keyFile, key.toString('hex'), { mode: 0o600 // Owner read/write only }); return key; } catch (error) { // Fallback to deterministic key based on machine const machineId = os.hostname() + os.userInfo().username; return createHash('sha256').update(machineId).digest(); } } }