This commit is contained in:
Lakr
2026-03-13 10:16:37 +08:00
parent eee55bd6ad
commit 02ed56b7d5
6 changed files with 759 additions and 144 deletions

1
.gitignore vendored
View File

@@ -330,3 +330,4 @@ setup_logs/
/research/artifacts
/research/xnu
/vm
/.tmp

View File

@@ -15,6 +15,9 @@ let package = Package(
.package(path: "vendor/libimg4-spm"),
.package(path: "vendor/MachOKit"),
.package(path: "vendor/AppleMobileDeviceLibrary"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"),
.package(url: "https://github.com/apple/swift-crypto.git", "1.0.0"..<"4.0.0"),
.package(path: "vendor/swift-nio-ssh"),
.package(path: "vendor/swift-subprocess"),
.package(path: "vendor/swift-trustcache"),
.package(path: "vendor/SWCompression"),
@@ -60,8 +63,12 @@ let package = Package(
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "Capstone", package: "libcapstone-spm"),
.product(name: "Crypto", package: "swift-crypto"),
.product(name: "Dynamic", package: "Dynamic"),
.product(name: "libirecovery", package: "AppleMobileDeviceLibrary"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOPosix", package: "swift-nio"),
.product(name: "NIOSSH", package: "swift-nio-ssh"),
.product(name: "SWCompression", package: "SWCompression"),
.product(name: "Subprocess", package: "swift-subprocess"),
.product(name: "TrustCache", package: "swift-trustcache"),

View File

@@ -26,15 +26,16 @@ struct CFWInstallCLI: AsyncParsableCommand {
var sshPort: Int = Int(ProcessInfo.processInfo.environment["SSH_PORT"] ?? "2222") ?? 2222
@Flag(help: "Skip halting the ramdisk after install")
var skipHalt: Bool = ProcessInfo.processInfo.environment["CFW_SKIP_HALT"] == "1"
var skipHalt = false
mutating func run() async throws {
let effectiveSkipHalt = skipHalt || ProcessInfo.processInfo.environment["CFW_SKIP_HALT"] == "1"
let installer = try VPhoneCFWInstaller(
vmDirectory: vmDirectory.standardizedFileURL,
projectRoot: projectRoot.standardizedFileURL,
variant: variant,
sshPort: sshPort,
skipHalt: skipHalt
skipHalt: effectiveSkipHalt
)
try await installer.run()
}

View File

@@ -1,4 +1,7 @@
import Foundation
import NIOCore
import NIOPosix
import NIOSSH
struct VPhoneSSHCommandResult {
let exitStatus: Int32
@@ -16,14 +19,20 @@ struct VPhoneSSHCommandResult {
enum VPhoneSSHError: Error, CustomStringConvertible {
case notConnected
case invalidChannelType
case commandFailed(String)
case timeout(String)
var description: String {
switch self {
case .notConnected:
return "SSH client is not connected"
case .invalidChannelType:
return "Invalid SSH channel type"
case let .commandFailed(message):
return message
case let .timeout(context):
return "SSH timeout: \(context)"
}
}
}
@@ -34,8 +43,13 @@ final class VPhoneSSHClient: @unchecked Sendable {
let username: String
let password: String
private var connected = false
private var controlPath: URL?
private let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
private var channel: Channel?
private var sshHandler: NIOSSHHandler?
private var shutDown = false
private var debugEnabled: Bool {
ProcessInfo.processInfo.environment["VPHONE_SSH_DEBUG"] == "1"
}
init(host: String, port: Int, username: String, password: String) {
self.host = host
@@ -44,48 +58,132 @@ final class VPhoneSSHClient: @unchecked Sendable {
self.password = password
}
deinit {
try? shutdown()
}
func connect() throws {
if connected { return }
let token = String(UUID().uuidString.replacingOccurrences(of: "-", with: "").prefix(12))
let socketPath = URL(fileURLWithPath: "/tmp/vssh.\(token).sock")
do {
let result = try runProcess(
executable: "/usr/bin/ssh",
arguments: masterArguments(controlPath: socketPath),
stdin: nil
)
guard result.exitStatus == 0 else {
throw VPhoneSSHError.commandFailed(result.standardErrorString)
guard channel == nil else { return }
let connectionState = VPhoneSSHConnectionState(eventLoop: group.next())
debug("connect begin host=\(host) port=\(port) user=\(username)")
let bootstrap = ClientBootstrap(group: group)
.channelInitializer { [username, password] channel in
let configuration = SSHClientConfiguration(
userAuthDelegate: SimplePasswordDelegate(username: username, password: password),
serverAuthDelegate: AcceptAllHostKeysDelegate(),
transportProtectionSchemes: [
AES256CTRHMACSHA256TransportProtection.self,
AES128CTRHMACSHA256TransportProtection.self,
AES256CTRHMACSHA1TransportProtection.self,
AES128CTRHMACSHA1TransportProtection.self,
]
)
let ssh = NIOSSHHandler(
role: .client(configuration),
allocator: channel.allocator,
inboundChildChannelInitializer: nil
)
connectionState.sshHandler = ssh
return channel.pipeline.addHandler(ssh).flatMap {
channel.pipeline.addHandler(VPhoneSSHConnectionHandler(readyPromise: connectionState.readyPromise))
}.flatMap {
channel.pipeline.addHandler(VPhoneSSHErrorHandler())
}
}
controlPath = socketPath
connected = true
} catch {
try? FileManager.default.removeItem(at: socketPath)
throw error
.connectTimeout(.seconds(5))
.channelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
.channelOption(ChannelOptions.socket(SocketOptionLevel(IPPROTO_TCP), TCP_NODELAY), value: 1)
let connectedChannel = try waitForFuture(
bootstrap.connect(host: host, port: port),
timeout: .seconds(8),
context: "tcp connect to \(host):\(port)"
)
debug("tcp connected")
channel = connectedChannel
_ = try waitForFuture(
connectionState.readyPromise.futureResult,
timeout: .seconds(8),
context: "SSH authentication"
)
debug("ssh authenticated")
guard let sshHandler = connectionState.sshHandler else {
throw VPhoneSSHError.notConnected
}
self.sshHandler = sshHandler
}
func shutdown() throws {
if let controlPath {
_ = try? runProcess(
executable: "/usr/bin/ssh",
arguments: controlArguments(controlPath: controlPath) + ["-O", "exit", "\(username)@\(host)"],
stdin: nil
)
guard !shutDown else { return }
shutDown = true
if let channel {
try? channel.close().wait()
self.channel = nil
self.sshHandler = nil
}
if let controlPath {
try? FileManager.default.removeItem(at: controlPath)
}
controlPath = nil
connected = false
try group.syncShutdownGracefully()
}
func execute(_ command: String, stdin: Data? = nil, requireSuccess: Bool = true) throws -> VPhoneSSHCommandResult {
guard connected else {
guard let channel, let sshHandler else {
throw VPhoneSSHError.notConnected
}
debug("execute command=\(command)")
let result = try runSSH(command: command, stdin: stdin)
let resultBox = VPhoneSSHCommandResultBox(eventLoop: channel.eventLoop)
let childPromise = channel.eventLoop.makePromise(of: Channel.self)
let createChannelPromise = channel.eventLoop.makePromise(of: Void.self)
let commandTimeout: TimeAmount
if let stdin, stdin.count > 1_000_000 {
commandTimeout = .seconds(600)
} else if stdin != nil {
commandTimeout = .seconds(120)
} else {
commandTimeout = .seconds(30)
}
channel.eventLoop.execute {
sshHandler.createChannel(childPromise) { childChannel, channelType in
guard channelType == .session else {
return channel.eventLoop.makeFailedFuture(VPhoneSSHError.invalidChannelType)
}
return childChannel.pipeline.addHandler(
VPhoneSSHExecHandler(command: command, stdinData: stdin, resultBox: resultBox)
)
}
createChannelPromise.succeed(())
}
childPromise.futureResult.whenFailure { error in
resultBox.fail(error)
}
let result: VPhoneSSHCommandResult
do {
_ = try waitForFuture(
createChannelPromise.futureResult,
timeout: .seconds(2),
context: "schedule exec channel open"
)
debug("exec channel scheduled")
let childChannel = try waitForFuture(
childPromise.futureResult,
timeout: .seconds(8),
context: "open exec channel"
)
debug("exec channel active")
result = try waitForFuture(
resultBox.futureResult,
timeout: commandTimeout,
context: "command result"
)
debug("command result exit=\(result.exitStatus)")
childChannel.close(promise: nil)
} catch {
resultBox.fail(error)
throw error
}
if requireSuccess, result.exitStatus != 0 {
let stderr = result.standardErrorString.trimmingCharacters(in: .whitespacesAndNewlines)
throw VPhoneSSHError.commandFailed(
@@ -143,109 +241,6 @@ final class VPhoneSSHClient: @unchecked Sendable {
}
}
private func runSSH(command: String, stdin: Data?) throws -> VPhoneSSHCommandResult {
let result = try runProcess(
executable: "/usr/bin/ssh",
arguments: sshArguments(command: command),
stdin: stdin
)
return VPhoneSSHCommandResult(
exitStatus: result.exitStatus,
standardOutput: result.standardOutput,
standardError: result.standardError
)
}
private func sshArguments(command: String) -> [String] {
var arguments = sshBaseArguments()
if let controlPath {
arguments.append(contentsOf: controlArguments(controlPath: controlPath))
}
arguments.append(contentsOf: [
"-p", "\(port)",
"\(username)@\(host)",
command,
])
return arguments
}
private func masterArguments(controlPath: URL) -> [String] {
var arguments = sshBaseArguments()
arguments.append(contentsOf: [
"-M",
"-S", controlPath.path,
"-o", "ControlMaster=yes",
"-o", "ControlPersist=yes",
"-p", "\(port)",
"-f",
"-N",
"\(username)@\(host)",
])
return arguments
}
private func sshBaseArguments() -> [String] {
[
"-F", "/dev/null",
"-T",
"-o", "LogLevel=ERROR",
"-o", "StrictHostKeyChecking=no",
"-o", "UserKnownHostsFile=/dev/null",
"-o", "GlobalKnownHostsFile=/dev/null",
"-o", "UpdateHostKeys=no",
"-o", "PreferredAuthentications=password,keyboard-interactive",
"-o", "PubkeyAuthentication=no",
"-o", "NumberOfPasswordPrompts=1",
"-o", "ConnectTimeout=5",
]
}
private func controlArguments(controlPath: URL) -> [String] {
["-S", controlPath.path, "-o", "ControlMaster=no"]
}
private func runProcess(executable: String, arguments: [String], stdin: Data?) throws -> VPhoneSSHCommandResult {
let process = Process()
process.executableURL = URL(fileURLWithPath: executable)
process.arguments = arguments
var environment = ProcessInfo.processInfo.environment
for (key, value) in VPhoneHost.sshAskpassEnvironment(password: password) {
if let value {
environment[key] = value
} else {
environment.removeValue(forKey: key)
}
}
process.environment = environment
let stdoutPipe = Pipe()
let stderrPipe = Pipe()
process.standardOutput = stdoutPipe
process.standardError = stderrPipe
if let stdin, !stdin.isEmpty {
let stdinPipe = Pipe()
process.standardInput = stdinPipe
try process.run()
stdinPipe.fileHandleForWriting.write(stdin)
try stdinPipe.fileHandleForWriting.close()
} else {
process.standardInput = FileHandle.nullDevice
try process.run()
}
let stdout = stdoutPipe.fileHandleForReading.readDataToEndOfFile()
let stderr = stderrPipe.fileHandleForReading.readDataToEndOfFile()
process.waitUntilExit()
return VPhoneSSHCommandResult(
exitStatus: process.terminationStatus,
standardOutput: stdout,
standardError: stderr
)
}
private func shellQuote(_ string: String) -> String {
"'" + string.replacingOccurrences(of: "'", with: "'\\''") + "'"
}
@@ -312,4 +307,264 @@ final class VPhoneSSHClient: @unchecked Sendable {
}
return remotePath
}
private func waitForFuture<Value>(
_ future: EventLoopFuture<Value>,
timeout: TimeAmount,
context: String
) throws -> Value {
let promise = future.eventLoop.makePromise(of: Value.self)
let scheduled = future.eventLoop.scheduleTask(in: timeout) {
promise.fail(VPhoneSSHError.timeout(context))
}
future.whenComplete { result in
scheduled.cancel()
switch result {
case let .success(value):
promise.succeed(value)
case let .failure(error):
promise.fail(error)
}
}
return try promise.futureResult.wait()
}
private func debug(_ message: String) {
guard debugEnabled else { return }
FileHandle.standardError.write(Data("[ssh] \(message)\n".utf8))
}
}
private final class VPhoneSSHExecHandler: ChannelDuplexHandler, @unchecked Sendable {
typealias InboundIn = SSHChannelData
typealias InboundOut = SSHChannelData
typealias OutboundIn = SSHChannelData
typealias OutboundOut = SSHChannelData
let command: String
let stdinData: Data?
let resultBox: VPhoneSSHCommandResultBox
let stdinChunkSize = 256 * 1024
var standardOutput = Data()
var standardError = Data()
var exitStatus: Int32 = 0
var completed = false
var stdinOffset = 0
init(command: String, stdinData: Data?, resultBox: VPhoneSSHCommandResultBox) {
self.command = command
self.stdinData = stdinData
self.resultBox = resultBox
}
func handlerAdded(context: ChannelHandlerContext) {
let loopBoundContext = NIOLoopBound(context, eventLoop: context.eventLoop)
context.channel.setOption(ChannelOptions.allowRemoteHalfClosure, value: true).whenFailure { error in
self.fail(error, context: loopBoundContext.value)
}
}
func channelActive(context: ChannelHandlerContext) {
let loopBoundContext = NIOLoopBound(context, eventLoop: context.eventLoop)
if ProcessInfo.processInfo.environment["VPHONE_SSH_DEBUG"] == "1" {
FileHandle.standardError.write(Data("[ssh] exec handler active command=\(command)\n".utf8))
}
let request = SSHChannelRequestEvent.ExecRequest(command: command, wantReply: true)
context.triggerUserOutboundEvent(request).whenComplete { result in
switch result {
case .success:
self.sendStandardInput(context: loopBoundContext.value)
case .failure(let error):
self.fail(error, context: loopBoundContext.value)
}
}
}
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
let message = unwrapInboundIn(data)
guard case .byteBuffer(var bytes) = message.data,
let chunk = bytes.readData(length: bytes.readableBytes)
else {
return
}
switch message.type {
case .channel:
standardOutput.append(chunk)
case .stdErr:
standardError.append(chunk)
default:
break
}
}
func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
if let exit = event as? SSHChannelRequestEvent.ExitStatus {
exitStatus = Int32(exit.exitStatus)
if ProcessInfo.processInfo.environment["VPHONE_SSH_DEBUG"] == "1" {
FileHandle.standardError.write(Data("[ssh] exit status=\(exit.exitStatus)\n".utf8))
}
} else {
context.fireUserInboundEventTriggered(event)
}
}
func channelInactive(context: ChannelHandlerContext) {
if ProcessInfo.processInfo.environment["VPHONE_SSH_DEBUG"] == "1" {
FileHandle.standardError.write(Data("[ssh] exec handler inactive command=\(command)\n".utf8))
}
succeedIfNeeded()
context.fireChannelInactive()
}
func errorCaught(context: ChannelHandlerContext, error: Error) {
fail(error, context: context)
}
private func sendStandardInput(context: ChannelHandlerContext) {
guard let stdinData, !stdinData.isEmpty else {
context.close(mode: .output, promise: nil)
return
}
writeNextStandardInputChunk(context: context, stdinData: stdinData)
}
private func writeNextStandardInputChunk(context: ChannelHandlerContext, stdinData: Data) {
if stdinOffset >= stdinData.count {
context.close(mode: .output, promise: nil)
return
}
let nextOffset = min(stdinOffset + stdinChunkSize, stdinData.count)
let chunk = stdinData[stdinOffset..<nextOffset]
stdinOffset = nextOffset
var buffer = context.channel.allocator.buffer(capacity: chunk.count)
buffer.writeBytes(chunk)
let payload = SSHChannelData(type: .channel, data: .byteBuffer(buffer))
let loopBoundContext = NIOLoopBound(context, eventLoop: context.eventLoop)
context.writeAndFlush(wrapOutboundOut(payload)).whenComplete { result in
switch result {
case .success:
self.writeNextStandardInputChunk(context: loopBoundContext.value, stdinData: stdinData)
case .failure(let error):
self.fail(error, context: loopBoundContext.value)
}
}
}
private func succeedIfNeeded() {
guard !completed else { return }
completed = true
resultBox.succeed(
VPhoneSSHCommandResult(
exitStatus: exitStatus,
standardOutput: standardOutput,
standardError: standardError
)
)
}
private func fail(_ error: Error, context: ChannelHandlerContext) {
guard !completed else { return }
completed = true
resultBox.fail(error)
context.close(promise: nil)
}
}
private final class VPhoneSSHCommandResultBox: @unchecked Sendable {
let futureResult: EventLoopFuture<VPhoneSSHCommandResult>
private let promise: EventLoopPromise<VPhoneSSHCommandResult>
private let lock = NSLock()
private var completed = false
init(eventLoop: EventLoop) {
promise = eventLoop.makePromise(of: VPhoneSSHCommandResult.self)
futureResult = promise.futureResult
}
func succeed(_ value: VPhoneSSHCommandResult) {
lock.lock()
defer { lock.unlock() }
guard !completed else { return }
completed = true
promise.succeed(value)
}
func fail(_ error: Error) {
lock.lock()
defer { lock.unlock() }
guard !completed else { return }
completed = true
promise.fail(error)
}
}
private final class VPhoneSSHConnectionState: @unchecked Sendable {
let readyPromise: EventLoopPromise<Void>
var sshHandler: NIOSSHHandler?
init(eventLoop: EventLoop) {
readyPromise = eventLoop.makePromise(of: Void.self)
}
}
private final class VPhoneSSHConnectionHandler: ChannelInboundHandler, @unchecked Sendable {
typealias InboundIn = Any
let readyPromise: EventLoopPromise<Void>
var completed = false
init(readyPromise: EventLoopPromise<Void>) {
self.readyPromise = readyPromise
}
func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
if event is UserAuthSuccessEvent {
if ProcessInfo.processInfo.environment["VPHONE_SSH_DEBUG"] == "1" {
FileHandle.standardError.write(Data("[ssh] auth success event\n".utf8))
}
succeedIfNeeded()
}
context.fireUserInboundEventTriggered(event)
}
func errorCaught(context: ChannelHandlerContext, error: Error) {
failIfNeeded(error)
context.fireErrorCaught(error)
}
func channelInactive(context: ChannelHandlerContext) {
failIfNeeded(VPhoneSSHError.notConnected)
context.fireChannelInactive()
}
private func succeedIfNeeded() {
guard !completed else { return }
completed = true
readyPromise.succeed(())
}
private func failIfNeeded(_ error: Error) {
guard !completed else { return }
completed = true
readyPromise.fail(error)
}
}
private final class AcceptAllHostKeysDelegate: NIOSSHClientServerAuthenticationDelegate {
func validateHostKey(hostKey: NIOSSHPublicKey, validationCompletePromise: EventLoopPromise<Void>) {
validationCompletePromise.succeed(())
}
}
private final class VPhoneSSHErrorHandler: ChannelInboundHandler {
typealias InboundIn = Any
func errorCaught(context: ChannelHandlerContext, error: Error) {
context.close(promise: nil)
}
}

View File

@@ -0,0 +1,350 @@
import CommonCrypto
import Crypto
import Foundation
import NIOCore
import NIOSSH
enum VPhoneSSHTransportError: Error, CustomStringConvertible {
case invalidKeySize(expected: Int, actual: Int)
case invalidIVSize(expected: Int, actual: Int)
case cryptorCreateFailed(CCCryptorStatus)
case cryptorUpdateFailed(CCCryptorStatus)
case invalidPacket
case invalidMAC
var description: String {
switch self {
case let .invalidKeySize(expected, actual):
return "invalid SSH key size: expected \(expected), got \(actual)"
case let .invalidIVSize(expected, actual):
return "invalid SSH IV size: expected \(expected), got \(actual)"
case let .cryptorCreateFailed(status):
return "failed to create SSH AES-CTR cryptor: \(status)"
case let .cryptorUpdateFailed(status):
return "failed to update SSH AES-CTR cryptor: \(status)"
case .invalidPacket:
return "invalid SSH packet"
case .invalidMAC:
return "invalid SSH packet MAC"
}
}
}
class AESCTRTransportProtectionBase: NIOSSHTransportProtection {
class var cipherName: String { fatalError("Override cipherName") }
class var macName: String? { fatalError("Override macName") }
class var keySizes: ExpectedKeySizes { fatalError("Override keySizes") }
class var macAlgorithm: CCHmacAlgorithm { fatalError("Override macAlgorithm") }
class var macLength: Int { fatalError("Override macLength") }
static var cipherBlockSize: Int { 16 }
var macBytes: Int { Self.macLength }
var lengthEncrypted: Bool { true }
private var inboundCipher: AESCTRStreamCipher
private var outboundCipher: AESCTRStreamCipher
private var inboundMACKey: Data
private var outboundMACKey: Data
required init(initialKeys: NIOSSHSessionKeys) throws {
let configured = try Self.makeState(from: initialKeys)
self.inboundCipher = configured.inboundCipher
self.outboundCipher = configured.outboundCipher
self.inboundMACKey = configured.inboundMACKey
self.outboundMACKey = configured.outboundMACKey
}
func updateKeys(_ newKeys: NIOSSHSessionKeys) throws {
let configured = try Self.makeState(from: newKeys)
self.inboundCipher = configured.inboundCipher
self.outboundCipher = configured.outboundCipher
self.inboundMACKey = configured.inboundMACKey
self.outboundMACKey = configured.outboundMACKey
}
func decryptFirstBlock(_ source: inout ByteBuffer) throws {
let start = source.readerIndex
guard let encrypted = source.getBytes(at: start, length: Self.cipherBlockSize) else {
throw VPhoneSSHTransportError.invalidPacket
}
let plaintext = try inboundCipher.update(encrypted)
source.setBytes(plaintext, at: start)
}
func decryptAndVerifyRemainingPacket(_ source: inout ByteBuffer, sequenceNumber: UInt32) throws -> ByteBuffer {
let packetByteCount = source.readableBytes - Self.macLength
guard packetByteCount >= Self.cipherBlockSize else {
throw VPhoneSSHTransportError.invalidPacket
}
let remainingCiphertextCount = packetByteCount - Self.cipherBlockSize
if remainingCiphertextCount > 0 {
let offset = source.readerIndex + Self.cipherBlockSize
guard let encrypted = source.getBytes(at: offset, length: remainingCiphertextCount) else {
throw VPhoneSSHTransportError.invalidPacket
}
let plaintext = try inboundCipher.update(encrypted)
source.setBytes(plaintext, at: offset)
}
let packetStart = source.readerIndex
guard let plaintextPacket = source.getBytes(at: packetStart, length: packetByteCount),
let receivedMAC = source.getBytes(at: packetStart + packetByteCount, length: Self.macLength)
else {
throw VPhoneSSHTransportError.invalidPacket
}
let expectedMAC = Self.computeMAC(
key: inboundMACKey,
sequenceNumber: sequenceNumber,
packet: plaintextPacket
)
guard Self.constantTimeEqual(expectedMAC, receivedMAC) else {
throw VPhoneSSHTransportError.invalidMAC
}
guard var packetBuffer = source.readSlice(length: packetByteCount) else {
throw VPhoneSSHTransportError.invalidPacket
}
source.moveReaderIndex(forwardBy: Self.macLength)
return try Self.payloadBuffer(fromPlaintextPacket: &packetBuffer)
}
func encryptPacket(_ destination: inout ByteBuffer, sequenceNumber: UInt32) throws {
let packetStart = destination.readerIndex
let packetByteCount = destination.readableBytes
guard let plaintextPacket = destination.getBytes(at: packetStart, length: packetByteCount) else {
throw VPhoneSSHTransportError.invalidPacket
}
let mac = Self.computeMAC(
key: outboundMACKey,
sequenceNumber: sequenceNumber,
packet: plaintextPacket
)
let ciphertext = try outboundCipher.update(plaintextPacket)
destination.setBytes(ciphertext, at: packetStart)
destination.writeBytes(mac)
}
private static func makeState(
from keys: NIOSSHSessionKeys
) throws -> (
inboundCipher: AESCTRStreamCipher,
outboundCipher: AESCTRStreamCipher,
inboundMACKey: Data,
outboundMACKey: Data
) {
let inboundEncryptionKey = Data(keys.inboundEncryptionKey.withUnsafeBytes { Array($0) })
let outboundEncryptionKey = Data(keys.outboundEncryptionKey.withUnsafeBytes { Array($0) })
let inboundMACKey = Data(keys.inboundMACKey.withUnsafeBytes { Array($0) })
let outboundMACKey = Data(keys.outboundMACKey.withUnsafeBytes { Array($0) })
guard inboundEncryptionKey.count == Self.keySizes.encryptionKeySize else {
throw VPhoneSSHTransportError.invalidKeySize(
expected: Self.keySizes.encryptionKeySize,
actual: inboundEncryptionKey.count
)
}
guard outboundEncryptionKey.count == Self.keySizes.encryptionKeySize else {
throw VPhoneSSHTransportError.invalidKeySize(
expected: Self.keySizes.encryptionKeySize,
actual: outboundEncryptionKey.count
)
}
guard inboundMACKey.count == Self.keySizes.macKeySize else {
throw VPhoneSSHTransportError.invalidKeySize(
expected: Self.keySizes.macKeySize,
actual: inboundMACKey.count
)
}
guard outboundMACKey.count == Self.keySizes.macKeySize else {
throw VPhoneSSHTransportError.invalidKeySize(
expected: Self.keySizes.macKeySize,
actual: outboundMACKey.count
)
}
guard keys.initialInboundIV.count == Self.keySizes.ivSize else {
throw VPhoneSSHTransportError.invalidIVSize(
expected: Self.keySizes.ivSize,
actual: keys.initialInboundIV.count
)
}
guard keys.initialOutboundIV.count == Self.keySizes.ivSize else {
throw VPhoneSSHTransportError.invalidIVSize(
expected: Self.keySizes.ivSize,
actual: keys.initialOutboundIV.count
)
}
return (
inboundCipher: try AESCTRStreamCipher(key: inboundEncryptionKey, iv: keys.initialInboundIV),
outboundCipher: try AESCTRStreamCipher(key: outboundEncryptionKey, iv: keys.initialOutboundIV),
inboundMACKey: inboundMACKey,
outboundMACKey: outboundMACKey
)
}
private static func computeMAC(key: Data, sequenceNumber: UInt32, packet: [UInt8]) -> [UInt8] {
var sequenceNumber = sequenceNumber.bigEndian
var mac = [UInt8](repeating: 0, count: Self.macLength)
key.withUnsafeBytes { keyBuffer in
withUnsafeBytes(of: &sequenceNumber) { sequenceBuffer in
packet.withUnsafeBytes { packetBuffer in
CCHmacInitBuffer(Self.macAlgorithm, keyBuffer.baseAddress, keyBuffer.count) { context in
CCHmacUpdate(context, sequenceBuffer.baseAddress, sequenceBuffer.count)
CCHmacUpdate(context, packetBuffer.baseAddress, packetBuffer.count)
mac.withUnsafeMutableBytes { outputBuffer in
CCHmacFinal(context, outputBuffer.baseAddress)
}
}
}
}
}
return mac
}
private static func constantTimeEqual(_ lhs: [UInt8], _ rhs: [UInt8]) -> Bool {
guard lhs.count == rhs.count else { return false }
var diff: UInt8 = 0
for index in lhs.indices {
diff |= lhs[index] ^ rhs[index]
}
return diff == 0
}
private static func payloadBuffer(fromPlaintextPacket packetBuffer: inout ByteBuffer) throws -> ByteBuffer {
packetBuffer.moveReaderIndex(forwardBy: MemoryLayout<UInt32>.size)
guard let paddingLength = packetBuffer.readInteger(as: UInt8.self) else {
throw VPhoneSSHTransportError.invalidPacket
}
let payloadLength = packetBuffer.readableBytes - Int(paddingLength)
guard payloadLength >= 0,
let payload = packetBuffer.readSlice(length: payloadLength),
packetBuffer.readerIndex + Int(paddingLength) == packetBuffer.writerIndex
else {
throw VPhoneSSHTransportError.invalidPacket
}
packetBuffer.moveReaderIndex(forwardBy: Int(paddingLength))
return payload
}
}
final class AES128CTRHMACSHA256TransportProtection: AESCTRTransportProtectionBase {
override class var cipherName: String { "aes128-ctr" }
override class var macName: String? { "hmac-sha2-256" }
override class var keySizes: ExpectedKeySizes {
.init(ivSize: 16, encryptionKeySize: 16, macKeySize: Int(CC_SHA256_DIGEST_LENGTH))
}
override class var macAlgorithm: CCHmacAlgorithm { CCHmacAlgorithm(kCCHmacAlgSHA256) }
override class var macLength: Int { Int(CC_SHA256_DIGEST_LENGTH) }
}
final class AES256CTRHMACSHA256TransportProtection: AESCTRTransportProtectionBase {
override class var cipherName: String { "aes256-ctr" }
override class var macName: String? { "hmac-sha2-256" }
override class var keySizes: ExpectedKeySizes {
.init(ivSize: 16, encryptionKeySize: 32, macKeySize: Int(CC_SHA256_DIGEST_LENGTH))
}
override class var macAlgorithm: CCHmacAlgorithm { CCHmacAlgorithm(kCCHmacAlgSHA256) }
override class var macLength: Int { Int(CC_SHA256_DIGEST_LENGTH) }
}
final class AES128CTRHMACSHA1TransportProtection: AESCTRTransportProtectionBase {
override class var cipherName: String { "aes128-ctr" }
override class var macName: String? { "hmac-sha1" }
override class var keySizes: ExpectedKeySizes {
.init(ivSize: 16, encryptionKeySize: 16, macKeySize: Int(CC_SHA1_DIGEST_LENGTH))
}
override class var macAlgorithm: CCHmacAlgorithm { CCHmacAlgorithm(kCCHmacAlgSHA1) }
override class var macLength: Int { Int(CC_SHA1_DIGEST_LENGTH) }
}
final class AES256CTRHMACSHA1TransportProtection: AESCTRTransportProtectionBase {
override class var cipherName: String { "aes256-ctr" }
override class var macName: String? { "hmac-sha1" }
override class var keySizes: ExpectedKeySizes {
.init(ivSize: 16, encryptionKeySize: 32, macKeySize: Int(CC_SHA1_DIGEST_LENGTH))
}
override class var macAlgorithm: CCHmacAlgorithm { CCHmacAlgorithm(kCCHmacAlgSHA1) }
override class var macLength: Int { Int(CC_SHA1_DIGEST_LENGTH) }
}
final class AESCTRStreamCipher {
private var cryptor: CCCryptorRef?
init(key: Data, iv: [UInt8]) throws {
var cryptor: CCCryptorRef?
let status = key.withUnsafeBytes { keyBuffer in
iv.withUnsafeBytes { ivBuffer in
CCCryptorCreateWithMode(
CCOperation(kCCEncrypt),
CCMode(kCCModeCTR),
CCAlgorithm(kCCAlgorithmAES),
CCPadding(ccNoPadding),
ivBuffer.baseAddress,
keyBuffer.baseAddress,
key.count,
nil,
0,
0,
CCModeOptions(kCCModeOptionCTR_BE),
&cryptor
)
}
}
guard status == kCCSuccess, let cryptor else {
throw VPhoneSSHTransportError.cryptorCreateFailed(status)
}
self.cryptor = cryptor
}
deinit {
if let cryptor {
CCCryptorRelease(cryptor)
}
}
func update(_ input: [UInt8]) throws -> [UInt8] {
guard let cryptor else {
throw VPhoneSSHTransportError.invalidPacket
}
if input.isEmpty {
return []
}
let outputCount = input.count
var output = [UInt8](repeating: 0, count: outputCount)
var bytesMoved = 0
let status = input.withUnsafeBytes { inputBuffer in
output.withUnsafeMutableBytes { outputBuffer in
CCCryptorUpdate(
cryptor,
inputBuffer.baseAddress,
input.count,
outputBuffer.baseAddress,
outputCount,
&bytesMoved
)
}
}
guard status == kCCSuccess, bytesMoved == input.count else {
throw VPhoneSSHTransportError.cryptorUpdateFailed(status)
}
return output
}
}
private func CCHmacInitBuffer(
_ algorithm: CCHmacAlgorithm,
_ key: UnsafeRawPointer?,
_ keyLength: Int,
body: (UnsafeMutablePointer<CCHmacContext>) -> Void
) {
var context = CCHmacContext()
CCHmacInit(&context, algorithm, key, keyLength)
body(&context)
}

View File

@@ -56,16 +56,13 @@ struct SetupMachineCLI: AsyncParsableCommand {
}()
@Flag(help: "Skip setup_tools/build stage")
var skipProjectSetup: Bool = {
["1", "true", "yes"].contains(ProcessInfo.processInfo.environment["SKIP_PROJECT_SETUP"]?.lowercased() ?? "")
}()
var skipProjectSetup = false
@Flag(help: "Auto-continue first boot and final boot analysis")
var nonInteractive: Bool = {
["1", "true", "yes"].contains(ProcessInfo.processInfo.environment["NONE_INTERACTIVE"]?.lowercased() ?? "")
}()
var nonInteractive = false
mutating func run() async throws {
let environment = ProcessInfo.processInfo.environment
let runner = try SetupMachineRunner(
projectRoot: projectRoot.standardizedFileURL,
vmDirectoryName: vmDirectory,
@@ -79,11 +76,15 @@ struct SetupMachineCLI: AsyncParsableCommand {
cloudOSSource: cloudOSSource,
ipswDirectory: ipswDirectory?.standardizedFileURL,
variant: variant,
skipProjectSetup: skipProjectSetup,
nonInteractive: nonInteractive
skipProjectSetup: skipProjectSetup || Self.boolEnvironmentValue("SKIP_PROJECT_SETUP", environment: environment),
nonInteractive: nonInteractive || Self.boolEnvironmentValue("NONE_INTERACTIVE", environment: environment)
)
try await runner.run()
}
static func boolEnvironmentValue(_ key: String, environment: [String: String]) -> Bool {
["1", "true", "yes"].contains(environment[key]?.lowercased() ?? "")
}
}
private struct SetupMachineRunner {