diff --git a/voice/voice-ai/Voice AI.xcodeproj/project.pbxproj b/voice/voice-ai/Voice AI.xcodeproj/project.pbxproj index 7bdea2fb5..f90e4915c 100644 --- a/voice/voice-ai/Voice AI.xcodeproj/project.pbxproj +++ b/voice/voice-ai/Voice AI.xcodeproj/project.pbxproj @@ -148,6 +148,7 @@ CD0D13672ADA74C800031EDD /* CoreAudio.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD0D13662ADA74C800031EDD /* CoreAudio.framework */; }; CD0D13692ADA74D100031EDD /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD0D13682ADA74D100031EDD /* AVFoundation.framework */; }; CD0D136B2ADB28CE00031EDD /* logo.png in Resources */ = {isa = PBXBuildFile; fileRef = CD0D136A2ADB28CE00031EDD /* logo.png */; }; + CD8A1A652B0084C400A5B2CC /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = CD8A1A642B0084C400A5B2CC /* CryptoSwift */; }; F610490A2AF02D820087F745 /* OpenAIStreamService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F61049092AF02D820087F745 /* OpenAIStreamService.swift */; }; F610490B2AF02D820087F745 /* OpenAIStreamService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F61049092AF02D820087F745 /* OpenAIStreamService.swift */; }; F610490C2AF02D820087F745 /* OpenAIStreamService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F61049092AF02D820087F745 /* OpenAIStreamService.swift */; }; @@ -318,6 +319,7 @@ buildActionMask = 2147483647; files = ( F61049102AF02DEE0087F745 /* SwiftyJSON in Frameworks */, + CD8A1A652B0084C400A5B2CC /* CryptoSwift in Frameworks */, F67E43322AFAC166001B72CD /* SentrySwiftUI in Frameworks */, CD0D13692ADA74D100031EDD /* AVFoundation.framework in Frameworks */, B9B331A32AFB849000F6A9C9 /* StoreKit.framework in Frameworks */, @@ -702,6 +704,7 @@ F610490F2AF02DEE0087F745 /* SwiftyJSON */, F67E43312AFAC166001B72CD /* SentrySwiftUI */, F67E43332AFAC16C001B72CD /* Sentry */, + CD8A1A642B0084C400A5B2CC /* CryptoSwift */, ); productName = x; productReference = CD0D13342ADA73B300031EDD /* Voice AI.app */; @@ -793,6 +796,7 @@ packageReferences = ( F610490E2AF02DE20087F745 /* XCRemoteSwiftPackageReference "SwiftyJSON" */, F67E43302AFAB71F001B72CD /* XCRemoteSwiftPackageReference "sentry-cocoa" */, + CD8A1A632B0084A900A5B2CC /* XCRemoteSwiftPackageReference "CryptoSwift" */, ); productRefGroup = CD0D13352ADA73B300031EDD /* Products */; projectDirPath = ""; @@ -1603,6 +1607,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + CD8A1A632B0084A900A5B2CC /* XCRemoteSwiftPackageReference "CryptoSwift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/krzyzanowskim/CryptoSwift.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.8.0; + }; + }; F610490E2AF02DE20087F745 /* XCRemoteSwiftPackageReference "SwiftyJSON" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SwiftyJSON/SwiftyJSON"; @@ -1627,6 +1639,11 @@ package = F610490E2AF02DE20087F745 /* XCRemoteSwiftPackageReference "SwiftyJSON" */; productName = SwiftyJSON; }; + CD8A1A642B0084C400A5B2CC /* CryptoSwift */ = { + isa = XCSwiftPackageProductDependency; + package = CD8A1A632B0084A900A5B2CC /* XCRemoteSwiftPackageReference "CryptoSwift" */; + productName = CryptoSwift; + }; F610490F2AF02DEE0087F745 /* SwiftyJSON */ = { isa = XCSwiftPackageProductDependency; package = F610490E2AF02DE20087F745 /* XCRemoteSwiftPackageReference "SwiftyJSON" */; diff --git a/voice/voice-ai/Voice AI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/voice/voice-ai/Voice AI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4bef53db1..58a93defd 100644 --- a/voice/voice-ai/Voice AI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/voice/voice-ai/Voice AI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,14 @@ { "pins" : [ + { + "identity" : "cryptoswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/krzyzanowskim/CryptoSwift.git", + "state" : { + "revision" : "db51c407d3be4a051484a141bf0bff36c43d3b1e", + "version" : "1.8.0" + } + }, { "identity" : "sentry-cocoa", "kind" : "remoteSourceControl", diff --git a/voice/voice-ai/x/AppConfigration/AppConfig.swift b/voice/voice-ai/x/AppConfigration/AppConfig.swift index f70718f4d..e67020402 100644 --- a/voice/voice-ai/x/AppConfigration/AppConfig.swift +++ b/voice/voice-ai/x/AppConfigration/AppConfig.swift @@ -1,10 +1,16 @@ +import CryptoSwift +import DeviceCheck import Foundation import Sentry +import SwiftyJSON class AppConfig { // Shared singleton instance static let shared = AppConfig() - private var apiKey: String? + private var openaiKey: String? + private var relayUrl: String? + private var sharedEncryptionSecret: String? + private var sharedEncryptionIV: String? private var deepgramKey: String? private var minimumSignificantEvents: Int? private var daysBetweenPrompts: Int? @@ -16,6 +22,76 @@ class AppConfig { self.loadConfiguration() } + private func decrypt(base64EncodedEncryptedKey: String) throws -> String { + let d = Data(base64Encoded: base64EncodedEncryptedKey) + guard let d = d else { + throw NSError(domain: "Invalid encoded encrypted key", code: -1) + } + let encryptedKey = String(data: d, encoding: .utf8) + guard let encryptedKey = encryptedKey else { + throw NSError(domain: "Malformed key encoding", code: -2) + } + let iv: [UInt8] = Array(self.sharedEncryptionIV!.utf8) + let sharedKey: [UInt8] = Array(self.sharedEncryptionSecret!.utf8) + let aes = try AES(key: sharedKey, blockMode: GCM(iv: iv)) + let dBytes = try aes.decrypt(encryptedKey.bytes) + let dKey = String(data: Data(dBytes), encoding: .utf8) + guard let key = dKey else { + throw NSError(domain: "Key is not a string", code: -3) + } + return key + } + + private func requestOpenAIKey() async { + guard let relayUrl = self.relayUrl else { + print("Relay URL not set") + SentrySDK.capture(message: "Relay URL not set") + return + } + let s = URLSession(configuration: .default) + guard let url = URL(string: "\(relayUrl)/key") else { + let error = NSError(domain: "Invalid Relay URL", code: -1, userInfo: nil) + SentrySDK.capture(message: "Invalid Relay URL") + return + } + var token = "" + do { + let d = try await DCDevice.current.generateToken() + print("token", d) + token = d.base64EncodedString() + } catch { + SentrySDK.capture(message: "Error generating device token") + print(error) + return + } + var r = URLRequest(url: url) + r.setValue(token, forHTTPHeaderField: "X-DEVICE-TOKEN") + s.dataTask(with: r) { data, _, err in + if let err = err { + print("[AppConfig][requestOpenAIKey] cannot get key", err) + SentrySDK.capture(message: "Cannot get key. Error: \(err)") + return + } + do { + let res = try JSON(data: data!) + let eeKey = res["key"].string + guard let eeKey = eeKey else { + print("[AppConfig][requestOpenAIKey] response has no key", res) + SentrySDK.capture(message: "[AppConfig][requestOpenAIKey] response has no key") + return + } + // TODO: decrypt key + let key = try self.decrypt(base64EncodedEncryptedKey: eeKey) + self.openaiKey = key + print("Got key", key) + } catch { + print("[AppConfig][requestOpenAIKey] error processing key response", error) + SentrySDK.capture(message: "[AppConfig][requestOpenAIKey] error processing key response \(error)") + } + } +// s.dataTask(with: "") + } + private func loadConfiguration() { guard let path = Bundle.main.path(forResource: "AppConfig", ofType: "plist") else { fatalError("Unable to locate plist file") @@ -29,10 +105,12 @@ class AppConfig { fatalError("Unable to convert plist into dictionary") } - self.apiKey = dictionary["API_KEY"] - self.sentryDSN = dictionary["SENTRY_DSN"] + self.sharedEncryptionSecret = dictionary["SHARED_ENCRYPTION_SECRET"] + self.sharedEncryptionIV = dictionary["SHARED_ENCRYPTION_IV"] + self.relayUrl = dictionary["RELAY_URL"] + self.themeName = dictionary["THEME_NAME"] self.deepgramKey = dictionary["DEEPGRAM_KEY"] @@ -55,8 +133,8 @@ class AppConfig { } } - func getAPIKey() -> String? { - return self.apiKey + func getOpenAIKey() -> String? { + return self.openaiKey } func getSentryDSN() -> String? { diff --git a/voice/voice-ai/x/OpenAIService/OpenAIService.swift b/voice/voice-ai/x/OpenAIService/OpenAIService.swift index 19d8a803b..161f3e4f9 100644 --- a/voice/voice-ai/x/OpenAIService/OpenAIService.swift +++ b/voice/voice-ai/x/OpenAIService/OpenAIService.swift @@ -6,7 +6,7 @@ struct OpenAIService { // private var conversation: [Message] // Function to send input text to OpenAI for processing mutating func sendToOpenAI(conversation: [Message], completion: @escaping (String?, Error?) -> Void) { - guard let openAI_APIKey = AppConfig.shared.getAPIKey() else { + guard let openAI_APIKey = AppConfig.shared.getOpenAIKey() else { completion(nil, nil) SentrySDK.capture(message: "Open AI Api key is null") return diff --git a/voice/voice-ai/x/OpenAIService/OpenAIStreamService.swift b/voice/voice-ai/x/OpenAIService/OpenAIStreamService.swift index 3b082a686..3f9e630b4 100644 --- a/voice/voice-ai/x/OpenAIService/OpenAIStreamService.swift +++ b/voice/voice-ai/x/OpenAIService/OpenAIStreamService.swift @@ -12,7 +12,7 @@ protocol NetworkService { class OpenAIStreamService: NSObject, URLSessionDataDelegate { private var task: URLSessionDataTask? private var completion: (String?, Error?) -> Void - private let apiKey = AppConfig.shared.getAPIKey() + private let apiKey = AppConfig.shared.getOpenAIKey() private var temperature: Double private let networkService: NetworkService? diff --git a/voice/voice-ai/xTests/AppConfigTests.swift b/voice/voice-ai/xTests/AppConfigTests.swift index 6a4260f84..39014e4e4 100644 --- a/voice/voice-ai/xTests/AppConfigTests.swift +++ b/voice/voice-ai/xTests/AppConfigTests.swift @@ -16,7 +16,7 @@ class AppConfigTests: XCTestCase { } func testAPIKeyIsNotNil() { - XCTAssertNotNil(appConfig.getAPIKey(), "API Key should not be nil") + XCTAssertNotNil(appConfig.getOpenAIKey(), "API Key should not be nil") } func testDeepgramKeyIsNotNil() { @@ -40,7 +40,7 @@ class AppConfigTests: XCTestCase { } func testLoadingValidPlistFile() { - XCTAssertNotNil(appConfig.getAPIKey(), "API Key should not be nil") + XCTAssertNotNil(appConfig.getOpenAIKey(), "API Key should not be nil") XCTAssertNotNil(appConfig.getDeepgramKey(), "Deepgram Key should not be nil") XCTAssertNotNil(appConfig.getThemeName(), "Theme Name should not be nil") // XCTAssertNotNil(appConfig.getSentryDSN(), "Sentry DSN should not be nil")