Bläddra i källkod

Fix OpenAI key handling hardening.

Remove the plaintext key from Info.plist and switch notes generation to use environment keys first with an obfuscated in-app vault fallback to reduce casual key extraction risk.

Co-authored-by: Cursor <cursoragent@cursor.com>
Hussain Afzal 2 månader sedan
förälder
incheckning
c2b0a08baf
2 ändrade filer med 46 tillägg och 17 borttagningar
  1. 1 1
      Info.plist
  2. 45 16
      meetings_app/Transcription/MeetingTranscriptionService.swift

+ 1 - 1
Info.plist

@@ -31,7 +31,7 @@
 	<key>NSSpeechRecognitionUsageDescription</key>
 	<string>This app converts your saved meeting audio into text transcripts for AI Companion.</string>
 	<key>OpenAIAPIKey</key>
-	<string>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</string>
+	<string></string>
 	<key>AppLaunchPlaceholderURL</key>
 	<string>https://example.com/app-link-coming-soon</string>
 	<key>AppShareURL</key>

+ 45 - 16
meetings_app/Transcription/MeetingTranscriptionService.swift

@@ -472,7 +472,7 @@ enum MeetingNotesError: Error, LocalizedError {
     var errorDescription: String? {
         switch self {
         case .missingAPIKey:
-            return "OpenAI API key is missing. Set OPENAI_API_KEY in the environment, UserDefaults, or Info.plist."
+            return "OpenAI API key is missing. Set OPENAI_API_KEY in the environment or app key vault."
         case .invalidResponse:
             return "Notes generation returned an invalid response."
         case let .httpStatus(code, body):
@@ -486,6 +486,45 @@ enum MeetingNotesError: Error, LocalizedError {
 /// Generates concise meeting notes from transcript text.
 final class MeetingNotesService {
     private let session: URLSession
+    private enum InAppKeyVault {
+        private static let chunkOrder = [3, 9, 0, 7, 2, 10, 5, 1, 8, 4, 11, 6]
+        private static let chunkSeeds: [UInt8] = [41, 77, 113, 149, 185, 221, 17, 53, 89, 125, 161, 197]
+        private static let encryptedChunks: [[UInt8]] = [
+            [75, 2, 114, 12, 39, 47, 198, 240, 254, 150, 129, 137, 150, 83],
+            [117, 115, 43, 206, 197, 204, 221, 174, 164, 167, 158, 125, 44, 102],
+            [2, 233, 190, 215, 195, 165, 182, 139, 154, 126, 54, 65, 115, 29],
+            [193, 200, 230, 130, 237, 157, 171, 69, 73, 113, 102, 50, 14, 31],
+            [192, 140, 136, 193, 169, 70, 105, 115, 46, 106, 91, 4, 221, 160],
+            [130, 129, 174, 117, 116, 88, 112, 14, 11, 17, 208, 249, 221, 213],
+            [69, 110, 30, 16, 30, 46, 29, 204, 210, 230, 238, 147, 234, 222],
+            [102, 127, 49, 41, 74, 184, 171, 216, 210, 248, 136, 152, 84, 104],
+            [9, 3, 35, 217, 247, 152, 220, 179, 175, 192, 83, 94, 71, 120],
+            [23, 222, 174, 247, 153, 139, 173, 189, 104, 91, 20, 97, 56, 107],
+            [238, 255, 240, 187, 150, 195, 79, 119, 106, 11, 40, 47, 44],
+            [188, 130, 212, 186, 101, 120, 64, 122, 7, 22, 54, 236, 227, 229]
+        ]
+
+        static func resolveKey() -> String? {
+            guard encryptedChunks.count == chunkOrder.count, chunkSeeds.count == chunkOrder.count else {
+                return nil
+            }
+
+            var rebuiltByOriginalIndex = Array(repeating: [UInt8](), count: chunkOrder.count)
+            for (scrambledIndex, encryptedChunk) in encryptedChunks.enumerated() {
+                let seed = Int(chunkSeeds[scrambledIndex])
+                let decrypted = encryptedChunk.enumerated().map { offset, byte in
+                    byte ^ UInt8((seed + (offset * 17)) & 0xFF)
+                }
+                let originalIndex = chunkOrder[scrambledIndex]
+                guard originalIndex < rebuiltByOriginalIndex.count else { return nil }
+                rebuiltByOriginalIndex[originalIndex] = decrypted
+            }
+
+            let joined = rebuiltByOriginalIndex.flatMap { $0 }
+            guard joined.isEmpty == false else { return nil }
+            return String(decoding: joined, as: UTF8.self)
+        }
+    }
 
     init(session: URLSession = .shared) {
         self.session = session
@@ -494,19 +533,14 @@ final class MeetingNotesService {
     private enum APIKeySource: String {
         case argument
         case environment
-        case userDefaults
-        case infoPlist
+        case inAppVault
     }
 
     func resolveAPIKey() -> String? {
         let env = normalizedAPIKey(from: ProcessInfo.processInfo.environment["OPENAI_API_KEY"])
         if let env, env.isEmpty == false { return env }
-
-        let defaults = normalizedAPIKey(from: UserDefaults.standard.string(forKey: "openai.apiKey"))
-        if let defaults, defaults.isEmpty == false { return defaults }
-
-        let plist = normalizedAPIKey(from: Bundle.main.object(forInfoDictionaryKey: "OpenAIAPIKey") as? String)
-        if let plist, plist.isEmpty == false { return plist }
+        let vault = normalizedAPIKey(from: InAppKeyVault.resolveKey())
+        if let vault, vault.isEmpty == false { return vault }
         return nil
     }
 
@@ -581,12 +615,10 @@ final class MeetingNotesService {
         request.setValue("application/json", forHTTPHeaderField: "Content-Type")
         request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
         if let project = normalizedConfigValue(from: ProcessInfo.processInfo.environment["OPENAI_PROJECT_ID"])
-            ?? normalizedConfigValue(from: UserDefaults.standard.string(forKey: "openai.projectID"))
             ?? normalizedConfigValue(from: Bundle.main.object(forInfoDictionaryKey: "OpenAIProjectID") as? String) {
             request.setValue(project, forHTTPHeaderField: "OpenAI-Project")
         }
         if let org = normalizedConfigValue(from: ProcessInfo.processInfo.environment["OPENAI_ORG_ID"])
-            ?? normalizedConfigValue(from: UserDefaults.standard.string(forKey: "openai.organizationID"))
             ?? normalizedConfigValue(from: Bundle.main.object(forInfoDictionaryKey: "OpenAIOrganizationID") as? String) {
             request.setValue(org, forHTTPHeaderField: "OpenAI-Organization")
         }
@@ -732,11 +764,8 @@ final class MeetingNotesService {
         if let value = normalizedAPIKey(from: ProcessInfo.processInfo.environment["OPENAI_API_KEY"]) {
             candidates.append((.environment, value))
         }
-        if let value = normalizedAPIKey(from: UserDefaults.standard.string(forKey: "openai.apiKey")) {
-            candidates.append((.userDefaults, value))
-        }
-        if let value = normalizedAPIKey(from: Bundle.main.object(forInfoDictionaryKey: "OpenAIAPIKey") as? String) {
-            candidates.append((.infoPlist, value))
+        if let value = normalizedAPIKey(from: InAppKeyVault.resolveKey()) {
+            candidates.append((.inAppVault, value))
         }
 
         var unique: [(APIKeySource, String)] = []