keychain-helper.swift 4.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
  1. import Foundation
  2. import Security
  3. import CryptoKit
  4. import Darwin
  5. // This helper's signed identity owns each Keychain item. Plaintext travels only
  6. // on private stdin/stdout, never in command arguments or logs.
  7. func fail(_ message: String) -> Never {
  8. FileHandle.standardError.write(Data((message + "\n").utf8)); exit(1)
  9. }
  10. #if !DEVELOPMENT_HELPER
  11. // The distributed helper accepts only its containing, validly signed App with
  12. // the same signing team. Copying the helper or invoking it from a shell fails.
  13. @_silgen_name("proc_pidpath")
  14. func processPath(_ pid: Int32, _ buffer: UnsafeMutableRawPointer, _ size: UInt32) -> Int32
  15. var parentPath = [CChar](repeating: 0, count: 4096)
  16. let pathLength = parentPath.withUnsafeMutableBytes { processPath(getppid(), $0.baseAddress!, UInt32($0.count)) }
  17. guard pathLength > 0 else { fail("无法核对 App 身份") }
  18. let parentURL = URL(fileURLWithPath: String(cString: parentPath)).resolvingSymlinksInPath()
  19. let expected = parentURL.deletingLastPathComponent().appendingPathComponent("../Resources/runtime/keychain-helper").standardizedFileURL.resolvingSymlinksInPath()
  20. guard expected.path == URL(fileURLWithPath: CommandLine.arguments[0]).resolvingSymlinksInPath().path else { fail("加密助手只允许所属 App 调用") }
  21. var parentCode: SecCode?, ownCode: SecCode?
  22. guard SecCodeCopyGuestWithAttributes(nil, [kSecGuestAttributePid as String: getppid()] as CFDictionary, [], &parentCode) == errSecSuccess,
  23. SecCodeCopySelf([], &ownCode) == errSecSuccess, let parentCode, let ownCode,
  24. SecCodeCheckValidity(parentCode, [], nil) == errSecSuccess else { fail("App 签名无效") }
  25. func signingInfo(_ code: SecCode) -> [String: Any] {
  26. var info: CFDictionary?
  27. var staticCode: SecStaticCode?
  28. guard SecCodeCopyStaticCode(code, [], &staticCode) == errSecSuccess, let staticCode else { fail("无法读取签名身份") }
  29. guard SecCodeCopySigningInformation(staticCode, SecCSFlags(rawValue: kSecCSSigningInformation), &info) == errSecSuccess else { fail("无法核对签名身份") }
  30. return info as? [String: Any] ?? [:]
  31. }
  32. let parentInfo = signingInfo(parentCode), ownInfo = signingInfo(ownCode)
  33. guard let team = ownInfo[kSecCodeInfoTeamIdentifier as String] as? String,
  34. parentInfo[kSecCodeInfoTeamIdentifier as String] as? String == team,
  35. // The client keeps the historical identifier for upgrade continuity;
  36. // the full dual-mode build moved to its own identifier.
  37. ["cn.samien.yaoyao.desktop", "cn.samien.yaoyao.desktop.full"]
  38. .contains(parentInfo[kSecCodeInfoIdentifier as String] as? String ?? "") else { fail("App 签名身份不匹配") }
  39. #endif
  40. guard CommandLine.arguments.count == 3 else { fail("钥匙串请求格式无效") }
  41. let operation = CommandLine.arguments[1], account = CommandLine.arguments[2]
  42. guard ["encrypt", "decrypt"].contains(operation), account.range(of: "^[a-f0-9]{64}$", options: .regularExpression) != nil else { fail("钥匙串请求格式无效") }
  43. let service = "cn.samien.yaoyao.runner-key.v1"
  44. let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account]
  45. func loadKey(create: Bool) -> Data {
  46. var result: CFTypeRef?
  47. let status = SecItemCopyMatching(query.merging([kSecReturnData as String: true, kSecMatchLimit as String: kSecMatchLimitOne]) { _, next in next } as CFDictionary, &result)
  48. if status == errSecSuccess, let data = result as? Data, data.count == 32 { return data }
  49. guard status == errSecItemNotFound, create else { fail("无法读取当前数据目录的加密密钥,请检查系统钥匙串") }
  50. var key = Data(count: 32)
  51. guard key.withUnsafeMutableBytes({ SecRandomCopyBytes(kSecRandomDefault, 32, $0.baseAddress!) }) == errSecSuccess else { fail("无法创建加密密钥") }
  52. let added = SecItemAdd(query.merging([kSecValueData as String: key, kSecAttrLabel as String: "夭夭 Runner 配置密钥"]) { _, next in next } as CFDictionary, nil)
  53. if added == errSecDuplicateItem { return loadKey(create: false) }
  54. guard added == errSecSuccess else { fail("无法保存加密密钥,请检查系统钥匙串") }
  55. return key
  56. }
  57. let input = FileHandle.standardInput.readDataToEndOfFile()
  58. guard input.count <= 1_048_576 else { fail("配置超过加密大小上限") }
  59. do {
  60. let key = SymmetricKey(data: loadKey(create: operation == "encrypt"))
  61. let output: Data
  62. if operation == "encrypt" {
  63. guard let combined = try AES.GCM.seal(input, using: key).combined else { fail("配置加密失败") }
  64. output = combined
  65. } else { output = try AES.GCM.open(AES.GCM.SealedBox(combined: input), using: key) }
  66. FileHandle.standardOutput.write(output)
  67. } catch { fail("配置加解密失败,原配置保留") }