Skip to main content
Glama
crypto.ts5.98 kB
import { createCipheriv, createDecipheriv, randomBytes, pbkdf2Sync } from 'crypto'; import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; import { homedir } from 'os'; import { join } from 'path'; import type { GameData } from '../types.js'; const ALGORITHM = 'aes-256-gcm'; const SALT_LENGTH = 32; const IV_LENGTH = 16; const AUTH_TAG_LENGTH = 16; const KEY_LENGTH = 32; const ITERATIONS = 100000; export class CryptoStorage { private gameDir: string; private dataFile: string; private keyFile: string; constructor() { this.gameDir = join(homedir(), '.mcp-idle-game'); this.dataFile = join(this.gameDir, 'player_data.enc'); this.keyFile = join(this.gameDir, 'key.enc'); if (!existsSync(this.gameDir)) { mkdirSync(this.gameDir, { recursive: true }); } } private deriveKey(password: string, salt: Buffer): Buffer { return pbkdf2Sync(password, salt, ITERATIONS, KEY_LENGTH, 'sha256'); } private encrypt(data: string, key: Buffer): Buffer { const iv = randomBytes(IV_LENGTH); const cipher = createCipheriv(ALGORITHM, key, iv); const encrypted = Buffer.concat([ cipher.update(data, 'utf8'), cipher.final() ]); const authTag = cipher.getAuthTag(); return Buffer.concat([iv, authTag, encrypted]); } private decrypt(encryptedData: Buffer, key: Buffer): string { const iv = encryptedData.subarray(0, IV_LENGTH); const authTag = encryptedData.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH); const encrypted = encryptedData.subarray(IV_LENGTH + AUTH_TAG_LENGTH); const decipher = createDecipheriv(ALGORITHM, key, iv); decipher.setAuthTag(authTag); return decipher.update(encrypted) + decipher.final('utf8'); } async initialize(password: string): Promise<void> { if (existsSync(this.keyFile)) { throw new Error('Game already initialized. Use load() instead.'); } const salt = randomBytes(SALT_LENGTH); const key = this.deriveKey(password, salt); // Store salt with encrypted marker const keyData = Buffer.concat([salt]); writeFileSync(this.keyFile, keyData); // Create initial empty game data const initialData: GameData = { player: { name: '', equipment: { weapon: null, shield: null, armor: null, accessory: null, item1: null, item2: null }, inventory: [], itemInventory: [], gold: 0, hp: 100, maxHp: 100, state: 'idle' }, version: '0.1.0' }; await this.save(initialData, password); } async save(data: GameData, password: string): Promise<void> { if (!existsSync(this.keyFile)) { throw new Error('Game not initialized. Use initialize() first.'); } const keyData = readFileSync(this.keyFile); const salt = keyData.subarray(0, SALT_LENGTH); const key = this.deriveKey(password, salt); const jsonData = JSON.stringify(data, null, 2); const encrypted = this.encrypt(jsonData, key); writeFileSync(this.dataFile, encrypted); } async load(password: string): Promise<GameData> { if (!existsSync(this.keyFile) || !existsSync(this.dataFile)) { throw new Error('No save data found. Use initialize() first.'); } const keyData = readFileSync(this.keyFile); const salt = keyData.subarray(0, SALT_LENGTH); const key = this.deriveKey(password, salt); const encryptedData = readFileSync(this.dataFile); const decrypted = this.decrypt(encryptedData, key); const data = JSON.parse(decrypted) as GameData; // マイグレーション処理を実行 const migratedData = this.migrateData(data); return migratedData; } private migrateData(data: GameData): GameData { const currentVersion = '0.1.0'; const saveVersion = data.version || '0.0.0'; // バージョンが既に最新の場合はそのまま返す if (saveVersion === currentVersion) { return data; } console.error(`Migrating save data from version ${saveVersion} to ${currentVersion}...`); // v0.0.0 → v0.1.0: 持ち物システムの追加 if (this.compareVersions(saveVersion, '0.1.0') < 0) { // itemInventoryが存在しない場合は追加 if (!data.player.itemInventory) { data.player.itemInventory = []; } // item1, item2が存在しない場合は追加 if (!data.player.equipment.item1 && data.player.equipment.item1 !== null) { data.player.equipment.item1 = null; } if (!data.player.equipment.item2 && data.player.equipment.item2 !== null) { data.player.equipment.item2 = null; } // HPシステムの追加 if (typeof data.player.hp !== 'number') { data.player.hp = 100; } if (typeof data.player.maxHp !== 'number') { data.player.maxHp = 100; } } // 将来のマイグレーション例: // if (this.compareVersions(saveVersion, '0.2.0') < 0) { // // v0.2.0の新機能のマイグレーション処理 // } // バージョン番号を更新 data.version = currentVersion; console.error(`Migration complete: ${saveVersion} → ${currentVersion}`); return data; } /** * バージョン文字列を比較する * @returns v1 < v2 なら負の数、v1 === v2 なら0、v1 > v2 なら正の数 */ private compareVersions(v1: string, v2: string): number { const parts1 = v1.split('.').map(Number); const parts2 = v2.split('.').map(Number); for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { const num1 = parts1[i] || 0; const num2 = parts2[i] || 0; if (num1 !== num2) { return num1 - num2; } } return 0; } exists(): boolean { return existsSync(this.keyFile) && existsSync(this.dataFile); } getSaveLocation(): string { return this.gameDir; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bellsanct/mcp-dungeon-game'

If you have feedback or need assistance with the MCP directory API, please join our Discord server