@kikkimo/claude-launcher 1.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Secure Password Input Module
3
+ * Handles masked password input without showing plaintext characters
4
+ */
5
+
6
+ const readline = require('readline');
7
+ const stdinManager = require('../utils/stdin-manager');
8
+
9
+ /**
10
+ * Get password input with proper masking (no plaintext display)
11
+ * @param {string} prompt - The prompt message to display
12
+ * @returns {Promise<string>} - The entered password
13
+ */
14
+ function getPasswordInput(prompt) {
15
+ return new Promise((resolve, reject) => {
16
+ let password = '';
17
+ let scope = null;
18
+ let cleanedUp = false;
19
+
20
+ // Display prompt - this is necessary for password input
21
+ process.stdout.write(prompt);
22
+
23
+ if (!process.stdin.isTTY) {
24
+ // Non-TTY environment fallback - use scope's createReadline
25
+ try {
26
+ scope = stdinManager.acquire('line', {
27
+ id: 'passwordInput_nonTTY',
28
+ allowNested: false
29
+ });
30
+ } catch (error) {
31
+ reject(new Error(`Failed to acquire stdin for password input: ${error.message}`));
32
+ return;
33
+ }
34
+
35
+ const rl = scope.createReadline();
36
+ rl.question('', (answer) => {
37
+ rl.close();
38
+ scope.release();
39
+ resolve(answer.trim());
40
+ });
41
+ return;
42
+ }
43
+
44
+ // Use StdinManager to acquire raw mode scope
45
+ try {
46
+ scope = stdinManager.acquire('raw', {
47
+ id: 'passwordInput',
48
+ allowNested: false
49
+ });
50
+ } catch (error) {
51
+ reject(new Error(`Failed to acquire stdin for password input: ${error.message}`));
52
+ return;
53
+ }
54
+
55
+ const cleanup = () => {
56
+ if (cleanedUp) return;
57
+ cleanedUp = true;
58
+
59
+ try {
60
+ scope.removeAllListeners('data');
61
+ scope.release();
62
+ } catch (error) {
63
+ // Ignore cleanup errors
64
+ }
65
+ };
66
+
67
+ const handleKeyPress = (data) => {
68
+ const key = data.toString();
69
+
70
+ // Handle Ctrl+C - cancel password input immediately
71
+ if (key === '\u0003') {
72
+ cleanup();
73
+ reject(new Error('Password input cancelled by Ctrl+C'));
74
+ return;
75
+ }
76
+
77
+ // If waiting for second Ctrl+C, any other key cancels it
78
+ if (stdinManager.isCtrlCPending()) {
79
+ stdinManager.cancelCtrlC();
80
+ // Continue to process this key normally
81
+ }
82
+
83
+ // Handle different key combinations
84
+ switch (key) {
85
+ case '\r': // Enter (CR)
86
+ case '\n': // Line Feed (LF)
87
+ case '\r\n': // CRLF
88
+ process.stdout.write('\n');
89
+ cleanup();
90
+ resolve(password);
91
+ return;
92
+
93
+ case '\u007f': // Backspace (DEL)
94
+ case '\b': // Backspace (BS)
95
+ if (password.length > 0) {
96
+ password = password.slice(0, -1);
97
+ // Clear the last asterisk
98
+ process.stdout.write('\b \b');
99
+ }
100
+ return;
101
+
102
+ case '\u001b': // Escape
103
+ cleanup();
104
+ reject(new Error('Password input cancelled'));
105
+ return;
106
+
107
+ default:
108
+ // Process each character individually to handle IME batch input
109
+ // When using Chinese IME, multiple chars may come in one event (e.g., "vis")
110
+ for (let i = 0; i < key.length; i++) {
111
+ const char = key[i];
112
+ const charCode = char.charCodeAt(0);
113
+
114
+ // Filter out control characters (only accept ASCII printable)
115
+ if (charCode >= 32 && charCode < 127) {
116
+ password += char;
117
+ process.stdout.write('*');
118
+ }
119
+ }
120
+ return;
121
+ }
122
+ };
123
+
124
+ try {
125
+ scope.on('data', handleKeyPress);
126
+ } catch (error) {
127
+ cleanup();
128
+ reject(error);
129
+ }
130
+ });
131
+ }
132
+
133
+ /**
134
+ * Get password input with confirmation
135
+ * @param {string} prompt - The initial prompt message
136
+ * @param {string} confirmPrompt - The confirmation prompt message
137
+ * @returns {Promise<string>} - The confirmed password
138
+ */
139
+ async function getPasswordWithConfirmation(prompt, confirmPrompt = 'Confirm Password: ') {
140
+ const password = await getPasswordInput(prompt);
141
+
142
+ if (!password) {
143
+ throw new Error('Password cannot be empty');
144
+ }
145
+
146
+ const confirmPassword = await getPasswordInput(confirmPrompt);
147
+
148
+ if (password !== confirmPassword) {
149
+ throw new Error('Passwords do not match');
150
+ }
151
+
152
+ return password;
153
+ }
154
+
155
+ module.exports = {
156
+ getPasswordInput,
157
+ getPasswordWithConfirmation
158
+ };
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Password Strength Validation Module
3
+ * Enforces strong password requirements for security
4
+ */
5
+
6
+ const i18n = require('../i18n');
7
+
8
+ /**
9
+ * Validate password strength according to security requirements
10
+ * @param {string} password - The password to validate
11
+ * @returns {Object} - Validation result with success status and detailed feedback
12
+ */
13
+ function validatePasswordStrength(password) {
14
+ const result = {
15
+ valid: false,
16
+ score: 0,
17
+ errors: [],
18
+ suggestions: []
19
+ };
20
+
21
+ // Basic validation
22
+ if (!password) {
23
+ result.errors.push(i18n.tSync('errors.password.empty'));
24
+ return result;
25
+ }
26
+
27
+ // Length requirement (≥6 characters)
28
+ if (password.length < 6) {
29
+ result.errors.push(i18n.tSync('errors.password.too_short'));
30
+ } else {
31
+ result.score += 1;
32
+ }
33
+
34
+ // ASCII only validation
35
+ if (!/^[\x20-\x7E]*$/.test(password)) {
36
+ result.errors.push(i18n.tSync('errors.password.non_ascii'));
37
+ } else {
38
+ result.score += 1;
39
+ }
40
+
41
+ // No spaces or unusual whitespace characters
42
+ if (/\s/.test(password)) {
43
+ result.errors.push(i18n.tSync('errors.password.contains_spaces'));
44
+ } else {
45
+ result.score += 1;
46
+ }
47
+
48
+ // Character type requirements (at least 2 of 4 types)
49
+ let characterTypes = 0;
50
+ const hasLowercase = /[a-z]/.test(password);
51
+ const hasUppercase = /[A-Z]/.test(password);
52
+ const hasNumbers = /[0-9]/.test(password);
53
+ const hasSpecialChars = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?~`]/.test(password);
54
+
55
+ if (hasLowercase) characterTypes++;
56
+ if (hasUppercase) characterTypes++;
57
+ if (hasNumbers) characterTypes++;
58
+ if (hasSpecialChars) characterTypes++;
59
+
60
+ if (characterTypes < 2) {
61
+ result.errors.push(i18n.tSync('errors.password.insufficient_types'));
62
+
63
+ // Provide specific suggestions
64
+ if (!hasLowercase) result.suggestions.push(i18n.tSync('errors.password.suggest_lowercase'));
65
+ if (!hasUppercase) result.suggestions.push(i18n.tSync('errors.password.suggest_uppercase'));
66
+ if (!hasNumbers) result.suggestions.push(i18n.tSync('errors.password.suggest_numbers'));
67
+ if (!hasSpecialChars) result.suggestions.push(i18n.tSync('errors.password.suggest_special'));
68
+ } else {
69
+ result.score += characterTypes; // Bonus points for more character types
70
+ }
71
+
72
+ // Common weak password patterns - exact matches only for common weak passwords
73
+ const weakPatterns = [
74
+ /^123456$/,
75
+ /^1234567$/,
76
+ /^12345678$/,
77
+ /^password$/i,
78
+ /^admin$/i,
79
+ /^qwerty$/i,
80
+ /^abc123$/i,
81
+ /^111111$/,
82
+ /^000000$/,
83
+ /^(.)\1{5,}/, // Repeated characters (6+ times)
84
+ /^(012|123|234|345|456|567|678|789|890){3,}/, // Sequential numbers (3+ repetitions)
85
+ /^(abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz){3,}/i // Sequential letters (3+ repetitions)
86
+ ];
87
+
88
+ for (const pattern of weakPatterns) {
89
+ if (pattern.test(password)) {
90
+ result.errors.push(i18n.tSync('errors.password.weak_pattern'));
91
+ break;
92
+ }
93
+ }
94
+
95
+ // Additional strength scoring
96
+ if (password.length >= 8) result.score += 1;
97
+ if (password.length >= 12) result.score += 1;
98
+ if (characterTypes >= 3) result.score += 1;
99
+ if (characterTypes === 4) result.score += 1;
100
+
101
+ // Strength classification
102
+ if (result.score >= 8) {
103
+ result.strength = 'Very Strong';
104
+ } else if (result.score >= 6) {
105
+ result.strength = 'Strong';
106
+ } else if (result.score >= 4) {
107
+ result.strength = 'Good';
108
+ } else if (result.score >= 2) {
109
+ result.strength = 'Weak';
110
+ } else {
111
+ result.strength = 'Very Weak';
112
+ }
113
+
114
+ // Final validation - require Good level or above (score >= 4) and no errors
115
+ // Explicitly reject Weak and Very Weak passwords
116
+ if (result.strength === 'Weak' || result.strength === 'Very Weak') {
117
+ result.errors.push(i18n.tSync('errors.password.strength_insufficient', result.strength));
118
+ result.suggestions.push(i18n.tSync('errors.password.suggest_longer'));
119
+ result.suggestions.push(i18n.tSync('errors.password.suggest_more_types'));
120
+ }
121
+
122
+ result.valid = result.errors.length === 0 && result.score >= 4;
123
+
124
+ return result;
125
+ }
126
+
127
+ /**
128
+ * Get password requirements description for user guidance
129
+ * @returns {Array} - Array of requirement strings
130
+ */
131
+ function getPasswordRequirements() {
132
+ return i18n.tSync('ui.general.password_requirements_list');
133
+ }
134
+
135
+ /**
136
+ * Generate example of a strong password for user reference
137
+ * @returns {string} - Example strong password
138
+ */
139
+ function generatePasswordExample() {
140
+ const examples = [
141
+ 'MyStr0ng!Pass',
142
+ 'Secure#123Pass',
143
+ 'Good$Pass456',
144
+ 'Safe&Strong7',
145
+ 'Best!Choice9'
146
+ ];
147
+ return examples[Math.floor(Math.random() * examples.length)];
148
+ }
149
+
150
+ module.exports = {
151
+ validatePasswordStrength,
152
+ getPasswordRequirements,
153
+ generatePasswordExample
154
+ };
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Password Validation Module
3
+ * Handles all password verification scenarios
4
+ */
5
+
6
+ const { getPasswordInput } = require('./password-input');
7
+ const colors = require('../ui/colors');
8
+ const { validatePasswordStrength, getPasswordRequirements, generatePasswordExample } = require('./password-strength');
9
+ const i18n = require('../i18n');
10
+
11
+ /**
12
+ * Force cleanup stdin state to prevent navigation issues
13
+ */
14
+ function forceStdinCleanup() {
15
+ try {
16
+ if (process.stdin.isTTY) {
17
+ process.stdin.setRawMode(false);
18
+ process.stdin.removeAllListeners('data');
19
+ process.stdin.removeAllListeners('keypress');
20
+ process.stdin.pause();
21
+ }
22
+ } catch (error) {
23
+ // Ignore cleanup errors
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Convert strength string to translated version
29
+ * @param {string} strength - English strength string
30
+ * @returns {string} - Translated strength string
31
+ */
32
+ function getTranslatedStrength(strength) {
33
+ const strengthMap = {
34
+ 'Very Weak': 'very_weak',
35
+ 'Weak': 'weak',
36
+ 'Good': 'good',
37
+ 'Strong': 'strong',
38
+ 'Very Strong': 'very_strong'
39
+ };
40
+
41
+ const strengthKey = strengthMap[strength] || 'weak';
42
+ return i18n.tSync(`password.strength.${strengthKey}`);
43
+ }
44
+
45
+ /**
46
+ * Verify user password for export/import operations
47
+ * @param {Object} apiManager - The API manager instance
48
+ * @param {string} operation - The operation being performed (for error messages)
49
+ * @returns {Promise<boolean>} - True if password is verified, false otherwise
50
+ */
51
+ async function verifyExportPassword(apiManager, operation = 'operation') {
52
+ try {
53
+ const password = await getPasswordInput(i18n.tSync('messages.prompts.enter_password'));
54
+
55
+ if (!password) {
56
+ forceStdinCleanup();
57
+ console.log(colors.red + i18n.tSync('errors.password.empty') + colors.reset);
58
+ return false;
59
+ }
60
+
61
+ if (!apiManager.verifyExportPassword(password)) {
62
+ forceStdinCleanup();
63
+ console.log(colors.red + '❌ ' + i18n.tSync('errors.password.verification_failed') + colors.reset);
64
+ console.log(colors.gray + i18n.tSync('errors.general.operation_failed', operation) + colors.reset);
65
+ return false;
66
+ }
67
+
68
+ return true;
69
+ } catch (error) {
70
+ forceStdinCleanup();
71
+ if (error.message.includes('cancelled')) {
72
+ console.log(colors.yellow + '\n' + i18n.tSync('errors.general.cancelled_by_user').replace('{0}', operation) + colors.reset);
73
+ } else {
74
+ console.log(colors.red + `❌ Password verification error: ${error.message}` + colors.reset);
75
+ }
76
+ return false;
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Verify current password before changing it
82
+ * @param {Object} apiManager - The API manager instance
83
+ * @returns {Promise<boolean>} - True if current password is verified, false otherwise
84
+ */
85
+ async function verifyCurrentPassword(apiManager) {
86
+ try {
87
+ const currentPassword = await getPasswordInput(i18n.tSync('messages.prompts.enter_current_password'));
88
+
89
+ if (!currentPassword) {
90
+ forceStdinCleanup();
91
+ console.log(colors.red + i18n.tSync('errors.password.empty') + colors.reset);
92
+ return false;
93
+ }
94
+
95
+ if (!apiManager.verifyExportPassword(currentPassword)) {
96
+ forceStdinCleanup();
97
+ console.log(colors.red + '❌ ' + i18n.tSync('errors.password.current_incorrect') + colors.reset);
98
+ return false;
99
+ }
100
+
101
+ return true;
102
+ } catch (error) {
103
+ forceStdinCleanup();
104
+ if (error.message.includes('cancelled')) {
105
+ console.log(colors.yellow + '\n' + i18n.tSync('errors.password.verification_cancelled') + colors.reset);
106
+ } else {
107
+ console.log(colors.red + `❌ Password verification error: ${error.message}` + colors.reset);
108
+ }
109
+ return false;
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Prompt user to set up a new password with validation
115
+ * @param {Object} apiManager - The API manager instance
116
+ * @param {boolean} isFirstTime - Whether this is the first time setting a password
117
+ * @returns {Promise<boolean>} - True if password is successfully set, false otherwise
118
+ */
119
+ async function setupNewPassword(apiManager, isFirstTime = false) {
120
+ try {
121
+ console.log('');
122
+ console.log(colors.cyan + (isFirstTime ? i18n.tSync('password.setup.title') : i18n.tSync('password.setup.change_title')) + colors.reset);
123
+ console.log('');
124
+
125
+ if (!isFirstTime) {
126
+ console.log(colors.yellow + '⚠️ ' + i18n.tSync('password.setup.warning') + colors.reset);
127
+ console.log('');
128
+ }
129
+
130
+ // Display password requirements
131
+ console.log(colors.cyan + i18n.tSync('ui.general.password_requirements_title') + colors.reset);
132
+ const requirements = getPasswordRequirements();
133
+ requirements.forEach(req => {
134
+ console.log(colors.gray + ' ' + req + colors.reset);
135
+ });
136
+ console.log('');
137
+ console.log(colors.gray + i18n.tSync('ui.general.example_strong_password', generatePasswordExample()) + colors.reset);
138
+ console.log('');
139
+
140
+ let attempts = 0;
141
+ const maxAttempts = 3;
142
+
143
+ while (attempts < maxAttempts) {
144
+ attempts++;
145
+
146
+ // Get new password
147
+ const passwordPrompt = i18n.tSync('ui.general.new_password_attempt', attempts, maxAttempts);
148
+ const newPassword = await getPasswordInput(passwordPrompt);
149
+
150
+ if (!newPassword) {
151
+ forceStdinCleanup();
152
+ console.log(colors.red + i18n.tSync('errors.password.empty') + colors.reset);
153
+ if (attempts < maxAttempts) {
154
+ console.log('');
155
+ continue;
156
+ } else {
157
+ return false;
158
+ }
159
+ }
160
+
161
+ // Validate password strength
162
+ const validation = validatePasswordStrength(newPassword);
163
+
164
+ if (!validation.valid) {
165
+ console.log(colors.red + '❌ ' + i18n.tSync('errors.password.requirements_not_met') + colors.reset);
166
+ validation.errors.forEach(error => {
167
+ console.log(colors.red + ' • ' + error + colors.reset);
168
+ });
169
+
170
+ if (validation.suggestions.length > 0) {
171
+ console.log(colors.yellow + '💡 ' + i18n.tSync('ui.general.suggestions') + colors.reset);
172
+ validation.suggestions.forEach(suggestion => {
173
+ console.log(colors.yellow + ' • ' + suggestion + colors.reset);
174
+ });
175
+ }
176
+ // If password is invalid, force display strength as "Weak" regardless of technical score
177
+ const strengthKey = validation.valid ? validation.strength : 'Weak';
178
+ const translatedStrength = getTranslatedStrength(strengthKey);
179
+ console.log(colors.gray + i18n.tSync('ui.general.current_password_strength', translatedStrength) + colors.reset);
180
+
181
+ if (attempts < maxAttempts) {
182
+ console.log('');
183
+ continue;
184
+ } else {
185
+ console.log(colors.red + i18n.tSync('ui.general.max_attempts_password_failed') + colors.reset);
186
+ return false;
187
+ }
188
+ }
189
+
190
+ // Confirm password
191
+ const confirmPassword = await getPasswordInput(i18n.tSync('password.setup.confirm_password_prompt'));
192
+
193
+ if (newPassword !== confirmPassword) {
194
+ forceStdinCleanup();
195
+ console.log(colors.red + i18n.tSync('ui.general.passwords_mismatch') + colors.reset);
196
+ if (attempts < maxAttempts) {
197
+ console.log('');
198
+ continue;
199
+ } else {
200
+ return false;
201
+ }
202
+ }
203
+
204
+ // Success - set the password
205
+ apiManager.setExportPassword(newPassword);
206
+ console.log('');
207
+ console.log(colors.green + '✓ ' + i18n.tSync('password.setup.password_success', getTranslatedStrength(validation.strength)) + colors.reset);
208
+ return true;
209
+ }
210
+
211
+ return false;
212
+
213
+ } catch (error) {
214
+ forceStdinCleanup();
215
+ if (error.message.includes('cancelled')) {
216
+ console.log(colors.yellow + '\n' + i18n.tSync('errors.password.setup_cancelled') + colors.reset);
217
+ } else {
218
+ console.log(colors.red + `❌ Failed to set password: ${error.message}` + colors.reset);
219
+ }
220
+ return false;
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Handle password change operation
226
+ * @param {Object} apiManager - The API manager instance
227
+ * @returns {Promise<boolean>} - True if password is successfully changed, false otherwise
228
+ */
229
+ async function changePassword(apiManager) {
230
+ try {
231
+ // First verify current password
232
+ const currentVerified = await verifyCurrentPassword(apiManager);
233
+ if (!currentVerified) {
234
+ return false;
235
+ }
236
+
237
+ console.log(colors.green + i18n.tSync('errors.password.current_password_verified') + colors.reset);
238
+ console.log('');
239
+
240
+ // Set new password
241
+ return await setupNewPassword(apiManager, false);
242
+
243
+ } catch (error) {
244
+ forceStdinCleanup();
245
+ console.log(colors.red + `❌ Password change failed: ${error.message}` + colors.reset);
246
+ return false;
247
+ }
248
+ }
249
+
250
+ module.exports = {
251
+ verifyExportPassword,
252
+ verifyCurrentPassword,
253
+ setupNewPassword,
254
+ changePassword
255
+ };
package/lib/crypto.js ADDED
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Crypto Module - Handles encryption and decryption for sensitive data
3
+ */
4
+
5
+ const crypto = require('crypto');
6
+ const os = require('os');
7
+
8
+ /**
9
+ * Generate encryption key from machine-specific data
10
+ */
11
+ function getEncryptionKey() {
12
+ const machineId = os.hostname() + os.userInfo().username + os.platform();
13
+ return crypto.pbkdf2Sync(machineId, 'claude-launcher-salt', 10000, 32, 'sha256');
14
+ }
15
+
16
+ /**
17
+ * Encrypt data using AES-256-CBC
18
+ * @param {string} plaintext - The text to encrypt
19
+ * @returns {object} Result object with success status and encrypted value or error
20
+ */
21
+ function encrypt(plaintext) {
22
+ try {
23
+ const key = getEncryptionKey();
24
+ const iv = crypto.randomBytes(16);
25
+ const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
26
+
27
+ let encrypted = cipher.update(plaintext, 'utf8', 'hex');
28
+ encrypted += cipher.final('hex');
29
+
30
+ const result = iv.toString('hex') + ':' + encrypted;
31
+
32
+ return {
33
+ success: true,
34
+ value: result,
35
+ error: null
36
+ };
37
+ } catch (error) {
38
+ return {
39
+ success: false,
40
+ value: null,
41
+ error: error.message
42
+ };
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Decrypt data using AES-256-CBC
48
+ * @param {string} encryptedData - The encrypted data to decrypt
49
+ * @returns {object} Result object with success status and decrypted value or error
50
+ */
51
+ function decrypt(encryptedData) {
52
+ try {
53
+ const key = getEncryptionKey();
54
+
55
+ const parts = encryptedData.split(':');
56
+ if (parts.length !== 2) {
57
+ throw new Error('Invalid encrypted data format');
58
+ }
59
+
60
+ const iv = Buffer.from(parts[0], 'hex');
61
+ const encrypted = parts[1];
62
+
63
+ const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
64
+
65
+ let decrypted = decipher.update(encrypted, 'hex', 'utf8');
66
+ decrypted += decipher.final('utf8');
67
+
68
+ return {
69
+ success: true,
70
+ value: decrypted,
71
+ error: null
72
+ };
73
+ } catch (error) {
74
+ return {
75
+ success: false,
76
+ value: null,
77
+ error: error.message
78
+ };
79
+ }
80
+ }
81
+
82
+ module.exports = {
83
+ encrypt,
84
+ decrypt
85
+ };