OctagonTests.swift   [plain text]


// Copyright (C) 2018 Apple Inc. All Rights Reserved.

import CloudDeviceTest
import CoreDeviceAutomation
import CoreDeviceAutomationFrameworkFacade
import Foundation
import OctagonTestHarnessXPCServiceProtocol

extension CDAIOSDevice {
    func octagonFacade(block: @escaping (AnyObject) -> Void) throws {
        try self.withFrameworkFacade(bundlePath: "/AppleInternal/Library/Frameworks/OctagonTestHarness.framework",
                                     xpcService: "com.apple.trieste.OctagonTestHarnessXPCService",
                                     ofType: OctagonTestHarnessXPCServiceProtocol.self,
                                     block: block)
    }
}

final class OctagonTests: CDTTestCase {

    let username: String? = nil
    let password: String? = nil
    var signedIn: Bool = false

    let ckksTool = "/usr/sbin/ckksctl"
    let securityTool = "/usr/local/bin/security"

    private func getDevice() throws -> CDAIOSDevice {
        let common = CDAIOSCapabilities(
            .buildTrain("Yukon"),
            .location(country: "SE", building: "AP1"),
            .installedFrameworkFacade(true),
            .allowAnyCodeSignature(true)
        )

        if let rawPrkitUrl = ProcessInfo.processInfo.environment["PRKIT_URL"] {
            if let prkitUrl = URL(string: rawPrkitUrl) {
                let capabilities = common

                let device = try self.trieste.getIOSDevice(capabilities: capabilities)

                addTeardownBlock {
                    device.relinquish()
                }

                try device.installPRKit(at: prkitUrl)

                return device
            } else {
                throw CDTFail("PRKIT_URL was set, but can't be parsed as URL, aborting: '\(rawPrkitUrl)'")
            }
        } else {
            let device: CDAIOSDevice

            if !ProcessInfo.processInfo.environment.keys.contains("NO_XCODE") {
                let capabilities = common.merging(CDAIOSCapabilities(
                    .setup(.complete),
                    .collectSysdiagnose()
                ))

                device = try self.trieste.getIOSDevice(capabilities: capabilities)

                let targets = CDAInstalledXcodeTargetsSpec(
                    CDAInstalledXcodeTargetsSpec.Target(projectFile: "Security.xcodeproj", configuration: "Debug", scheme: "OctagonTestHarness", sdk: "iphoneos13.0.internal", roots:
                        CDAInstalledXcodeTargetsSpec.Root(product: "OctagonTestHarness.framework", installDirectory: "/AppleInternal/Library/Frameworks"),
                                                        CDAInstalledXcodeTargetsSpec.Root(product: "Security.framework", installDirectory: "/System/Library/Frameworks")
                    )
                )

                try device.installXcodeTargets(targets, rebootWhenDone: true)
            } else {
                let capabilities = common.merging(CDAIOSCapabilities(
                    .setup(.complete),
                    .collectSysdiagnose()
                ))

                device = try self.trieste.getIOSDevice(capabilities: capabilities)
            }

            addTeardownBlock {
                if self.signedIn {
                    do {
                        let listAccounts = try device.executeFile(atPath: "/usr/local/bin/accounts_tool", withArguments: ["--no-confirmation", "deleteAccountsForUsername", self.username!])
                        XCTAssertEqual(listAccounts.returnCode, 0, "deleteAccountsForUsername")
                        print("accounts_tool deleteAccountsForUsername\n\(String(data: listAccounts.standardOutput, encoding: .utf8)!)\n")
                    } catch {
                    }
                }

                device.relinquish()
            }

            if self.username != nil {
                CDALog(at: .infoLevel, "Signing in to iCloud here \(self.username!)")

                let listAccounts = try device.executeFile(atPath: "/usr/local/bin/accounts_tool", withArguments: ["listAccounts", "-v"])
                print("accounts_tool listAccounts -v\n\(String(data: listAccounts.standardOutput, encoding: .utf8)!)\n")

                let result = try device.executeFile(atPath: "/usr/local/bin/appleAccountSetupTool", withArguments: [self.username!, self.password!])
                XCTAssertEqual(result.returnCode, 0, "appleAccountSetupTool failed")
                print("appleAccountSetupTool\n\(String(data: result.standardOutput, encoding: .utf8)!)\n")

                let enableKVS = try device.executeFile(atPath: "/bin/sh", withArguments: ["-c", "accounts_tool enableDataclass  $(accounts_tool listAccountsForType com.apple.account.AppleAccount | grep identifier: | cut -f2 -d: ) com.apple.Dataclass.KeyValue"])
                XCTAssertEqual(enableKVS.returnCode, 0, "enableKVS failed")
                print("enableKVS\n\(String(data: enableKVS.standardOutput, encoding: .utf8)!)\n")

                self.signedIn = true
            }

            return device
        }
    }

