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
JavaScript
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();