An experimental share extension to send files and too much refactoring

closes #4
This commit is contained in:
Grishka
2023-09-26 03:22:23 +03:00
parent 22a2fff54b
commit 8f0eccd7b6
39 changed files with 2158 additions and 235 deletions

View File

@@ -0,0 +1,424 @@
//
// OutboundNearbyConnection.swift
// NearbyShare
//
// Created by Grishka on 23.09.2023.
//
import Foundation
import Network
import CryptoKit
import CommonCrypto
import System
import UniformTypeIdentifiers
import SwiftECC
import BigInt
class OutboundNearbyConnection:NearbyConnection{
private var currentState:State = .initial
private let urlsToSend:[URL]
private var ukeyClientFinishMsgData:Data?
private var queue:[OutgoingFileTransfer]=[]
private var currentTransfer:OutgoingFileTransfer?
public var delegate:OutboundNearbyConnectionDelegate?
private var totalBytesToSend:Int64=0
private var totalBytesSent:Int64=0
private var cancelled:Bool=false
enum State{
case initial, sentUkeyClientInit, sentUkeyClientFinish, sentPairedKeyEncryption, sentPairedKeyResult, sentIntroduction, sendingFiles
}
init(connection: NWConnection, id: String, urlsToSend:[URL]){
self.urlsToSend=urlsToSend
super.init(connection: connection, id: id)
}
deinit {
if let transfer=currentTransfer, let handle=transfer.handle{
try? handle.close()
}
for transfer in queue{
if let handle=transfer.handle{
try? handle.close()
}
}
}
public func cancel(){
cancelled=true
if encryptionDone{
var cancel=Sharing_Nearby_Frame()
cancel.version = .v1
cancel.v1=Sharing_Nearby_V1Frame()
cancel.v1.type = .cancel
try? sendTransferSetupFrame(cancel)
}
try? sendDisconnectionAndDisconnect()
}
override func connectionReady() {
super.connectionReady()
do{
try sendConnectionRequest()
try sendUkey2ClientInit()
}catch{
lastError=error
protocolError()
}
}
override func isServer() -> Bool {
return false
}
override func processReceivedFrame(frameData: Data) {
do{
#if DEBUG
print("received \(frameData), state is \(currentState)")
#endif
switch currentState {
case .initial:
protocolError()
case .sentUkeyClientInit:
try processUkey2ServerInit(frame: try Securegcm_Ukey2Message(serializedData: frameData), raw: frameData)
case .sentUkeyClientFinish:
try processConnectionResponse(frame: try Location_Nearby_Connections_OfflineFrame(serializedData: frameData))
default:
let smsg=try Securemessage_SecureMessage(serializedData: frameData)
try decryptAndProcessReceivedSecureMessage(smsg)
}
}catch{
if case NearbyError.ukey2=error{
}else if currentState == .sentUkeyClientInit{
sendUkey2Alert(type: .badMessage)
}
lastError=error
protocolError()
}
}
override func processTransferSetupFrame(_ frame: Sharing_Nearby_Frame) throws {
if frame.hasV1 && frame.v1.hasType, case .cancel = frame.v1.type {
print("Transfer canceled")
try sendDisconnectionAndDisconnect()
delegate?.outboundConnection(connection: self, failedWithError: NearbyError.canceled(reason: .userCanceled))
return
}
print(frame)
switch currentState{
case .sentPairedKeyEncryption:
try processPairedKeyEncryption(frame: frame)
case .sentPairedKeyResult:
try processPairedKeyResult(frame: frame)
case .sentIntroduction:
try processConsent(frame: frame)
case .sendingFiles:
break
default:
assertionFailure("Unexpected state \(currentState)")
}
}
override func protocolError() {
super.protocolError()
delegate?.outboundConnection(connection: self, failedWithError: lastError!)
}
private func sendConnectionRequest() throws {
var frame=Location_Nearby_Connections_OfflineFrame()
frame.version = .v1
frame.v1=Location_Nearby_Connections_V1Frame()
frame.v1.type = .connectionRequest
frame.v1.connectionRequest=Location_Nearby_Connections_ConnectionRequestFrame()
frame.v1.connectionRequest.endpointID=String(bytes: NearbyConnectionManager.shared.endpointID, encoding: .ascii)!
frame.v1.connectionRequest.endpointName=Host.current().localizedName!
let endpointInfo=EndpointInfo(name: Host.current().localizedName!, deviceType: .computer)
frame.v1.connectionRequest.endpointInfo=endpointInfo.serialize()
frame.v1.connectionRequest.mediums=[.wifiLan]
sendFrameAsync(try frame.serializedData())
}
private func sendUkey2ClientInit() throws {
let domain=Domain.instance(curve: .EC256r1)
let (pubKey, privKey)=domain.makeKeyPair()
publicKey=pubKey
privateKey=privKey
var finishFrame=Securegcm_Ukey2Message()
finishFrame.messageType = .clientFinish
var finish=Securegcm_Ukey2ClientFinished()
var pkey=Securemessage_GenericPublicKey()
pkey.type = .ecP256
pkey.ecP256PublicKey=Securemessage_EcP256PublicKey()
pkey.ecP256PublicKey.x=Data(pubKey.w.x.asSignedBytes())
pkey.ecP256PublicKey.y=Data(pubKey.w.y.asSignedBytes())
finish.publicKey=try pkey.serializedData()
finishFrame.messageData=try finish.serializedData()
ukeyClientFinishMsgData=try finishFrame.serializedData()
var frame=Securegcm_Ukey2Message()
frame.messageType = .clientInit
var clientInit=Securegcm_Ukey2ClientInit()
clientInit.version=1
clientInit.random=Data.randomData(length: 32)
clientInit.nextProtocol="AES_256_CBC-HMAC_SHA256"
var sha=SHA512()
sha.update(data: ukeyClientFinishMsgData!)
var commitment=Securegcm_Ukey2ClientInit.CipherCommitment()
commitment.commitment=Data(sha.finalize())
commitment.handshakeCipher = .p256Sha512
clientInit.cipherCommitments.append(commitment)
frame.messageData=try clientInit.serializedData()
ukeyClientInitMsgData=try frame.serializedData()
sendFrameAsync(ukeyClientInitMsgData!)
currentState = .sentUkeyClientInit
}
private func processUkey2ServerInit(frame:Securegcm_Ukey2Message, raw:Data) throws{
ukeyServerInitMsgData=raw
guard frame.messageType == .serverInit else{
sendUkey2Alert(type: .badMessageType)
throw NearbyError.ukey2
}
let serverInit=try Securegcm_Ukey2ServerInit(serializedData: frame.messageData)
guard serverInit.version==1 else{
sendUkey2Alert(type: .badVersion)
throw NearbyError.ukey2
}
guard serverInit.random.count==32 else{
sendUkey2Alert(type: .badRandom)
throw NearbyError.ukey2
}
guard serverInit.handshakeCipher == .p256Sha512 else{
sendUkey2Alert(type: .badHandshakeCipher)
throw NearbyError.ukey2
}
let serverKey=try Securemessage_GenericPublicKey(serializedData: serverInit.publicKey)
try finalizeKeyExchange(peerKey: serverKey)
sendFrameAsync(ukeyClientFinishMsgData!)
currentState = .sentUkeyClientFinish
var resp=Location_Nearby_Connections_OfflineFrame()
resp.version = .v1
resp.v1=Location_Nearby_Connections_V1Frame()
resp.v1.type = .connectionResponse
resp.v1.connectionResponse=Location_Nearby_Connections_ConnectionResponseFrame()
resp.v1.connectionResponse.response = .accept
resp.v1.connectionResponse.status=0
resp.v1.connectionResponse.osInfo=Location_Nearby_Connections_OsInfo()
resp.v1.connectionResponse.osInfo.type = .apple
sendFrameAsync(try resp.serializedData())
encryptionDone=true
delegate?.outboundConnectionWasEstablished(connection: self)
}
private func processConnectionResponse(frame:Location_Nearby_Connections_OfflineFrame) throws{
#if DEBUG
print("connection response: \(frame)")
#endif
guard frame.version == .v1 else {throw NearbyError.protocolError("Unexpected offline frame version \(frame.version)")}
guard frame.v1.type == .connectionResponse else {throw NearbyError.protocolError("Unexpected frame type \(frame.v1.type)")}
guard frame.v1.connectionResponse.response == .accept else {throw NearbyError.protocolError("Connection was rejected by recipient")}
var pairedEncryption=Sharing_Nearby_Frame()
pairedEncryption.version = .v1
pairedEncryption.v1=Sharing_Nearby_V1Frame()
pairedEncryption.v1.type = .pairedKeyEncryption
pairedEncryption.v1.pairedKeyEncryption=Sharing_Nearby_PairedKeyEncryptionFrame()
pairedEncryption.v1.pairedKeyEncryption.secretIDHash=Data.randomData(length: 6)
pairedEncryption.v1.pairedKeyEncryption.signedData=Data.randomData(length: 72)
try sendTransferSetupFrame(pairedEncryption)
currentState = .sentPairedKeyEncryption
}
private func processPairedKeyEncryption(frame:Sharing_Nearby_Frame) throws{
guard frame.hasV1, frame.v1.hasPairedKeyEncryption else { throw NearbyError.requiredFieldMissing }
var pairedResult=Sharing_Nearby_Frame()
pairedResult.version = .v1
pairedResult.v1=Sharing_Nearby_V1Frame()
pairedResult.v1.type = .pairedKeyResult
pairedResult.v1.pairedKeyResult=Sharing_Nearby_PairedKeyResultFrame()
pairedResult.v1.pairedKeyResult.status = .unable
try sendTransferSetupFrame(pairedResult)
currentState = .sentPairedKeyResult
}
private func processPairedKeyResult(frame:Sharing_Nearby_Frame) throws{
guard frame.hasV1, frame.v1.hasPairedKeyResult else { throw NearbyError.requiredFieldMissing }
var introduction=Sharing_Nearby_Frame()
introduction.version = .v1
introduction.v1.type = .introduction
if urlsToSend.count==1 && urlsToSend[0].scheme != "file"{
var meta=Sharing_Nearby_TextMetadata()
meta.type = .url
meta.textTitle=urlsToSend[0].host ?? "URL"
meta.size=Int64(urlsToSend[0].absoluteString.utf8.count)
meta.payloadID=Int64.random(in: Int64.min...Int64.max)
introduction.v1.introduction.textMetadata.append(meta)
}else{
for url in urlsToSend{
guard url.scheme=="file" else {continue}
var meta=Sharing_Nearby_FileMetadata()
meta.name=OutboundNearbyConnection.sanitizeFileName(name: url.lastPathComponent)
let attrs=try FileManager.default.attributesOfItem(atPath: url.path)
meta.size=(attrs[FileAttributeKey.size] as! NSNumber).int64Value
let typeID=try? url.resourceValues(forKeys: [.typeIdentifierKey]).typeIdentifier
meta.mimeType="application/octet-stream"
if let typeID=typeID{
let type=UTType(typeID)
if let type=type, let mimeType=type.preferredMIMEType{
meta.mimeType=mimeType
}
}
if meta.mimeType.starts(with: "image/"){
meta.type = .image
}else if meta.mimeType.starts(with: "video/"){
meta.type = .video
}else if(meta.mimeType.starts(with: "audio/")){
meta.type = .audio
}else if(url.pathExtension.lowercased()=="apk"){
meta.type = .app
}else{
meta.type = .unknown
}
meta.payloadID=Int64.random(in: Int64.min...Int64.max)
queue.append(OutgoingFileTransfer(url: url, payloadID: meta.payloadID, handle: try FileHandle(forReadingFrom: url), totalBytes: meta.size, currentOffset: 0))
introduction.v1.introduction.fileMetadata.append(meta)
totalBytesToSend+=meta.size
}
}
#if DEBUG
print("sent introduction: \(introduction)")
#endif
try sendTransferSetupFrame(introduction)
currentState = .sentIntroduction
}
private func processConsent(frame:Sharing_Nearby_Frame) throws{
guard frame.version == .v1, frame.v1.type == .response else {throw NearbyError.requiredFieldMissing}
switch frame.v1.connectionResponse.status{
case .accept:
currentState = .sendingFiles
delegate?.outboundConnectionTransferAccepted(connection: self)
try sendNextFileChunk()
case .reject, .unknown:
delegate?.outboundConnection(connection: self, failedWithError: NearbyError.canceled(reason: .userRejected))
try sendDisconnectionAndDisconnect()
case .notEnoughSpace:
delegate?.outboundConnection(connection: self, failedWithError: NearbyError.canceled(reason: .notEnoughSpace))
try sendDisconnectionAndDisconnect()
case .timedOut:
delegate?.outboundConnection(connection: self, failedWithError: NearbyError.canceled(reason: .timedOut))
try sendDisconnectionAndDisconnect()
case .unsupportedAttachmentType:
delegate?.outboundConnection(connection: self, failedWithError: NearbyError.canceled(reason: .unsupportedType))
try sendDisconnectionAndDisconnect()
}
}
private func sendNextFileChunk() throws{
print("SEND NEXT: \(Thread.current)")
if cancelled{
return
}
if currentTransfer==nil || currentTransfer?.currentOffset==currentTransfer?.totalBytes{
if currentTransfer != nil && currentTransfer?.handle != nil{
try currentTransfer?.handle?.close()
}
if queue.isEmpty{
#if DEBUG
print("Disconnecting because all files have been transferred")
#endif
try sendDisconnectionAndDisconnect()
delegate?.outboundConnectionTransferFinished(connection: self)
return
}
currentTransfer=queue.removeFirst()
}
guard let fileBuffer=try currentTransfer!.handle!.read(upToCount: 512*1024) else{
throw NearbyError.inputOutput(cause: Errno.ioError)
}
var transfer=Location_Nearby_Connections_PayloadTransferFrame()
transfer.packetType = .data
transfer.payloadChunk.offset=currentTransfer!.currentOffset
transfer.payloadChunk.flags=0
transfer.payloadChunk.body=fileBuffer
transfer.payloadHeader.id=currentTransfer!.payloadID
transfer.payloadHeader.type = .file
transfer.payloadHeader.totalSize=Int64(currentTransfer!.totalBytes)
transfer.payloadHeader.isSensitive=false
currentTransfer!.currentOffset+=Int64(fileBuffer.count)
var wrapper=Location_Nearby_Connections_OfflineFrame()
wrapper.version = .v1
wrapper.v1=Location_Nearby_Connections_V1Frame()
wrapper.v1.type = .payloadTransfer
wrapper.v1.payloadTransfer=transfer
try encryptAndSendOfflineFrame(wrapper, completion: {
do{
try self.sendNextFileChunk()
}catch{
self.lastError=error
self.protocolError()
}
})
#if DEBUG
print("sent file chunk, current transfer: \(String(describing: currentTransfer))")
#endif
totalBytesSent+=Int64(fileBuffer.count)
delegate?.outboundConnection(connection: self, transferProgress: Double(totalBytesSent)/Double(totalBytesToSend))
if currentTransfer!.currentOffset==currentTransfer!.totalBytes{
// Signal end of file (yes, all this for one bit)
var transfer=Location_Nearby_Connections_PayloadTransferFrame()
transfer.packetType = .data
transfer.payloadChunk.offset=currentTransfer!.currentOffset
transfer.payloadChunk.flags=1 // <- this one here
transfer.payloadHeader.id=currentTransfer!.payloadID
transfer.payloadHeader.type = .file
transfer.payloadHeader.totalSize=Int64(currentTransfer!.totalBytes)
transfer.payloadHeader.isSensitive=false
var wrapper=Location_Nearby_Connections_OfflineFrame()
wrapper.version = .v1
wrapper.v1=Location_Nearby_Connections_V1Frame()
wrapper.v1.type = .payloadTransfer
wrapper.v1.payloadTransfer=transfer
try encryptAndSendOfflineFrame(wrapper)
#if DEBUG
print("sent EOF, current transfer: \(String(describing: currentTransfer))")
#endif
}
}
private static func sanitizeFileName(name:String)->String{
return name.replacingOccurrences(of: "[\\/\\\\?%\\*:\\|\"<>=]", with: "_", options: .regularExpression)
}
}
fileprivate struct OutgoingFileTransfer{
let url:URL
let payloadID:Int64
let handle:FileHandle?
let totalBytes:Int64
var currentOffset:Int64
}
protocol OutboundNearbyConnectionDelegate{
func outboundConnectionWasEstablished(connection:OutboundNearbyConnection)
func outboundConnection(connection:OutboundNearbyConnection, transferProgress:Double)
func outboundConnectionTransferAccepted(connection:OutboundNearbyConnection)
func outboundConnection(connection:OutboundNearbyConnection, failedWithError:Error)
func outboundConnectionTransferFinished(connection:OutboundNearbyConnection)
}