Files
CxIDE/Services/CredentialStore.swift
T
cx-git-agent 7e79fe89ca Add WebsiteService for static website generation and local preview
- Implemented WebsiteService to create and manage website projects from templates.
- Added functionality to start and stop a local HTTP server for project previews.
- Defined multiple built-in website templates (blank, landing page, portfolio, blog, documentation, dashboard) with corresponding HTML, CSS, and JS generation methods.
- Introduced WebsiteProject and WebsiteTemplate models to encapsulate project data and template details.
- Included error handling for project creation and template management.
2026-04-21 19:10:12 -05:00

156 lines
4.8 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: - 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"
}
}
}
}