87 lines
3.7 KiB
Swift
87 lines
3.7 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
import Foundation
|
|
import TermindApplication
|
|
|
|
/// Incremental, bounded SSH agent framing. Unknown management messages fail closed.
|
|
nonisolated struct SSHAgentProtocol {
|
|
static let maximumPacket = 256 * 1024
|
|
private(set) var bytes = Data()
|
|
|
|
mutating func append(_ data: Data) throws {
|
|
guard bytes.count + data.count <= Self.maximumPacket + 4 else { throw Failure.invalid }
|
|
bytes.append(data)
|
|
if bytes.count >= 4 {
|
|
var reader = Reader(bytes)
|
|
let length = try reader.uint32()
|
|
guard length > 0, length <= Self.maximumPacket else { throw Failure.invalid }
|
|
}
|
|
}
|
|
|
|
mutating func next() throws -> Data? {
|
|
guard bytes.count >= 4 else { return nil }
|
|
var reader = Reader(bytes)
|
|
let length = Int(try reader.uint32())
|
|
guard length > 0, length <= Self.maximumPacket else { throw Failure.invalid }
|
|
guard bytes.count >= length + 4 else { return nil }
|
|
let packet = Data(bytes.dropFirst(4).prefix(length))
|
|
bytes = Data(bytes.dropFirst(length + 4))
|
|
return packet
|
|
}
|
|
|
|
static func response(to packet: Data, provider: any SSHAgentForwarding) async -> Data {
|
|
do {
|
|
try Task.checkCancellation()
|
|
guard let kind = packet.first else { throw Failure.invalid }
|
|
var reader = Reader(Data(packet.dropFirst()))
|
|
switch kind {
|
|
case 11: // SSH2_AGENTC_REQUEST_IDENTITIES
|
|
guard reader.isEmpty else { throw Failure.invalid }
|
|
let identities = try await provider.identities()
|
|
try Task.checkCancellation()
|
|
guard identities.count <= 1024 else { throw Failure.invalid }
|
|
var reply = Data([12]) + uint32(UInt32(identities.count))
|
|
for identity in identities {
|
|
reply += field(identity.blob) + field(Data(identity.comment.utf8))
|
|
guard reply.count <= maximumPacket else { throw Failure.invalid }
|
|
}
|
|
return frame(reply)
|
|
case 13: // SSH2_AGENTC_SIGN_REQUEST
|
|
let key = try reader.field()
|
|
let data = try reader.field()
|
|
let flags = try reader.uint32()
|
|
guard reader.isEmpty else { throw Failure.invalid }
|
|
let signature = try await provider.sign(key: key, data: data, flags: flags)
|
|
try Task.checkCancellation()
|
|
guard signature.count < maximumPacket - 5 else { throw Failure.invalid }
|
|
return frame(Data([14]) + field(signature))
|
|
default: return frame(Data([5]))
|
|
}
|
|
} catch { return frame(Data([5])) }
|
|
}
|
|
|
|
enum Failure: Error { case invalid, unavailable }
|
|
static func uint32(_ value: UInt32) -> Data { SSHWireEncoding.uint32(value) }
|
|
static func field(_ data: Data) -> Data { SSHWireEncoding.field(data) }
|
|
static func frame(_ data: Data) -> Data { field(data) }
|
|
|
|
struct Reader {
|
|
let data: Data
|
|
var offset = 0
|
|
init(_ data: Data) { self.data = Data(data) }
|
|
var isEmpty: Bool { offset == data.count }
|
|
mutating func uint32() throws -> UInt32 {
|
|
guard data.count - offset >= 4 else { throw Failure.invalid }
|
|
defer { offset += 4 }
|
|
return data[offset..<offset + 4].reduce(UInt32(0)) { ($0 << 8) | UInt32($1) }
|
|
}
|
|
mutating func field() throws -> Data {
|
|
let count = Int(try uint32())
|
|
guard count <= data.count - offset else { throw Failure.invalid }
|
|
defer { offset += count }
|
|
return data.subdata(in: offset..<offset + count)
|
|
}
|
|
}
|
|
}
|