UNPKG

appstore-cli

Version:

A command-line interface (CLI) to interact with the Apple App Store Connect API.

185 lines (157 loc) 5.86 kB
import keytar from 'keytar'; import crypto from 'crypto'; import os from 'os'; // Service name for keytar const SERVICE_NAME = 'appstore-cli'; const FALLBACK_KEY = 'appstore-cli-fallback-key'; /** * Store a private key securely using the OS keychain * @param keyId The key ID to associate with this private key * @param privateKey The private key to store */ export async function storePrivateKey(keyId: string, privateKey: string): Promise<void> { try { // Try to use keytar for secure storage await keytar.setPassword(SERVICE_NAME, keyId, privateKey); } catch (error) { // If keytar fails, fall back to encrypted file storage console.warn('Keytar failed, using fallback encryption method'); await storePrivateKeyFallback(keyId, privateKey); } } /** * Retrieve a private key from secure storage * @param keyId The key ID associated with the private key * @returns The private key or null if not found */ export async function retrievePrivateKey(keyId: string): Promise<string | null> { try { // Try to use keytar for secure storage return await keytar.getPassword(SERVICE_NAME, keyId); } catch (error) { // If keytar fails, try the fallback method console.warn('Keytar failed, trying fallback decryption method'); return await retrievePrivateKeyFallback(keyId); } } /** * Delete a private key from secure storage * @param keyId The key ID associated with the private key */ export async function deletePrivateKey(keyId: string): Promise<void> { try { // Try to use keytar for secure storage await keytar.deletePassword(SERVICE_NAME, keyId); } catch (error) { // If keytar fails, try the fallback method console.warn('Keytar failed, trying fallback deletion method'); await deletePrivateKeyFallback(keyId); } } /** * Fallback method to store private key using file encryption * @param keyId The key ID to associate with this private key * @param privateKey The private key to store */ async function storePrivateKeyFallback(keyId: string, privateKey: string): Promise<void> { // Generate a key for encryption based on machine-specific information const encryptionKey = await generateMachineKey(); // Encrypt the private key const encryptedKey = encryptString(privateKey, encryptionKey); // Store the encrypted key in a file const fs = await import('fs'); const path = await import('path'); const configDir = path.join(os.homedir(), '.appstore-cli'); if (!fs.existsSync(configDir)) { fs.mkdirSync(configDir); } const fallbackFile = path.join(configDir, `key-${keyId}.enc`); fs.writeFileSync(fallbackFile, encryptedKey); } /** * Fallback method to retrieve private key using file decryption * @param keyId The key ID associated with the private key * @returns The private key or null if not found */ async function retrievePrivateKeyFallback(keyId: string): Promise<string | null> { try { const fs = await import('fs'); const path = await import('path'); const configDir = path.join(os.homedir(), '.appstore-cli'); const fallbackFile = path.join(configDir, `key-${keyId}.enc`); if (!fs.existsSync(fallbackFile)) { return null; } // Read the encrypted key const encryptedKey = fs.readFileSync(fallbackFile, 'utf8'); // Generate the decryption key const decryptionKey = await generateMachineKey(); // Decrypt the private key return decryptString(encryptedKey, decryptionKey); } catch (error) { console.error('Error retrieving private key from fallback storage:', error); return null; } } /** * Fallback method to delete private key file * @param keyId The key ID associated with the private key */ async function deletePrivateKeyFallback(keyId: string): Promise<void> { try { const fs = await import('fs'); const path = await import('path'); const configDir = path.join(os.homedir(), '.appstore-cli'); const fallbackFile = path.join(configDir, `key-${keyId}.enc`); if (fs.existsSync(fallbackFile)) { fs.unlinkSync(fallbackFile); } } catch (error) { console.error('Error deleting private key from fallback storage:', error); } } /** * Generate a machine-specific key for encryption * @returns A promise that resolves to a machine-specific key */ async function generateMachineKey(): Promise<Buffer> { // Get machine-specific information const machineId = os.hostname() + os.platform() + os.arch(); // Create a hash of the machine information return crypto.createHash('sha256').update(machineId).digest(); } /** * Encrypt a string using AES-256-CBC * @param text The text to encrypt * @param key The encryption key * @returns The encrypted text as a base64 string */ function encryptString(text: string, key: Buffer): string { // Generate a random initialization vector const iv = crypto.randomBytes(16); // Create cipher const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); // Encrypt the text let encrypted = cipher.update(text, 'utf8', 'base64'); encrypted += cipher.final('base64'); // Return iv + encrypted text as base64 return iv.toString('base64') + ':' + encrypted; } /** * Decrypt a string using AES-256-CBC * @param encryptedText The encrypted text as a base64 string * @param key The decryption key * @returns The decrypted text */ function decryptString(encryptedText: string, key: Buffer): string { // Split the iv and encrypted text const parts = encryptedText.split(':'); const iv = Buffer.from(parts[0], 'base64'); const encrypted = parts[1]; // Create decipher const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv); // Decrypt the text let decrypted = decipher.update(encrypted, 'base64', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; }