7e79fe89ca
- 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.
156 lines
4.8 KiB
Swift
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"
|
|
}
|
|
}
|
|
}
|
|
}
|