appstore-cli
Version:
A command-line interface (CLI) to interact with the Apple App Store Connect API.
185 lines (157 loc) • 5.86 kB
text/typescript
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;
}