    func compareCKKSZone(name zone: String, status1: NSDictionary, status2: NSDictionary) -> Bool {

        let zone1 = status1[zone] as! NSDictionary
        let zone2 = status2[zone] as! NSDictionary

        let keystate1 = zone1["keystate"] as! NSString
        let keystate2 = zone2["keystate"] as! NSString

        XCTAssertEqual(keystate1, "ready", "keystate should be ready for zone \(zone)")
        XCTAssertEqual(keystate2, "ready", "keystate should be ready for zone \(zone)")

        let ckaccountstatus1 = zone1["ckaccountstatus"] as! NSString
        let ckaccountstatus2 = zone2["ckaccountstatus"] as! NSString

        XCTAssertEqual(ckaccountstatus1, "logged in", "ckaccountstatus should be 'logged in' for zone \(zone)")
        XCTAssertEqual(ckaccountstatus2, "logged in", "ckaccountstatus should be 'logged in' for zone \(zone)")

        let currentTLK1 = zone1["currentTLK"] as? NSString
        let currentTLK2 = zone2["currentTLK"] as? NSString

        XCTAssertEqual(currentTLK1, currentTLK2, "TLK for zone \(zone) should be the same")

        return true
    }

    func compareCKKSStatus(c1: NSDictionary, c2: NSDictionary) -> Bool {

        let status1 = c1["status"] as! NSDictionary
        let status2 = c2["status"] as! NSDictionary

        XCTAssert(compareCKKSZone(name: "ApplePay", status1: status1, status2: status2))
        XCTAssert(compareCKKSZone(name: "Home", status1: status1, status2: status2))
        XCTAssert(compareCKKSZone(name: "Manatee", status1: status1, status2: status2))
        XCTAssert(compareCKKSZone(name: "AutoUnlock", status1: status1, status2: status2))

        return true
    }

    func sosStatus(_ device: CDAIOSDevice, verbose: Bool = false) throws {
        let result = try device.executeFile(atPath: securityTool, withArguments: ["sync", "-i"])
        if verbose {
            print("security sync -i\n\(String(data: result.standardOutput, encoding: .utf8)!)\n")
        }
    }
    func ckksStatus(_ device: CDAIOSDevice, verbose: Bool = false) throws -> NSDictionary {
        let ckks = try device.executeFile(atPath: self.ckksTool, withArguments: ["status", "--json"])
        if verbose {
            print("ckks status\n\(String(data: ckks.standardOutput, encoding: .utf8)!)\n")
        }

        return try JSONSerialization.jsonObject(with: ckks.standardOutput) as! NSDictionary
    }

    func sosApplication(_ device: CDAIOSDevice, verbose: Bool = false) throws {
        if self.password != nil {

            print("submitting application\n")

            let password = try device.executeFile(atPath: securityTool, withArguments: ["sync", "-P", self.password!])
            XCTAssertEqual(password.returnCode, 0, "setting password worked")

            let application = try device.executeFile(atPath: securityTool, withArguments: ["sync", "-e"])
            XCTAssertEqual(application.returnCode, 0, "submissing application worked password worked")

            try self.sosStatus(device, verbose: true)

            print("waiting after application done\n")
            sleep(4)
        }
    }

