Files
CxIDE/Services/CredentialStore.swift
T
cx-git-agent 1af1db7ffc Add comprehensive AWS integration (S3, Route53, CloudFront, Lambda, ACM) with 72 new tests
- AWSService: SigV4 request signing, XML parser, credential management
- AWSS3Service: bucket/object CRUD, website hosting, deploy pipeline
- AWSRoute53Service: hosted zones, DNS records, S3/CloudFront aliases
- AWSCloudFrontService: distributions, cache invalidation, ACM certs
- AWSLambdaService: functions, invoke, zip deploy
- AWSTools: 15 MCP agent tools for all AWS operations
- CredentialStore: AWS credential convenience properties
- AWSAndWebsiteTests: 72 tests covering models, XML parsing, errors,
  credentials, templates, deploy providers (165 total tests passing)
2026-04-21 19:40:51 -05:00

168 lines
5.3 KiB
Swift

// CredentialStore.swift
// CxIDE Encrypted .env file credential management
//
// Stores API keys and secrets in an AES-256 encrypted .env file
// within the workspace. Never stores credentials in plain text in source.
import Foundation
import CryptoKit
// MARK: - Credential Store
final class CredentialStore: @unchecked Sendable {
static let shared = CredentialStore()
private let fileName = ".cxide-credentials.enc"
private var cachedCredentials: [String: String] = [:]
private let lock = NSLock()
// Derive encryption key from machine-specific seed + app bundle ID
private var encryptionKey: SymmetricKey {
let seed = ProcessInfo.processInfo.hostName
+ (Bundle.main.bundleIdentifier ?? "com.cxide.CxIDE")
+ NSUserName()
let hash = SHA256.hash(data: Data(seed.utf8))
return SymmetricKey(data: hash)
}
// MARK: - Public API
/// Load credentials from encrypted file in the given directory
func load(from directory: URL) throws {
let fileURL = directory.appendingPathComponent(fileName)
guard FileManager.default.fileExists(atPath: fileURL.path) else {
lock.lock()
cachedCredentials = [:]
lock.unlock()
return
}
let encryptedData = try Data(contentsOf: fileURL)
let sealedBox = try AES.GCM.SealedBox(combined: encryptedData)
let decrypted = try AES.GCM.open(sealedBox, using: encryptionKey)
let json = try JSONSerialization.jsonObject(with: decrypted) as? [String: String] ?? [:]
lock.lock()
cachedCredentials = json
lock.unlock()
}
/// Save current credentials to encrypted file
func save(to directory: URL) throws {
let fileURL = directory.appendingPathComponent(fileName)
lock.lock()
let creds = cachedCredentials
lock.unlock()
let json = try JSONSerialization.data(withJSONObject: creds, options: .sortedKeys)
let sealedBox = try AES.GCM.seal(json, using: encryptionKey)
guard let combined = sealedBox.combined else {
throw CredentialError.encryptionFailed
}
try combined.write(to: fileURL)
// Set restrictive permissions (owner read/write only)
try FileManager.default.setAttributes(
[.posixPermissions: 0o600],
ofItemAtPath: fileURL.path
)
}
/// Get a credential value
func get(_ key: String) -> String? {
lock.lock()
defer { lock.unlock() }
return cachedCredentials[key]
}
/// Set a credential value
func set(_ key: String, value: String) {
lock.lock()
cachedCredentials[key] = value
lock.unlock()
}
/// Remove a credential
func remove(_ key: String) {
lock.lock()
cachedCredentials.removeValue(forKey: key)
lock.unlock()
}
/// List all credential keys (not values)
func keys() -> [String] {
lock.lock()
defer { lock.unlock() }
return Array(cachedCredentials.keys).sorted()
}
/// Check if a credential exists
func has(_ key: String) -> Bool {
lock.lock()
defer { lock.unlock() }
return cachedCredentials[key] != nil
}
/// Remove the encrypted file
func deleteStore(in directory: URL) throws {
let fileURL = directory.appendingPathComponent(fileName)
if FileManager.default.fileExists(atPath: fileURL.path) {
try FileManager.default.removeItem(at: fileURL)
}
lock.lock()
cachedCredentials = [:]
lock.unlock()
}
// MARK: - Convenience: GoDaddy
/// GoDaddy OTE (test environment) credentials
var godaddyOTEKey: String? { self.get("GODADDY_OTE_KEY") }
var godaddyOTESecret: String? { self.get("GODADDY_OTE_SECRET") }
/// GoDaddy Production credentials
var godaddyProdKey: String? { self.get("GODADDY_PROD_KEY") }
var godaddyProdSecret: String? { self.get("GODADDY_PROD_SECRET") }
/// Set GoDaddy OTE credentials
func setGoDaddyOTE(key: String, secret: String) {
set("GODADDY_OTE_KEY", value: key)
set("GODADDY_OTE_SECRET", value: secret)
}
/// Set GoDaddy Production credentials
func setGoDaddyProduction(key: String, secret: String) {
set("GODADDY_PROD_KEY", value: key)
set("GODADDY_PROD_SECRET", value: secret)
}
// MARK: - Convenience: AWS
var awsAccessKeyId: String? { self.get("AWS_ACCESS_KEY_ID") }
var awsSecretAccessKey: String? { self.get("AWS_SECRET_ACCESS_KEY") }
var awsDefaultRegion: String { self.get("AWS_DEFAULT_REGION") ?? "us-east-1" }
func setAWS(accessKeyId: String, secretAccessKey: String, region: String = "us-east-1") {
set("AWS_ACCESS_KEY_ID", value: accessKeyId)
set("AWS_SECRET_ACCESS_KEY", value: secretAccessKey)
set("AWS_DEFAULT_REGION", value: region)
}
// MARK: - Errors
enum CredentialError: LocalizedError {
case encryptionFailed
case decryptionFailed
case invalidFormat
var errorDescription: String? {
switch self {
case .encryptionFailed: return "Failed to encrypt credentials"
case .decryptionFailed: return "Failed to decrypt credentials"
case .invalidFormat: return "Invalid credential file format"
}
}
}
}