forked from ilkerzg/fal-cli
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsecure-storage.js
More file actions
237 lines (202 loc) · 6.86 KB
/
secure-storage.js
File metadata and controls
237 lines (202 loc) · 6.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
#!/usr/bin/env node
/**
* ===== SECURE STORAGE MODULE =====
*
* This module provides secure storage capabilities for sensitive data like API keys.
* It implements AES-256-GCM encryption with machine-specific keys to protect
* stored credentials from unauthorized access.
*
* Security Features:
* - AES-256-GCM encryption with authenticated encryption
* - Machine-specific encryption keys based on hardware/OS fingerprints
* - OS-specific secure storage locations
* - API key format validation
* - Secure key rotation and removal capabilities
*
* Storage Locations:
* - Windows: %APPDATA%/fal-cli/
* - macOS: ~/Library/Application Support/fal-cli/
* - Linux: ~/.config/fal-cli/
*
* The module ensures that API keys are never stored in plain text and can only
* be decrypted on the machine where they were originally encrypted.
*
* @author ilkerzg
* @version 0.0.1
*/
import fs from 'fs-extra';
import path from 'path';
import os from 'os';
import crypto from 'crypto';
/**
* Get OS-specific configuration directory
*
* Returns the appropriate configuration directory based on the operating system,
* following platform conventions for application data storage.
*
* @returns {string} Platform-specific configuration directory path
*/
const getConfigDir = () => {
const platform = os.platform();
let configDir;
switch (platform) {
case 'win32':
// Windows: Use Roaming AppData for user-specific application data
configDir = path.join(os.homedir(), 'AppData', 'Roaming', 'fal-cli');
break;
case 'darwin':
// macOS: Use Application Support for app-specific data
configDir = path.join(os.homedir(), 'Library', 'Application Support', 'fal-cli');
break;
default: // Linux and other Unix-like systems
// Linux: Use XDG config directory specification
configDir = path.join(os.homedir(), '.config', 'fal-cli');
break;
}
return configDir;
};
/**
* Generate a machine-specific encryption key
*
* Creates a unique encryption key based on machine characteristics.
* This ensures that encrypted data can only be decrypted on the same machine
* where it was originally encrypted, providing an additional layer of security.
*
* @returns {Buffer} 32-byte SHA-256 hash of machine characteristics
*/
const getMachineKey = () => {
// Combine hostname, platform, and architecture for machine fingerprint
const machineId = os.hostname() + os.platform() + os.arch();
return crypto.createHash('sha256').update(machineId).digest();
};
/**
* Encrypt sensitive data using AES-256-GCM
*
* Uses authenticated encryption to ensure both confidentiality and integrity.
* The machine-specific key ensures data can only be decrypted on this machine.
*
* @param {string} text - Plain text data to encrypt
* @returns {Object} Encryption result containing:
* - encrypted: Encrypted data as hex string
* - iv: Initialization vector as hex string
* - authTag: Authentication tag as hex string
*/
const encrypt = (text) => {
const algorithm = 'aes-256-gcm';
const key = getMachineKey();
const iv = crypto.randomBytes(16); // Generate random IV for each encryption
const cipher = crypto.createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
// Get authentication tag for integrity verification
const authTag = cipher.getAuthTag();
return {
encrypted,
iv: iv.toString('hex'),
authTag: authTag.toString('hex')
};
};
// Decrypt data
const decrypt = (encryptedData) => {
try {
const algorithm = 'aes-256-gcm';
const key = getMachineKey();
const iv = Buffer.from(encryptedData.iv, 'hex');
const decipher = crypto.createDecipheriv(algorithm, key, iv);
decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex'));
let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
} catch (error) {
throw new Error('Failed to decrypt API key - data may be corrupted');
}
};
// Store API key securely
export const storeApiKey = async (apiKey) => {
try {
const configDir = getConfigDir();
await fs.ensureDir(configDir);
const encryptedData = encrypt(apiKey);
const configFile = path.join(configDir, 'config.json');
const config = {
version: '1.0.0',
created: new Date().toISOString(),
lastModified: new Date().toISOString(),
apiKey: encryptedData
};
await fs.writeFile(configFile, JSON.stringify(config, null, 2));
// Set secure file permissions (readable only by owner)
if (os.platform() !== 'win32') {
await fs.chmod(configFile, 0o600);
}
return true;
} catch (error) {
throw new Error(`Failed to store API key: ${error.message}`);
}
};
// Retrieve API key securely
export const retrieveApiKey = async () => {
try {
const configDir = getConfigDir();
const configFile = path.join(configDir, 'config.json');
if (!await fs.pathExists(configFile)) {
return null; // No stored API key
}
const configData = await fs.readFile(configFile, 'utf8');
const config = JSON.parse(configData);
if (!config.apiKey) {
return null;
}
const decryptedKey = decrypt(config.apiKey);
return decryptedKey;
} catch (error) {
throw new Error(`Failed to retrieve API key: ${error.message}`);
}
};
// Check if API key is stored
export const hasStoredApiKey = async () => {
try {
const configDir = getConfigDir();
const configFile = path.join(configDir, 'config.json');
return await fs.pathExists(configFile);
} catch (error) {
return false;
}
};
// Remove stored API key
export const removeApiKey = async () => {
try {
const configDir = getConfigDir();
const configFile = path.join(configDir, 'config.json');
if (await fs.pathExists(configFile)) {
await fs.remove(configFile);
}
return true;
} catch (error) {
throw new Error(`Failed to remove API key: ${error.message}`);
}
};
// Get config file path for display
export const getConfigPath = () => {
const configDir = getConfigDir();
return path.join(configDir, 'config.json');
};
// Validate API key format
export const validateApiKey = (apiKey) => {
if (!apiKey || typeof apiKey !== 'string') {
return false;
}
const trimmedKey = apiKey.trim();
// Basic length and character checks
if (trimmedKey.length < 10) {
return false;
}
// FAL API keys can have various formats, be more flexible:
// - Contains alphanumeric chars, hyphens, underscores, colons
// - Has reasonable length (10-200 chars)
// - Contains at least one colon or hyphen (typical separators)
const basicPattern = /^[a-zA-Z0-9_:\-\.]+$/;
const hasValidSeparator = /[:\-]/.test(trimmedKey);
const hasReasonableLength = trimmedKey.length >= 10 && trimmedKey.length <= 200;
return basicPattern.test(trimmedKey) && hasValidSeparator && hasReasonableLength;
};