Consolidates the mac.oai.pm subdomain references (introduced in the PolyForm Noncommercial relicense) to the root oai.pm domain, across the LICENSE file, README, and all Swift source file headers. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
128 lines
3.9 KiB
Swift
128 lines
3.9 KiB
Swift
//
|
|
// EncryptionService.swift
|
|
// oAI
|
|
//
|
|
// Secure encryption for sensitive data (API keys)
|
|
// Uses CryptoKit with machine-specific key derivation
|
|
//
|
|
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
|
// Copyright (C) 2026 Rune Olsen
|
|
//
|
|
// This file is part of oAI.
|
|
//
|
|
// oAI is licensed under the PolyForm Noncommercial License 1.0.0.
|
|
// You may use, study, modify, and share it for any noncommercial
|
|
// purpose. Commercial use — including selling oAI or any part of
|
|
// it, standalone or bundled into another product or service —
|
|
// requires a separate commercial license from the copyright holder.
|
|
//
|
|
// See the LICENSE file or
|
|
// <https://polyformproject.org/licenses/noncommercial/1.0.0> for
|
|
// the full license text. For commercial licensing, contact Rune
|
|
// Olsen via <https://oai.pm>.
|
|
|
|
|
|
import Foundation
|
|
import CryptoKit
|
|
import IOKit
|
|
|
|
class EncryptionService {
|
|
nonisolated static let shared = EncryptionService()
|
|
|
|
private let encryptionKey: SymmetricKey
|
|
|
|
private init() {
|
|
self.encryptionKey = Self.deriveEncryptionKey()
|
|
}
|
|
|
|
// MARK: - Public Interface
|
|
|
|
/// Encrypt a string value
|
|
nonisolated func encrypt(_ value: String) throws -> String {
|
|
guard let data = value.data(using: .utf8) else {
|
|
throw EncryptionError.invalidInput
|
|
}
|
|
|
|
let sealedBox = try AES.GCM.seal(data, using: encryptionKey)
|
|
guard let combined = sealedBox.combined else {
|
|
throw EncryptionError.encryptionFailed
|
|
}
|
|
|
|
return combined.base64EncodedString()
|
|
}
|
|
|
|
/// Decrypt a string value
|
|
nonisolated func decrypt(_ encryptedValue: String) throws -> String {
|
|
guard let data = Data(base64Encoded: encryptedValue) else {
|
|
throw EncryptionError.invalidInput
|
|
}
|
|
|
|
let sealedBox = try AES.GCM.SealedBox(combined: data)
|
|
let decryptedData = try AES.GCM.open(sealedBox, using: encryptionKey)
|
|
|
|
guard let decryptedString = String(data: decryptedData, encoding: .utf8) else {
|
|
throw EncryptionError.decryptionFailed
|
|
}
|
|
|
|
return decryptedString
|
|
}
|
|
|
|
// MARK: - Key Derivation
|
|
|
|
/// Derive encryption key from machine-specific data
|
|
private static func deriveEncryptionKey() -> SymmetricKey {
|
|
let machineUUID = getMachineUUID()
|
|
let bundleID = Bundle.main.bundleIdentifier ?? "com.oai.oAI"
|
|
let salt = "oAI-secure-storage-v1"
|
|
let keyMaterial = "\(machineUUID)-\(bundleID)-\(salt)"
|
|
let hash = SHA256.hash(data: Data(keyMaterial.utf8))
|
|
return SymmetricKey(data: hash)
|
|
}
|
|
|
|
/// Get machine-specific UUID (IOPlatformUUID)
|
|
private static func getMachineUUID() -> String {
|
|
// Get IOPlatformUUID from IOKit
|
|
let platformExpert = IOServiceGetMatchingService(
|
|
kIOMainPortDefault,
|
|
IOServiceMatching("IOPlatformExpertDevice")
|
|
)
|
|
|
|
guard platformExpert != 0 else {
|
|
// Fallback to a stable identifier if IOKit unavailable
|
|
return "oai-fallback-uuid"
|
|
}
|
|
|
|
defer { IOObjectRelease(platformExpert) }
|
|
|
|
guard let uuidData = IORegistryEntryCreateCFProperty(
|
|
platformExpert,
|
|
"IOPlatformUUID" as CFString,
|
|
kCFAllocatorDefault,
|
|
0
|
|
) else {
|
|
return "oai-fallback-uuid"
|
|
}
|
|
|
|
return (uuidData.takeRetainedValue() as? String) ?? "oai-fallback-uuid"
|
|
}
|
|
|
|
// MARK: - Errors
|
|
|
|
enum EncryptionError: LocalizedError {
|
|
case invalidInput
|
|
case encryptionFailed
|
|
case decryptionFailed
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .invalidInput:
|
|
return "Invalid input data for encryption/decryption"
|
|
case .encryptionFailed:
|
|
return "Failed to encrypt data"
|
|
case .decryptionFailed:
|
|
return "Failed to decrypt data"
|
|
}
|
|
}
|
|
}
|
|
}
|