    func sosApprove(_ device: CDAIOSDevice, verbose: Bool = false) throws {
        if self.password != nil {

            print("approving applications\n")

            let password = try device.executeFile(atPath: securityTool, withArguments: ["sync", "-P", self.password!])
            XCTAssertEqual(password.returnCode, 0, "setting password worked")

            let approve = try device.executeFile(atPath: securityTool, withArguments: ["sync", "-a"])
            XCTAssertEqual(approve.returnCode, 0, "submissing application worked password worked")

            try self.sosStatus(device, verbose: true)

            print("waiting after approving applications\n")
            sleep(4)
        }
    }

    func forceResetSOS(_ device: CDAIOSDevice, resetCKKS: Bool = false) throws {
        if self.password != nil {
            _ = try device.executeFile(atPath: securityTool, withArguments: ["sync", "-P", self.password!])
            _ = try device.executeFile(atPath: securityTool, withArguments: ["sync", "-R"])
            _ = try device.executeFile(atPath: securityTool, withArguments: ["sync", "-C"])
            _ = try device.executeFile(atPath: securityTool, withArguments: ["sync", "-P", self.password!])
            _ = try device.executeFile(atPath: securityTool, withArguments: ["sync", "-R"])
            _ = try device.executeFile(atPath: securityTool, withArguments: ["sync", "-O"])

            try self.sosStatus(device, verbose: true)

            print("sleeping some to allow cdpd, cloudd and friends to catch up \n")
            sleep(4)

            if resetCKKS {
                _ = try device.executeFile(atPath: self.ckksTool, withArguments: ["reset-cloudkit"])

                print("sleeps some after ckksctl reset (should be removed)\n")
                sleep(4)
            }
        }
    }

    func testBasiciCloudSignInSignOut() throws {
        let device = try getDevice()

        let result = try device.executeFile(atPath: "/usr/local/bin/accounts_tool", withArguments: ["listAccounts", "-v"])
        print("accounts_tool listAccounts -v\n\(String(data: result.standardOutput, encoding: .utf8)!)\n")

        try forceResetSOS(device, resetCKKS: true)

        _ = try self.ckksStatus(device)
    }

    func test2DeviceSOS() throws {
        if self.password == nil {
            print("this test only works with password")
            return
        }

        let device1 = try getDevice()
        let device2 = try getDevice()

        try forceResetSOS(device1, resetCKKS: true)
        try sosApplication(device2)
        try sosApprove(device1, verbose: true)
        try sosStatus(device2, verbose: true)

        print("waiting some \(Date())")
        Thread.sleep(until: Date().addingTimeInterval(30.0)) // CK really really slow to get us data
        print("waiting done \(Date())")

        let ckks1 = try ckksStatus(device1)
        let ckks2 = try ckksStatus(device2)

        print("comparing status \(Date())")
        XCTAssert(self.compareCKKSStatus(c1: ckks1, c2: ckks2), "compare of CKKS status")

        print("Done \(Date())")
    }

    func testOctagonReset() throws {
        let device = try getDevice()

        try forceResetSOS(device, resetCKKS: true)

        CDALog(at: .infoLevel, "Obtained signed in device \(device)")

        try device.octagonFacade { octagon in
            CDALog(at: .infoLevel, "Obtained framework facade \(octagon)")

            for i in 0..<2 {
                CDALog(at: .infoLevel, "Reset \(i)")
                octagon.octagonReset("altDSID") { _, error in
                    CDTAssert(error == nil, "Octagon wasn't reset, error was \(String(describing: error))")
                }
            }
        }

        CDALog(at: .infoLevel, "Done running")
    }
}