UNPKG

ms365-mcp-server

Version:

Microsoft 365 MCP Server for managing Microsoft 365 email through natural language interactions with full OAuth2 authentication support

282 lines (281 loc) 10 kB
import * as fs from 'fs'; import * as path from 'path'; import { logger } from './api.js'; import { getConfigDirWithFallback } from './config-dir.js'; // Service name for keychain storage const SERVICE_NAME = 'ms365-mcp-server'; // Directory for file-based storage (now primary storage method) const STORAGE_DIR = getConfigDirWithFallback(); /** * Secure credential store that uses file-based storage as default, * with optional OS keychain support if specifically enabled. */ export class CredentialStore { constructor() { this.useKeychain = false; this.keytar = null; this.initialized = true; // File storage is always ready this.forceFileStorage = true; // Force file storage as default this.ensureStorageDir(); // Don't initialize keychain by default - file storage is primary logger.log('Using file-based credential storage as default'); } /** * Ensure the credential store is initialized */ async ensureInitialized() { // File storage is always ready, no async initialization needed return; } /** * Enable keychain support (optional) */ async enableKeychain() { try { // Try to load keytar for secure OS keychain storage // Use eval to prevent TypeScript from checking the import at compile time const keytarModule = await eval('import("keytar")'); this.keytar = keytarModule.default || keytarModule; this.useKeychain = true; this.forceFileStorage = false; logger.log('OS keychain support enabled'); return true; } catch (error) { logger.log('OS keychain not available, continuing with file storage'); return false; } } /** * Force file storage (disable keychain) */ forceFileStorageMode() { this.useKeychain = false; this.forceFileStorage = true; logger.log('Forced file storage mode enabled'); } /** * Ensure storage directory exists */ ensureStorageDir() { if (!fs.existsSync(STORAGE_DIR)) { fs.mkdirSync(STORAGE_DIR, { recursive: true }); } } /** * Store credentials securely */ async setCredentials(account, credentials) { await this.ensureInitialized(); // Always use file storage as primary method if (this.forceFileStorage || !this.useKeychain || !this.keytar) { await this.setCredentialsFile(account, credentials); return; } // Only use keychain if explicitly enabled and file storage is disabled const credentialString = JSON.stringify(credentials); try { await this.keytar.setPassword(SERVICE_NAME, account, credentialString); logger.log(`Stored credentials for ${account} in OS keychain`); } catch (error) { logger.error(`Failed to store in keychain for ${account}:`, error); // Fall back to file storage await this.setCredentialsFile(account, credentials); } } /** * Retrieve credentials securely */ async getCredentials(account) { await this.ensureInitialized(); // Always try file storage first const fileCredentials = await this.getCredentialsFile(account); if (fileCredentials) { return fileCredentials; } // Only check keychain if file storage failed and keychain is enabled if (!this.forceFileStorage && this.useKeychain && this.keytar) { try { const credentialString = await this.keytar.getPassword(SERVICE_NAME, account); if (credentialString) { logger.log(`Retrieved credentials for ${account} from OS keychain`); return JSON.parse(credentialString); } } catch (error) { logger.error(`Failed to retrieve from keychain for ${account}:`, error); } } return null; } /** * Delete credentials securely */ async deleteCredentials(account) { await this.ensureInitialized(); let deleted = false; if (this.useKeychain && this.keytar) { try { deleted = await this.keytar.deletePassword(SERVICE_NAME, account); if (deleted) { logger.log(`Deleted credentials for ${account} from OS keychain`); } } catch (error) { logger.error(`Failed to delete from keychain for ${account}:`, error); } } // Also try file storage const fileDeleted = await this.deleteCredentialsFile(account); return deleted || fileDeleted; } /** * List all stored accounts */ async listAccounts() { await this.ensureInitialized(); const accounts = new Set(); // Get accounts from keychain (if available) if (this.useKeychain && this.keytar) { try { const keychainAccounts = await this.keytar.findCredentials(SERVICE_NAME); keychainAccounts.forEach((cred) => accounts.add(cred.account)); } catch (error) { logger.error('Failed to list keychain accounts:', error); } } // Get accounts from file storage const fileAccounts = await this.listFileAccounts(); fileAccounts.forEach(account => accounts.add(account)); return Array.from(accounts); } /** * File-based credential storage (primary method) */ async setCredentialsFile(account, credentials) { try { const filePath = path.join(STORAGE_DIR, `token_${this.sanitizeFilename(account)}.json`); const encryptedData = this.simpleEncrypt(JSON.stringify(credentials)); fs.writeFileSync(filePath, encryptedData); logger.log(`Stored credentials for ${account} in file`); } catch (error) { logger.error(`Failed to store credentials in file for ${account}:`, error); throw error; } } /** * File-based credential retrieval (primary method) */ async getCredentialsFile(account) { try { const filePath = path.join(STORAGE_DIR, `token_${this.sanitizeFilename(account)}.json`); if (!fs.existsSync(filePath)) { return null; } const encryptedData = fs.readFileSync(filePath, 'utf8'); const decryptedData = this.simpleDecrypt(encryptedData); logger.log(`Retrieved credentials for ${account} from file`); return JSON.parse(decryptedData); } catch (error) { logger.error(`Failed to retrieve credentials from file for ${account}:`, error); return null; } } /** * File-based credential deletion (primary method) */ async deleteCredentialsFile(account) { try { const filePath = path.join(STORAGE_DIR, `token_${this.sanitizeFilename(account)}.json`); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); logger.log(`Deleted credentials for ${account} from file`); return true; } return false; } catch (error) { logger.error(`Failed to delete credentials from file for ${account}:`, error); return false; } } /** * List accounts from file storage */ async listFileAccounts() { try { const files = fs.readdirSync(STORAGE_DIR); return files .filter(file => file.startsWith('token_') && file.endsWith('.json')) .map(file => file.replace('token_', '').replace('.json', '')) .map(filename => this.unsanitizeFilename(filename)); } catch (error) { logger.error('Failed to list file accounts:', error); return []; } } /** * Sanitize account name for file storage */ sanitizeFilename(account) { return account.replace(/[^a-zA-Z0-9._-]/g, '_'); } /** * Reverse filename sanitization */ unsanitizeFilename(filename) { // This is a simplified reverse - in practice, we'd need a more robust mapping return filename.replace(/_/g, '@'); } /** * Simple encryption for file storage (base64 + basic obfuscation) * Note: This is not cryptographically secure, just obfuscation */ simpleEncrypt(data) { const key = 'ms365-mcp-server-key'; let encrypted = ''; for (let i = 0; i < data.length; i++) { encrypted += String.fromCharCode(data.charCodeAt(i) ^ key.charCodeAt(i % key.length)); } return Buffer.from(encrypted).toString('base64'); } /** * Simple decryption for file storage */ simpleDecrypt(encryptedData) { const key = 'ms365-mcp-server-key'; const encrypted = Buffer.from(encryptedData, 'base64').toString(); let decrypted = ''; for (let i = 0; i < encrypted.length; i++) { decrypted += String.fromCharCode(encrypted.charCodeAt(i) ^ key.charCodeAt(i % key.length)); } return decrypted; } /** * Check if keychain is available */ isKeychainAvailable() { return this.useKeychain && this.keytar !== null && !this.forceFileStorage; } /** * Get storage method being used */ getStorageMethod() { if (this.forceFileStorage || !this.useKeychain || !this.keytar) { return 'Encrypted File'; } return 'OS Keychain'; } /** * Get storage location */ getStorageLocation() { return STORAGE_DIR; } } export const credentialStore = new CredentialStore();