From 6c6c79cdad2072feb8984fac71f72ea6a4584fbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20B=C3=B6ttger?= Date: Wed, 14 May 2025 15:06:32 +0200 Subject: [PATCH] Reformatting --- NearbyShare/Data+Extensions.swift | 76 +- NearbyShare/InboundNearbyConnection.swift | 483 ++++++------ NearbyShare/NearbyConnection.swift | 560 +++++++------- NearbyShare/NearbyConnectionManager.swift | 633 ++++++++-------- NearbyShare/OutboundNearbyConnection.swift | 834 ++++++++++----------- 5 files changed, 1279 insertions(+), 1307 deletions(-) diff --git a/NearbyShare/Data+Extensions.swift b/NearbyShare/Data+Extensions.swift index 5d3013e..7098bc4 100644 --- a/NearbyShare/Data+Extensions.swift +++ b/NearbyShare/Data+Extensions.swift @@ -1,47 +1,47 @@ // -// Data+URLSafeBase64.swift +// Data+Extensions.swift // QuickDrop // // Created by Grishka on 08.04.2023. // -import Foundation import CoreFoundation +import Foundation -extension Data{ - func urlSafeBase64EncodedString() -> String { - return String(base64EncodedString().replacingOccurrences(of: "=", with: "").map { - if $0=="/"{ - return "_" - } else if $0=="+" { - return "-" - } else { - return $0 - } - }) - } - - static func randomData(length: Int) -> Data{ - var data=Data(count: length) - data.withUnsafeMutableBytes { - guard 0 == SecRandomCopyBytes(kSecRandomDefault, length, $0.baseAddress!) else { fatalError() } - } - return data - } - - static func dataFromUrlSafeBase64(_ str:String)->Data?{ - var regularB64=String(str.map{ - if $0=="_"{ - return "/" - }else if $0=="-"{ - return "+" - }else{ - return $0 - } - }) - while (regularB64.count%4) != 0{ - regularB64=regularB64+"=" - } - return Data(base64Encoded: regularB64, options: .ignoreUnknownCharacters) - } +extension Data { + func urlSafeBase64EncodedString() -> String { + return String(base64EncodedString().replacingOccurrences(of: "=", with: "").map { + if $0 == "/" { + return "_" + } else if $0 == "+" { + return "-" + } else { + return $0 + } + }) + } + + static func randomData(length: Int) -> Data { + var data = Data(count: length) + data.withUnsafeMutableBytes { + guard SecRandomCopyBytes(kSecRandomDefault, length, $0.baseAddress!) == 0 else { fatalError() } + } + return data + } + + static func dataFromUrlSafeBase64(_ str: String) -> Data? { + var regularB64 = String(str.map { + if $0 == "_" { + return "/" + } else if $0 == "-" { + return "+" + } else { + return $0 + } + }) + while (regularB64.count % 4) != 0 { + regularB64 = regularB64 + "=" + } + return Data(base64Encoded: regularB64, options: .ignoreUnknownCharacters) + } } diff --git a/NearbyShare/InboundNearbyConnection.swift b/NearbyShare/InboundNearbyConnection.swift index dfe7a3f..c9942ae 100644 --- a/NearbyShare/InboundNearbyConnection.swift +++ b/NearbyShare/InboundNearbyConnection.swift @@ -5,94 +5,89 @@ // Created by Grishka on 08.04.2023. // +import AppKit +import CommonCrypto +import CryptoKit import Foundation import Network -import CryptoKit -import CommonCrypto import System -import AppKit -import SwiftECC import BigInt +import SwiftECC -class InboundNearbyConnection: NearbyConnection{ - - private var currentState:State = .initial - public var delegate:InboundNearbyConnectionDelegate? - private var cipherCommitment:Data? - - private var textPayloadID:Int64=0 - - enum State{ +class InboundNearbyConnection: NearbyConnection { + private var currentState: State = .initial + public var delegate: InboundNearbyConnectionDelegate? + private var cipherCommitment: Data? + + private var textPayloadID: Int64 = 0 + + enum State { case initial, receivedConnectionRequest, sentUkeyServerInit, receivedUkeyClientFinish, sentConnectionResponse, sentPairedKeyResult, receivedPairedKeyResult, waitingForUserConsent, receivingFiles, disconnected } - - override init(connection: NWConnection, id:String) { + + override init(connection: NWConnection, id: String) { super.init(connection: connection, id: id) } - + override func handleConnectionClosure() { - super.handleConnectionClosure() currentState = .disconnected do { try deletePartiallyReceivedFiles() - } catch{ + } catch { log("Error deleting partially received files: \(error)") } DispatchQueue.main.async { self.delegate?.connectionWasTerminated(connection: self, error: self.lastError) - + SaveFilesManager.shared.movePendingFilesToTarget() SaveFilesManager.shared.stopAccessingSecurityScopedResource() } } - - override internal func processReceivedFrame(frameData:Data){ - + + override func processReceivedFrame(frameData: Data) { if currentState != .receivingFiles { log("Received frame in state \(currentState)...") } - + do { switch currentState { case .initial: - let frame=try Location_Nearby_Connections_OfflineFrame(serializedBytes: frameData) + let frame = try Location_Nearby_Connections_OfflineFrame(serializedBytes: frameData) try processConnectionRequestFrame(frame) case .receivedConnectionRequest: - let msg=try Securegcm_Ukey2Message(serializedBytes: frameData) - ukeyClientInitMsgData=frameData + let msg = try Securegcm_Ukey2Message(serializedBytes: frameData) + ukeyClientInitMsgData = frameData try processUkey2ClientInit(msg) case .sentUkeyServerInit: - let msg=try Securegcm_Ukey2Message(serializedBytes: frameData) + let msg = try Securegcm_Ukey2Message(serializedBytes: frameData) try processUkey2ClientFinish(msg, raw: frameData) case .receivedUkeyClientFinish: - let frame=try Location_Nearby_Connections_OfflineFrame(serializedBytes: frameData) + let frame = try Location_Nearby_Connections_OfflineFrame(serializedBytes: frameData) try processConnectionResponseFrame(frame) default: - var smsg: Securemessage_SecureMessage - + do { smsg = try Securemessage_SecureMessage(serializedBytes: frameData) - } - catch { + } catch { log("Error deserializing secure message. Trying again....") - + // last 32 bytes = HMAC key // change 2 bytes before to 0x12 0x20 for Protobuf to succeed - + if frameData.count < 34 { throw NearbyError.protocolError("Frame too short") } - + var newData = frameData let count = newData.count newData[count - 34] = 0x12 newData[count - 33] = 0x20 - + smsg = try Securemessage_SecureMessage(serializedBytes: newData) - + log("Secure message deserialized successfully after fixing Protobuf message") } try decryptAndProcessReceivedSecureMessage(smsg) @@ -100,27 +95,27 @@ class InboundNearbyConnection: NearbyConnection{ } catch { lastError = error log("Deserialization error: \(error) in state \(currentState). Payload: \(frameData.hex)") - - //log("Public Key: \(publicKey?.pem ?? "nil")") - //log("Private Key: \(privateKey?.pem ?? "nil")") - //log("Ukey Client Init Msg: \(ukeyClientInitMsgData?.hex ?? "nil")") - //log("Ukey Server Init Msg: \(ukeyServerInitMsgData?.hex ?? "nil")") + + // log("Public Key: \(publicKey?.pem ?? "nil")") + // log("Private Key: \(privateKey?.pem ?? "nil")") + // log("Ukey Client Init Msg: \(ukeyClientInitMsgData?.hex ?? "nil")") + // log("Ukey Server Init Msg: \(ukeyServerInitMsgData?.hex ?? "nil")") log("S1: \(decryptKey?.map { String(format: "%02x", $0) }.joined() ?? "nil")") - //log("Encrypt Key: \(encryptKey?.map { String(format: "%02x", $0) }.joined() ?? "nil")") + // log("Encrypt Key: \(encryptKey?.map { String(format: "%02x", $0) }.joined() ?? "nil")") log("Received HMAC/Signature Key: \(recvHmacKey?.withUnsafeBytes { Data(Array($0)) }.map { String(format: "%02x", $0) }.joined() ?? "nil")") log("Sent HMAC/Signature Key: \(sendHmacKey?.withUnsafeBytes { Data(Array($0)) }.map { String(format: "%02x", $0) }.joined() ?? "nil")") - - self.protocolError() + + protocolError() } } - - override internal func processTransferSetupFrame(_ frame:Sharing_Nearby_Frame) throws{ + + override func processTransferSetupFrame(_ frame: Sharing_Nearby_Frame) throws { if frame.hasV1 && frame.v1.hasType, case .cancel = frame.v1.type { log("Transfer canceled") try sendDisconnectionAndDisconnect() return } - switch currentState{ + switch currentState { case .sentConnectionResponse: try processPairedKeyEncryptionFrame(frame) case .sentPairedKeyResult: @@ -132,47 +127,47 @@ class InboundNearbyConnection: NearbyConnection{ log(frame.debugDescription) } } - + override func isServer() -> Bool { return true } - - override func processFileChunk(frame: Location_Nearby_Connections_PayloadTransferFrame) throws{ - let id=frame.payloadHeader.id - guard let fileInfo=transferredFiles[id] else { throw NearbyError.protocolError("File payload ID \(id) is not known") } - let currentOffset=fileInfo.bytesTransferred - guard frame.payloadChunk.offset==currentOffset else { throw NearbyError.protocolError("Invalid offset into file \(frame.payloadChunk.offset), expected \(currentOffset)") } - guard currentOffset+Int64(frame.payloadChunk.body.count)<=fileInfo.meta.size else { throw NearbyError.protocolError("Transferred file size exceeds previously specified value") } - if frame.payloadChunk.body.count>0{ + + override func processFileChunk(frame: Location_Nearby_Connections_PayloadTransferFrame) throws { + let id = frame.payloadHeader.id + guard let fileInfo = transferredFiles[id] else { throw NearbyError.protocolError("File payload ID \(id) is not known") } + let currentOffset = fileInfo.bytesTransferred + guard frame.payloadChunk.offset == currentOffset else { throw NearbyError.protocolError("Invalid offset into file \(frame.payloadChunk.offset), expected \(currentOffset)") } + guard currentOffset + Int64(frame.payloadChunk.body.count) <= fileInfo.meta.size else { throw NearbyError.protocolError("Transferred file size exceeds previously specified value") } + if frame.payloadChunk.body.count > 0 { fileInfo.fileHandle?.write(frame.payloadChunk.body) - transferredFiles[id]!.bytesTransferred+=Int64(frame.payloadChunk.body.count) - fileInfo.progress?.completedUnitCount=transferredFiles[id]!.bytesTransferred - }else if (frame.payloadChunk.flags & 1)==1{ + transferredFiles[id]!.bytesTransferred += Int64(frame.payloadChunk.body.count) + fileInfo.progress?.completedUnitCount = transferredFiles[id]!.bytesTransferred + } else if (frame.payloadChunk.flags & 1) == 1 { try fileInfo.fileHandle?.close() - transferredFiles[id]!.fileHandle=nil + transferredFiles[id]!.fileHandle = nil fileInfo.progress?.unpublish() SaveFilesManager.shared.registerFileFinishedDownloading(fileInfo.destinationURL) - + transferredFiles.removeValue(forKey: id) - if transferredFiles.isEmpty{ + if transferredFiles.isEmpty { try sendDisconnectionAndDisconnect() } } } - + override func processBytesPayload(payload: Data, id: Int64) throws -> Bool { - if id==textPayloadID{ - if let urlStr=String(data: payload, encoding: .utf8), let url=URL(string: urlStr){ + if id == textPayloadID { + if let urlStr = String(data: payload, encoding: .utf8), let url = URL(string: urlStr) { NSWorkspace.shared.open(url) } try sendDisconnectionAndDisconnect() return true - }else if let fileInfo=transferredFiles[id]{ + } else if let fileInfo = transferredFiles[id] { fileInfo.fileHandle?.write(payload) - transferredFiles[id]!.bytesTransferred+=Int64(payload.count) - fileInfo.progress?.completedUnitCount=transferredFiles[id]!.bytesTransferred + transferredFiles[id]!.bytesTransferred += Int64(payload.count) + fileInfo.progress?.completedUnitCount = transferredFiles[id]!.bytesTransferred try fileInfo.fileHandle?.close() - transferredFiles[id]!.fileHandle=nil + transferredFiles[id]!.fileHandle = nil fileInfo.progress?.unpublish() transferredFiles.removeValue(forKey: id) SaveFilesManager.shared.registerFileFinishedDownloading(fileInfo.destinationURL) @@ -181,247 +176,243 @@ class InboundNearbyConnection: NearbyConnection{ } return false } - - private func processConnectionRequestFrame(_ frame:Location_Nearby_Connections_OfflineFrame) throws{ + + private func processConnectionRequestFrame(_ frame: Location_Nearby_Connections_OfflineFrame) throws { guard frame.hasV1 && frame.v1.hasConnectionRequest && frame.v1.connectionRequest.hasEndpointInfo else { throw NearbyError.requiredFieldMissing("connectionRequest.endpointInfo") } guard case .connectionRequest = frame.v1.type else { throw NearbyError.protocolError("Unexpected frame type \(frame.v1.type)") } - let endpointInfo=frame.v1.connectionRequest.endpointInfo - guard endpointInfo.count>17 else { throw NearbyError.protocolError("Endpoint info too short") } - let deviceNameLength=Int(endpointInfo[17]) - guard endpointInfo.count>=deviceNameLength+18 else { throw NearbyError.protocolError("Endpoint info too short to contain the device name") } - guard let deviceName=String(data: endpointInfo[18..<(18+deviceNameLength)], encoding: .utf8) else { throw NearbyError.protocolError("Device name is not valid UTF-8") } - let rawDeviceType:Int=Int(endpointInfo[0] & 7) >> 1 - remoteDeviceInfo=RemoteDeviceInfo(name: deviceName, type: RemoteDeviceInfo.DeviceType.fromRawValue(value: rawDeviceType)) + let endpointInfo = frame.v1.connectionRequest.endpointInfo + guard endpointInfo.count > 17 else { throw NearbyError.protocolError("Endpoint info too short") } + let deviceNameLength = Int(endpointInfo[17]) + guard endpointInfo.count >= deviceNameLength + 18 else { throw NearbyError.protocolError("Endpoint info too short to contain the device name") } + guard let deviceName = String(data: endpointInfo[18 ..< (18 + deviceNameLength)], encoding: .utf8) else { throw NearbyError.protocolError("Device name is not valid UTF-8") } + let rawDeviceType = Int(endpointInfo[0] & 7) >> 1 + remoteDeviceInfo = RemoteDeviceInfo(name: deviceName, type: RemoteDeviceInfo.DeviceType.fromRawValue(value: rawDeviceType)) currentState = .receivedConnectionRequest } - - private func processUkey2ClientInit(_ msg:Securegcm_Ukey2Message) throws{ + + private func processUkey2ClientInit(_ msg: Securegcm_Ukey2Message) throws { guard msg.hasMessageType, msg.hasMessageData else { throw NearbyError.requiredFieldMissing("clientInit ukey2message.type|data") } - guard case .clientInit = msg.messageType else{ + guard case .clientInit = msg.messageType else { sendUkey2Alert(type: .badMessageType) throw NearbyError.ukey2 } - let clientInit:Securegcm_Ukey2ClientInit - do{ - clientInit=try Securegcm_Ukey2ClientInit(serializedBytes: msg.messageData) - }catch{ + let clientInit: Securegcm_Ukey2ClientInit + do { + clientInit = try Securegcm_Ukey2ClientInit(serializedBytes: msg.messageData) + } catch { sendUkey2Alert(type: .badMessageData) throw NearbyError.ukey2 } - guard clientInit.version==1 else{ + guard clientInit.version == 1 else { sendUkey2Alert(type: .badVersion) throw NearbyError.ukey2 } - guard clientInit.random.count==32 else{ + guard clientInit.random.count == 32 else { sendUkey2Alert(type: .badRandom) throw NearbyError.ukey2 } - var found=false - for commitment in clientInit.cipherCommitments{ - if case .p256Sha512 = commitment.handshakeCipher{ - found=true - cipherCommitment=commitment.commitment + var found = false + for commitment in clientInit.cipherCommitments { + if case .p256Sha512 = commitment.handshakeCipher { + found = true + cipherCommitment = commitment.commitment break } } - guard found else{ + guard found else { sendUkey2Alert(type: .badHandshakeCipher) throw NearbyError.ukey2 } - guard clientInit.nextProtocol=="AES_256_CBC-HMAC_SHA256" else{ + guard clientInit.nextProtocol == "AES_256_CBC-HMAC_SHA256" else { sendUkey2Alert(type: .badNextProtocol) throw NearbyError.ukey2 } - - let domain=Domain.instance(curve: .EC256r1) - let (pubKey, privKey)=domain.makeKeyPair() - publicKey=pubKey - privateKey=privKey - - var serverInit=Securegcm_Ukey2ServerInit() - serverInit.version=1 - serverInit.random=Data.randomData(length: 32) + + let domain = Domain.instance(curve: .EC256r1) + let (pubKey, privKey) = domain.makeKeyPair() + publicKey = pubKey + privateKey = privKey + + var serverInit = Securegcm_Ukey2ServerInit() + serverInit.version = 1 + serverInit.random = Data.randomData(length: 32) serverInit.handshakeCipher = .p256Sha512 - - var pkey=Securemessage_GenericPublicKey() + + 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()) - serverInit.publicKey=try pkey.serializedData() - - var serverInitMsg=Securegcm_Ukey2Message() + pkey.ecP256PublicKey = Securemessage_EcP256PublicKey() + pkey.ecP256PublicKey.x = Data(pubKey.w.x.asSignedBytes()) + pkey.ecP256PublicKey.y = Data(pubKey.w.y.asSignedBytes()) + serverInit.publicKey = try pkey.serializedData() + + var serverInitMsg = Securegcm_Ukey2Message() serverInitMsg.messageType = .serverInit - serverInitMsg.messageData=try serverInit.serializedData() - let serverInitData=try serverInitMsg.serializedData() - ukeyServerInitMsgData=serverInitData + serverInitMsg.messageData = try serverInit.serializedData() + let serverInitData = try serverInitMsg.serializedData() + ukeyServerInitMsgData = serverInitData sendFrameAsync(serverInitData) currentState = .sentUkeyServerInit } - - private func processUkey2ClientFinish(_ msg:Securegcm_Ukey2Message, raw:Data) throws{ + + private func processUkey2ClientFinish(_ msg: Securegcm_Ukey2Message, raw: Data) throws { guard msg.hasMessageType, msg.hasMessageData else { throw NearbyError.requiredFieldMissing("clientFinish ukey2message.type|data") } guard case .clientFinish = msg.messageType else { throw NearbyError.ukey2 } - - var sha=SHA512() + + var sha = SHA512() sha.update(data: raw) - guard cipherCommitment==Data(sha.finalize()) else { throw NearbyError.ukey2 } - - let clientFinish=try Securegcm_Ukey2ClientFinished(serializedBytes: msg.messageData) - guard clientFinish.hasPublicKey else {throw NearbyError.requiredFieldMissing("ukey2clientFinish.publicKey") } - let clientKey=try Securemessage_GenericPublicKey(serializedBytes: clientFinish.publicKey) - + guard cipherCommitment == Data(sha.finalize()) else { throw NearbyError.ukey2 } + + let clientFinish = try Securegcm_Ukey2ClientFinished(serializedBytes: msg.messageData) + guard clientFinish.hasPublicKey else { throw NearbyError.requiredFieldMissing("ukey2clientFinish.publicKey") } + let clientKey = try Securemessage_GenericPublicKey(serializedBytes: clientFinish.publicKey) + try finalizeKeyExchange(peerKey: clientKey) - + currentState = .receivedUkeyClientFinish } - - private func processConnectionResponseFrame(_ frame:Location_Nearby_Connections_OfflineFrame) throws{ + + private func processConnectionResponseFrame(_ frame: Location_Nearby_Connections_OfflineFrame) throws { guard frame.hasV1, frame.v1.hasType else { throw NearbyError.requiredFieldMissing("offlineFrame.v1.type") } if case .connectionResponse = frame.v1.type { - var resp=Location_Nearby_Connections_OfflineFrame() + var resp = Location_Nearby_Connections_OfflineFrame() resp.version = .v1 - resp.v1=Location_Nearby_Connections_V1Frame() + resp.v1 = Location_Nearby_Connections_V1Frame() resp.v1.type = .connectionResponse - resp.v1.connectionResponse=Location_Nearby_Connections_ConnectionResponseFrame() + 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.status = 0 + resp.v1.connectionResponse.osInfo = Location_Nearby_Connections_OsInfo() resp.v1.connectionResponse.osInfo.type = .apple - sendFrameAsync(try resp.serializedData()) - - encryptionDone=true - - var pairedEncryption=Sharing_Nearby_Frame() + try sendFrameAsync(resp.serializedData()) + + encryptionDone = true + + var pairedEncryption = Sharing_Nearby_Frame() pairedEncryption.version = .v1 - pairedEncryption.v1=Sharing_Nearby_V1Frame() + pairedEncryption.v1 = Sharing_Nearby_V1Frame() pairedEncryption.v1.type = .pairedKeyEncryption - pairedEncryption.v1.pairedKeyEncryption=Sharing_Nearby_PairedKeyEncryptionFrame() + pairedEncryption.v1.pairedKeyEncryption = Sharing_Nearby_PairedKeyEncryptionFrame() // Presumably used for all the phone number stuff that no one needs anyway - pairedEncryption.v1.pairedKeyEncryption.secretIDHash=Data.randomData(length: 6) - pairedEncryption.v1.pairedKeyEncryption.signedData=Data.randomData(length: 72) + pairedEncryption.v1.pairedKeyEncryption.secretIDHash = Data.randomData(length: 6) + pairedEncryption.v1.pairedKeyEncryption.signedData = Data.randomData(length: 72) try sendTransferSetupFrame(pairedEncryption) currentState = .sentConnectionResponse } else { log("Unhandled offline frame plaintext: \(frame)") } } - - private func processPairedKeyEncryptionFrame(_ frame:Sharing_Nearby_Frame) throws{ + + private func processPairedKeyEncryptionFrame(_ frame: Sharing_Nearby_Frame) throws { guard frame.hasV1, frame.v1.hasPairedKeyEncryption else { throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.pairedKeyEncryption") } - var pairedResult=Sharing_Nearby_Frame() + var pairedResult = Sharing_Nearby_Frame() pairedResult.version = .v1 - pairedResult.v1=Sharing_Nearby_V1Frame() + pairedResult.v1 = Sharing_Nearby_V1Frame() pairedResult.v1.type = .pairedKeyResult - pairedResult.v1.pairedKeyResult=Sharing_Nearby_PairedKeyResultFrame() + pairedResult.v1.pairedKeyResult = Sharing_Nearby_PairedKeyResultFrame() pairedResult.v1.pairedKeyResult.status = .unable try sendTransferSetupFrame(pairedResult) currentState = .sentPairedKeyResult } - - private func processPairedKeyResultFrame(_ frame:Sharing_Nearby_Frame) throws{ + + private func processPairedKeyResultFrame(_ frame: Sharing_Nearby_Frame) throws { guard frame.hasV1, frame.v1.hasPairedKeyResult else { throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.pairedKeyResult") } currentState = .receivedPairedKeyResult } - - private func makeFileDestinationURL(_ initialDest:URL) -> URL { - var dest=initialDest - if FileManager.default.fileExists(atPath: dest.path){ - var counter=1 - var path:String - let ext=dest.pathExtension - let baseUrl=dest.deletingPathExtension() - repeat{ - path="\(baseUrl.path) (\(counter))" - if !ext.isEmpty{ - path+=".\(ext)" + + private func makeFileDestinationURL(_ initialDest: URL) -> URL { + var dest = initialDest + if FileManager.default.fileExists(atPath: dest.path) { + var counter = 1 + var path: String + let ext = dest.pathExtension + let baseUrl = dest.deletingPathExtension() + repeat { + path = "\(baseUrl.path) (\(counter))" + if !ext.isEmpty { + path += ".\(ext)" } - counter+=1 - }while FileManager.default.fileExists(atPath: path) - dest=URL(fileURLWithPath: path) + counter += 1 + } while FileManager.default.fileExists(atPath: path) + dest = URL(fileURLWithPath: path) } return dest } - - private func processIntroductionFrame(_ frame:Sharing_Nearby_Frame) throws{ + + private func processIntroductionFrame(_ frame: Sharing_Nearby_Frame) throws { guard frame.hasV1, frame.v1.hasIntroduction else { throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.introduction") } currentState = .waitingForUserConsent - - if frame.v1.introduction.fileMetadata.count>0 && frame.v1.introduction.textMetadata.isEmpty{ - + + if frame.v1.introduction.fileMetadata.count > 0 && frame.v1.introduction.textMetadata.isEmpty { let saveDirectory = SaveFilesManager.shared.getSaveDirectory() - + for file in frame.v1.introduction.fileMetadata { - let dest = makeFileDestinationURL(saveDirectory.appendingPathComponent(file.name)) let info = InternalFileInfo(meta: FileMetadata(name: file.name, size: file.size, mimeType: file.mimeType), payloadID: file.payloadID, destinationURL: dest) transferredFiles[file.payloadID] = info } - let metadata=TransferMetadata(files: transferredFiles.map({$0.value.meta}), id: id, pinCode: pinCode) + let metadata = TransferMetadata(files: transferredFiles.map { $0.value.meta }, id: id, pinCode: pinCode) DispatchQueue.main.async { self.delegate?.obtainUserConsent(for: metadata, from: self.remoteDeviceInfo!, connection: self) } - }else if frame.v1.introduction.textMetadata.count==1{ - let meta=frame.v1.introduction.textMetadata[0] - if case .url=meta.type{ - let metadata=TransferMetadata(files: [], id: id, pinCode: pinCode, textDescription: meta.textTitle) - textPayloadID=meta.payloadID + } else if frame.v1.introduction.textMetadata.count == 1 { + let meta = frame.v1.introduction.textMetadata[0] + if case .url = meta.type { + let metadata = TransferMetadata(files: [], id: id, pinCode: pinCode, textDescription: meta.textTitle) + textPayloadID = meta.payloadID DispatchQueue.main.async { self.delegate?.obtainUserConsent(for: metadata, from: self.remoteDeviceInfo!, connection: self) } - }else if case .text=meta.type{ - + } else if case .text = meta.type { let saveDirectory = SaveFilesManager.shared.getSaveDirectory() - - let dateFormatter=DateFormatter() - dateFormatter.dateFormat="yyyy-MM-dd HH.mm.ss" - let dest=makeFileDestinationURL(saveDirectory.appendingPathComponent("\(dateFormatter.string(from: Date())).txt")) - let info=InternalFileInfo(meta: FileMetadata(name: dest.lastPathComponent, size: meta.size, mimeType: "text/plain"), - payloadID: meta.payloadID, - destinationURL: dest) - transferredFiles[meta.payloadID]=info + + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH.mm.ss" + let dest = makeFileDestinationURL(saveDirectory.appendingPathComponent("\(dateFormatter.string(from: Date())).txt")) + let info = InternalFileInfo(meta: FileMetadata(name: dest.lastPathComponent, size: meta.size, mimeType: "text/plain"), + payloadID: meta.payloadID, + destinationURL: dest) + transferredFiles[meta.payloadID] = info DispatchQueue.main.async { self.delegate?.obtainUserConsent(for: TransferMetadata(files: [info.meta], id: self.id, pinCode: self.pinCode), from: self.remoteDeviceInfo!, connection: self) } - }else{ + } else { rejectTransfer(with: .unsupportedAttachmentType) } - }else{ + } else { rejectTransfer(with: .unsupportedAttachmentType) } } - - func submitUserConsent(accepted:Bool, storeInTemp: Bool = false){ + + func submitUserConsent(accepted: Bool, storeInTemp: Bool = false) { DispatchQueue.global(qos: .utility).async { - if accepted{ + if accepted { self.acceptTransfer(storeInTemp: storeInTemp) - }else{ + } else { self.rejectTransfer() } } } - + private func acceptTransfer(storeInTemp: Bool) { - if currentState == .disconnected { log("Detected timeout, not accepting transfer") return } - + do { if storeInTemp { try FileManager.default.createDirectory(at: SaveFilesManager.shared.tempDirectory, withIntermediateDirectories: true) } - + for (id, file) in transferredFiles { let targetURL = storeInTemp ? SaveFilesManager.shared.tempDirectory.appendingPathComponent(file.destinationURL.lastPathComponent) : file.destinationURL - + FileManager.default.createFile(atPath: targetURL.path, contents: nil) let handle = try FileHandle(forWritingTo: targetURL) transferredFiles[id]!.fileHandle = handle - + let progress = Progress() progress.fileURL = targetURL progress.totalUnitCount = file.meta.size @@ -431,7 +422,7 @@ class InboundNearbyConnection: NearbyConnection{ transferredFiles[id]!.progress = progress transferredFiles[id]!.created = true } - + var frame = Sharing_Nearby_Frame() frame.version = .v1 frame.v1.type = .response @@ -443,24 +434,22 @@ class InboundNearbyConnection: NearbyConnection{ protocolError() } } - - - private func rejectTransfer(with reason:Sharing_Nearby_ConnectionResponseFrame.Status = .reject){ - var frame=Sharing_Nearby_Frame() + + private func rejectTransfer(with reason: Sharing_Nearby_ConnectionResponseFrame.Status = .reject) { + var frame = Sharing_Nearby_Frame() frame.version = .v1 frame.v1.type = .response frame.v1.connectionResponse.status = reason - do{ + do { try sendTransferSetupFrame(frame) try sendDisconnectionAndDisconnect() - }catch{ + } catch { log("Error \(error)") protocolError() } } - - - private func deletePartiallyReceivedFiles() throws{ + + private func deletePartiallyReceivedFiles() throws { for (_, file) in transferredFiles { guard file.created else { continue } try FileManager.default.removeItem(at: file.destinationURL) @@ -468,48 +457,42 @@ class InboundNearbyConnection: NearbyConnection{ } } -protocol InboundNearbyConnectionDelegate{ - func obtainUserConsent(for transfer:TransferMetadata, from device:RemoteDeviceInfo, connection:InboundNearbyConnection) - func connectionWasTerminated(connection:InboundNearbyConnection, error:Error?) +protocol InboundNearbyConnectionDelegate { + func obtainUserConsent(for transfer: TransferMetadata, from device: RemoteDeviceInfo, connection: InboundNearbyConnection) + func connectionWasTerminated(connection: InboundNearbyConnection, error: Error?) } - public class SaveFilesManager { - private init() {} - + public static let shared = SaveFilesManager() - - internal let tempDirectory: URL = FileManager.default.temporaryDirectory.appendingPathComponent("Pending") + + let tempDirectory: URL = FileManager.default.temporaryDirectory.appendingPathComponent("Pending") private var securityScopeUrl: URL? - + private var filesFinishedDownloading = [URL]() - - + public func registerFileFinishedDownloading(_ fileURL: URL) { filesFinishedDownloading.append(fileURL) } - - + public func movePendingFilesToTarget() { - if !isPlusVersion() { return } - + log("Moving pending files to target directory") - + do { let fileManager = FileManager.default let files = try fileManager.contentsOfDirectory(at: tempDirectory, includingPropertiesForKeys: nil) - + let target = getSaveDirectory() - + for file in files { - let fileName = file.lastPathComponent let destinationURL = target.appendingPathComponent(fileName) - + if !filesFinishedDownloading.contains(destinationURL) { log("File \(file) not finished downloading, skipping") continue @@ -517,74 +500,69 @@ public class SaveFilesManager { log("Moving file: \(file.lastPathComponent) to \(destinationURL.lastPathComponent)") try fileManager.copyItem(at: file, to: destinationURL) - + let progress = Progress() progress.fileURL = destinationURL progress.totalUnitCount = 10 progress.kind = .file progress.isPausable = false progress.publish() - + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { progress.completedUnitCount = 10 progress.unpublish() } - + try fileManager.removeItem(at: file) } - + stopAccessingSecurityScopedResource() - + log("Moved all pending files to target directory") - + } catch { log("Error moving pending files: \(error)") } } - - + public func stopAccessingSecurityScopedResource() { guard let url = securityScopeUrl else { return } - + log("Stopping access to security scoped resource: \(url)") url.stopAccessingSecurityScopedResource() - self.securityScopeUrl = nil + securityScopeUrl = nil } - - + public func getSaveDirectory() -> URL { - if let securityScopeUrl = securityScopeUrl { log("Using existing security scope URL: \(securityScopeUrl)") return securityScopeUrl } - + if let bookmarkData = UserDefaults.standard.data(forKey: UserDefaultsKeys.saveFolderBookmark.rawValue) { - var isStale = false - + do { let url = try URL(resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale) - + if !isStale { if url.startAccessingSecurityScopedResource() { - log("Successfully accessed security scoped resource: \(url)") - - self.securityScopeUrl = url + + securityScopeUrl = url return url } } else { print("Bookmark is stale, using default downloads folder.") } - + } catch { print("Failed to resolve bookmark: \(error), using default downloads folder.") } } - + do { return try FileManager.default.url(for: .downloadsDirectory, in: .userDomainMask, appropriateFor: nil, create: true).resolvingSymlinksInPath() } catch { @@ -606,7 +584,8 @@ extension String { while index < endIndex { let nextIndex = self.index(index, offsetBy: 2) if nextIndex <= endIndex, - let byte = UInt8(self[index.. Bool{ + + func isServer() -> Bool { fatalError() } - - internal func processFileChunk(frame:Location_Nearby_Connections_PayloadTransferFrame) throws{ + + func processFileChunk(frame _: Location_Nearby_Connections_PayloadTransferFrame) throws { protocolError() } - - internal func processBytesPayload(payload:Data, id:Int64)throws ->Bool{ + + func processBytesPayload(payload _: Data, id _: Int64) throws -> Bool { return false } - - private func receiveFrameAsync(){ - connection.receive(minimumIncompleteLength: 4, maximumLength: 4) { content, contentContext, isComplete, error in - if self.connectionClosed{ + + private func receiveFrameAsync() { + connection.receive(minimumIncompleteLength: 4, maximumLength: 4) { content, _, isComplete, error in + if self.connectionClosed { return } - if isComplete{ + if isComplete { self.handleConnectionClosure() return } - if !(error==nil){ - self.lastError=error + if !(error == nil) { + self.lastError = error self.protocolError() return } - guard let content=content else { + guard let content = content else { assertionFailure() return } - let frameLength:UInt32=UInt32(content[0]) << 24 | UInt32(content[1]) << 16 | UInt32(content[2]) << 8 | UInt32(content[3]) - guard frameLengthVoid)?=nil){ - if connectionClosed{ + + func sendFrameAsync(_ frame: Data, completion: (() -> Void)? = nil) { + if connectionClosed { return } - var lengthPrefixedData=Data(capacity: frame.count+4) - let length:Int=frame.count + var lengthPrefixedData = Data(capacity: frame.count + 4) + let length: Int = frame.count lengthPrefixedData.append(contentsOf: [ UInt8(truncatingIfNeeded: length >> 24), UInt8(truncatingIfNeeded: length >> 16), UInt8(truncatingIfNeeded: length >> 8), - UInt8(truncatingIfNeeded: length) + UInt8(truncatingIfNeeded: length), ]) lengthPrefixedData.append(frame) - connection.send(content: lengthPrefixedData, completion: .contentProcessed({ error in - if let completion=completion{ + connection.send(content: lengthPrefixedData, completion: .contentProcessed { _ in + if let completion = completion { completion() } - })) + }) } - - internal func encryptAndSendOfflineFrame(_ frame:Location_Nearby_Connections_OfflineFrame, completion:(()->Void)?=nil) throws{ - var d2dMsg=Securegcm_DeviceToDeviceMessage() - serverSeq+=1 - d2dMsg.sequenceNumber=serverSeq - d2dMsg.message=try frame.serializedData() - - let serializedMsg=[UInt8](try d2dMsg.serializedData()) - let iv=Data.randomData(length: 16) - var encryptedData=Data(count: serializedMsg.count+16) - var encryptedLength:size_t=0 - encryptedData.withUnsafeMutableBytes({ - let status=CCCrypt( + + func encryptAndSendOfflineFrame(_ frame: Location_Nearby_Connections_OfflineFrame, completion: (() -> Void)? = nil) throws { + var d2dMsg = Securegcm_DeviceToDeviceMessage() + serverSeq += 1 + d2dMsg.sequenceNumber = serverSeq + d2dMsg.message = try frame.serializedData() + + let serializedMsg = try [UInt8](d2dMsg.serializedData()) + let iv = Data.randomData(length: 16) + var encryptedData = Data(count: serializedMsg.count + 16) + var encryptedLength: size_t = 0 + encryptedData.withUnsafeMutableBytes { + let status = CCCrypt( CCOperation(kCCEncrypt), CCAlgorithm(kCCAlgorithmAES128), CCOptions(kCCOptionPKCS7Padding), @@ -182,69 +182,69 @@ class NearbyConnection{ $0.baseAddress, $0.count, &encryptedLength ) - guard status==kCCSuccess else { fatalError("CCCrypt error: \(status)") } - }) - - var hb=Securemessage_HeaderAndBody() - hb.body=encryptedData.prefix(encryptedLength) - hb.header=Securemessage_Header() + guard status == kCCSuccess else { fatalError("CCCrypt error: \(status)") } + } + + var hb = Securemessage_HeaderAndBody() + hb.body = encryptedData.prefix(encryptedLength) + hb.header = Securemessage_Header() hb.header.encryptionScheme = .aes256Cbc hb.header.signatureScheme = .hmacSha256 - hb.header.iv=iv - var md=Securegcm_GcmMetadata() + hb.header.iv = iv + var md = Securegcm_GcmMetadata() md.type = .deviceToDeviceMessage - md.version=1 - hb.header.publicMetadata=try md.serializedData() - - var smsg=Securemessage_SecureMessage() - smsg.headerAndBody=try hb.serializedData() - smsg.signature=Data(HMAC.authenticationCode(for: smsg.headerAndBody, using: sendHmacKey!)) - sendFrameAsync(try smsg.serializedData(), completion: completion) + md.version = 1 + hb.header.publicMetadata = try md.serializedData() + + var smsg = Securemessage_SecureMessage() + smsg.headerAndBody = try hb.serializedData() + smsg.signature = Data(HMAC.authenticationCode(for: smsg.headerAndBody, using: sendHmacKey!)) + try sendFrameAsync(smsg.serializedData(), completion: completion) } - - internal func sendTransferSetupFrame(_ frame:Sharing_Nearby_Frame) throws{ - try sendBytesPayload(data: try frame.serializedData(), id: Int64.random(in: Int64.min...Int64.max)) + + func sendTransferSetupFrame(_ frame: Sharing_Nearby_Frame) throws { + try sendBytesPayload(data: frame.serializedData(), id: Int64.random(in: Int64.min ... Int64.max)) } - - internal func sendBytesPayload(data:Data, id:Int64) throws{ - var transfer=Location_Nearby_Connections_PayloadTransferFrame() + + func sendBytesPayload(data: Data, id: Int64) throws { + var transfer = Location_Nearby_Connections_PayloadTransferFrame() transfer.packetType = .data - transfer.payloadChunk.offset=0 - transfer.payloadChunk.flags=0 - transfer.payloadChunk.body=data - transfer.payloadHeader.id=id + transfer.payloadChunk.offset = 0 + transfer.payloadChunk.flags = 0 + transfer.payloadChunk.body = data + transfer.payloadHeader.id = id transfer.payloadHeader.type = .bytes - transfer.payloadHeader.totalSize=Int64(transfer.payloadChunk.body.count) - transfer.payloadHeader.isSensitive=false - - var wrapper=Location_Nearby_Connections_OfflineFrame() + transfer.payloadHeader.totalSize = Int64(transfer.payloadChunk.body.count) + transfer.payloadHeader.isSensitive = false + + var wrapper = Location_Nearby_Connections_OfflineFrame() wrapper.version = .v1 - wrapper.v1=Location_Nearby_Connections_V1Frame() + wrapper.v1 = Location_Nearby_Connections_V1Frame() wrapper.v1.type = .payloadTransfer - wrapper.v1.payloadTransfer=transfer + wrapper.v1.payloadTransfer = transfer try encryptAndSendOfflineFrame(wrapper) - - transfer.payloadChunk.flags=1 // .lastChunk - transfer.payloadChunk.offset=Int64(transfer.payloadChunk.body.count) + + transfer.payloadChunk.flags = 1 // .lastChunk + transfer.payloadChunk.offset = Int64(transfer.payloadChunk.body.count) transfer.payloadChunk.clearBody() - wrapper.v1.payloadTransfer=transfer + wrapper.v1.payloadTransfer = transfer try encryptAndSendOfflineFrame(wrapper) } - - internal func decryptAndProcessReceivedSecureMessage(_ smsg:Securemessage_SecureMessage) throws{ + + func decryptAndProcessReceivedSecureMessage(_ smsg: Securemessage_SecureMessage) throws { guard smsg.hasSignature, smsg.hasHeaderAndBody else { throw NearbyError.requiredFieldMissing("secureMessage.signature|headerAndBody") } - + if NearbyConnectionManager.shared.checkSignature { let hmac = Data(HMAC.authenticationCode(for: smsg.headerAndBody, using: recvHmacKey!)) guard hmac == smsg.signature else { throw NearbyError.protocolError("hmac!=signature: Expected \(hmac), got \(smsg.signature)") } } - - let headerAndBody=try Securemessage_HeaderAndBody(serializedBytes: smsg.headerAndBody) - var decryptedData=Data(count: headerAndBody.body.count) - - var decryptedLength:Int=0 - decryptedData.withUnsafeMutableBytes({ - let status=CCCrypt( + + let headerAndBody = try Securemessage_HeaderAndBody(serializedBytes: smsg.headerAndBody) + var decryptedData = Data(count: headerAndBody.body.count) + + var decryptedLength = 0 + decryptedData.withUnsafeMutableBytes { + let status = CCCrypt( CCOperation(kCCDecrypt), CCAlgorithm(kCCAlgorithmAES128), CCOptions(kCCOptionPKCS7Padding), @@ -254,209 +254,209 @@ class NearbyConnection{ $0.baseAddress, $0.count, &decryptedLength ) - guard status==kCCSuccess else { fatalError("CCCrypt error: \(status)") } - }) - decryptedData=decryptedData.prefix(decryptedLength) - let d2dMsg=try Securegcm_DeviceToDeviceMessage(serializedBytes: decryptedData) + guard status == kCCSuccess else { fatalError("CCCrypt error: \(status)") } + } + decryptedData = decryptedData.prefix(decryptedLength) + let d2dMsg = try Securegcm_DeviceToDeviceMessage(serializedBytes: decryptedData) guard d2dMsg.hasMessage, d2dMsg.hasSequenceNumber else { throw NearbyError.requiredFieldMissing("d2dMessage.message|sequenceNumber") } - clientSeq+=1 - guard d2dMsg.sequenceNumber==clientSeq else { throw NearbyError.protocolError("Wrong sequence number. Expected \(clientSeq), got \(d2dMsg.sequenceNumber)") } - let offlineFrame=try Location_Nearby_Connections_OfflineFrame(serializedBytes: d2dMsg.message) - - if offlineFrame.hasV1 && offlineFrame.v1.hasType, case .payloadTransfer = offlineFrame.v1.type { + clientSeq += 1 + guard d2dMsg.sequenceNumber == clientSeq else { throw NearbyError.protocolError("Wrong sequence number. Expected \(clientSeq), got \(d2dMsg.sequenceNumber)") } + let offlineFrame = try Location_Nearby_Connections_OfflineFrame(serializedBytes: d2dMsg.message) + + if offlineFrame.hasV1, offlineFrame.v1.hasType, case .payloadTransfer = offlineFrame.v1.type { guard offlineFrame.v1.hasPayloadTransfer else { throw NearbyError.requiredFieldMissing("offlineFrame.v1.payloadTransfer") } - let payloadTransfer=offlineFrame.v1.payloadTransfer - let header=payloadTransfer.payloadHeader; - let chunk=payloadTransfer.payloadChunk; + let payloadTransfer = offlineFrame.v1.payloadTransfer + let header = payloadTransfer.payloadHeader + let chunk = payloadTransfer.payloadChunk guard header.hasType, header.hasID else { throw NearbyError.requiredFieldMissing("payloadHeader.type|id") } guard payloadTransfer.hasPayloadChunk, chunk.hasOffset, chunk.hasFlags else { throw NearbyError.requiredFieldMissing("payloadTransfer.payloadChunk|offset|flags") } - if case .bytes = header.type{ - let payloadID=header.id - if header.totalSize>InboundNearbyConnection.SANE_FRAME_LENGTH{ + if case .bytes = header.type { + let payloadID = header.id + if header.totalSize > InboundNearbyConnection.SANE_FRAME_LENGTH { payloadBuffers.removeValue(forKey: payloadID) throw NearbyError.protocolError("Payload too large (\(header.totalSize) bytes)") } - if payloadBuffers[payloadID]==nil { - payloadBuffers[payloadID]=NSMutableData(capacity: Int(header.totalSize)) + if payloadBuffers[payloadID] == nil { + payloadBuffers[payloadID] = NSMutableData(capacity: Int(header.totalSize)) } - let buffer=payloadBuffers[payloadID]! - guard chunk.offset==buffer.count else { + let buffer = payloadBuffers[payloadID]! + guard chunk.offset == buffer.count else { payloadBuffers.removeValue(forKey: payloadID) throw NearbyError.protocolError("Unexpected chunk offset \(chunk.offset), expected \(buffer.count)") } if chunk.hasBody { buffer.append(chunk.body) } - if (chunk.flags & 1)==1 { + if (chunk.flags & 1) == 1 { payloadBuffers.removeValue(forKey: payloadID) - if !(try processBytesPayload(payload: Data(buffer), id: payloadID)){ - let innerFrame=try Sharing_Nearby_Frame(serializedBytes: buffer as Data) + if try !processBytesPayload(payload: Data(buffer), id: payloadID) { + let innerFrame = try Sharing_Nearby_Frame(serializedBytes: buffer as Data) try processTransferSetupFrame(innerFrame) } } - }else if case .file = header.type{ + } else if case .file = header.type { try processFileChunk(frame: payloadTransfer) } - }else if offlineFrame.hasV1 && offlineFrame.v1.hasType, case .keepAlive = offlineFrame.v1.type{ + } else if offlineFrame.hasV1, offlineFrame.v1.hasType, case .keepAlive = offlineFrame.v1.type { log("Sent keep-alive") sendKeepAlive(ack: true) - }else{ + } else { log("Unhandled offline frame encrypted: \(offlineFrame)") } } - - internal static func pinCodeFromAuthKey(_ key:SymmetricKey) -> String{ - var hash:Int=0 - var multiplier:Int=1 - let keyBytes:[UInt8]=key.withUnsafeBytes({ - return [UInt8]($0) - }) - - for _byte in keyBytes { - let byte=Int(Int8(bitPattern: _byte)) - hash=(hash+byte*multiplier)%9973 - multiplier=(multiplier*31)%9973 + + static func pinCodeFromAuthKey(_ key: SymmetricKey) -> String { + var hash = 0 + var multiplier = 1 + let keyBytes: [UInt8] = key.withUnsafeBytes { + [UInt8]($0) } - + + for _byte in keyBytes { + let byte = Int(Int8(bitPattern: _byte)) + hash = (hash + byte * multiplier) % 9973 + multiplier = (multiplier * 31) % 9973 + } + return String(format: "%04d", abs(hash)) } - - internal static func hkdfExtract(salt:Data, ikm:Data) -> Data{ - return HMAC.authenticationCode(for: ikm, using: SymmetricKey(data: salt)).withUnsafeBytes({return Data(bytes: $0.baseAddress!, count: $0.count)}) + + static func hkdfExtract(salt: Data, ikm: Data) -> Data { + return HMAC.authenticationCode(for: ikm, using: SymmetricKey(data: salt)).withUnsafeBytes { Data(bytes: $0.baseAddress!, count: $0.count) } } - - internal static func hkdfExpand(prk:Data, info:Data, length:Int) -> Data{ - var okm=Data() - var t=Data() - var i=0 - while okm.count.authenticationCode(for: toDigest, using: SymmetricKey(data: prk)).withUnsafeBytes({return Data(bytes: $0.baseAddress!, count: $0.count)}) - okm=okm+t + + static func hkdfExpand(prk: Data, info: Data, length: Int) -> Data { + var okm = Data() + var t = Data() + var i = 0 + while okm.count < length { + i = i + 1 + let toDigest = t + info + Data([UInt8(truncatingIfNeeded: i)]) + t = HMAC.authenticationCode(for: toDigest, using: SymmetricKey(data: prk)).withUnsafeBytes { Data(bytes: $0.baseAddress!, count: $0.count) } + okm = okm + t } - return okm.subdata(in: 0.. SymmetricKey{ - if #available(macOS 11.0, *){ + + static func hkdf(inputKeyMaterial: SymmetricKey, salt: Data, info: Data, outputByteCount: Int) -> SymmetricKey { + if #available(macOS 11.0, *) { return HKDF.deriveKey(inputKeyMaterial: inputKeyMaterial, salt: salt, info: info, outputByteCount: outputByteCount) - }else{ - return SymmetricKey(data: hkdfExpand(prk: hkdfExtract(salt: salt, ikm: inputKeyMaterial.withUnsafeBytes({return Data(bytes: $0.baseAddress!, count: $0.count)})), info: info, length: outputByteCount)) + } else { + return SymmetricKey(data: hkdfExpand(prk: hkdfExtract(salt: salt, ikm: inputKeyMaterial.withUnsafeBytes { Data(bytes: $0.baseAddress!, count: $0.count) }), info: info, length: outputByteCount)) } } - - internal func finalizeKeyExchange(peerKey:Securemessage_GenericPublicKey) throws{ + + func finalizeKeyExchange(peerKey: Securemessage_GenericPublicKey) throws { guard peerKey.hasEcP256PublicKey else { throw NearbyError.requiredFieldMissing("peerKey.ecP256PublicKey") } - - let domain=Domain.instance(curve: .EC256r1) - var clientX=peerKey.ecP256PublicKey.x - var clientY=peerKey.ecP256PublicKey.y - if clientX.count>32{ - clientX=clientX.suffix(32) + + let domain = Domain.instance(curve: .EC256r1) + var clientX = peerKey.ecP256PublicKey.x + var clientY = peerKey.ecP256PublicKey.y + if clientX.count > 32 { + clientX = clientX.suffix(32) } - if clientY.count>32{ - clientY=clientY.suffix(32) + if clientY.count > 32 { + clientY = clientY.suffix(32) } - let key=try ECPublicKey(domain: domain, w: Point(BInt(magnitude: [UInt8](clientX)), BInt(magnitude: [UInt8](clientY)))) - - let dhs=(try privateKey?.domain.multiplyPoint(key.w, privateKey!.s).x.asMagnitudeBytes())! - var sha=SHA256() + let key = try ECPublicKey(domain: domain, w: Point(BInt(magnitude: [UInt8](clientX)), BInt(magnitude: [UInt8](clientY)))) + + let dhs = try (privateKey?.domain.multiplyPoint(key.w, privateKey!.s).x.asMagnitudeBytes())! + var sha = SHA256() sha.update(data: dhs) - let derivedSecretKey=Data(sha.finalize()) - - var ukeyInfo=Data() + let derivedSecretKey = Data(sha.finalize()) + + var ukeyInfo = Data() ukeyInfo.append(ukeyClientInitMsgData!) ukeyInfo.append(ukeyServerInitMsgData!) - let authString=NearbyConnection.hkdf(inputKeyMaterial: SymmetricKey(data: derivedSecretKey), salt: "UKEY2 v1 auth".data(using: .utf8)!, info: ukeyInfo, outputByteCount: 32) - let nextSecret=NearbyConnection.hkdf(inputKeyMaterial: SymmetricKey(data: derivedSecretKey), salt: "UKEY2 v1 next".data(using: .utf8)!, info: ukeyInfo, outputByteCount: 32) - - pinCode=NearbyConnection.pinCodeFromAuthKey(authString) - - let salt:Data=Data([0x82, 0xAA, 0x55, 0xA0, 0xD3, 0x97, 0xF8, 0x83, 0x46, 0xCA, 0x1C, - 0xEE, 0x8D, 0x39, 0x09, 0xB9, 0x5F, 0x13, 0xFA, 0x7D, 0xEB, 0x1D, - 0x4A, 0xB3, 0x83, 0x76, 0xB8, 0x25, 0x6D, 0xA8, 0x55, 0x10]) - - let d2dClientKey=NearbyConnection.hkdf(inputKeyMaterial: nextSecret, salt: salt, info: "client".data(using: .utf8)!, outputByteCount: 32) - let d2dServerKey=NearbyConnection.hkdf(inputKeyMaterial: nextSecret, salt: salt, info: "server".data(using: .utf8)!, outputByteCount: 32) - - sha=SHA256() + let authString = NearbyConnection.hkdf(inputKeyMaterial: SymmetricKey(data: derivedSecretKey), salt: "UKEY2 v1 auth".data(using: .utf8)!, info: ukeyInfo, outputByteCount: 32) + let nextSecret = NearbyConnection.hkdf(inputKeyMaterial: SymmetricKey(data: derivedSecretKey), salt: "UKEY2 v1 next".data(using: .utf8)!, info: ukeyInfo, outputByteCount: 32) + + pinCode = NearbyConnection.pinCodeFromAuthKey(authString) + + let salt = Data([0x82, 0xAA, 0x55, 0xA0, 0xD3, 0x97, 0xF8, 0x83, 0x46, 0xCA, 0x1C, + 0xEE, 0x8D, 0x39, 0x09, 0xB9, 0x5F, 0x13, 0xFA, 0x7D, 0xEB, 0x1D, + 0x4A, 0xB3, 0x83, 0x76, 0xB8, 0x25, 0x6D, 0xA8, 0x55, 0x10]) + + let d2dClientKey = NearbyConnection.hkdf(inputKeyMaterial: nextSecret, salt: salt, info: "client".data(using: .utf8)!, outputByteCount: 32) + let d2dServerKey = NearbyConnection.hkdf(inputKeyMaterial: nextSecret, salt: salt, info: "server".data(using: .utf8)!, outputByteCount: 32) + + sha = SHA256() sha.update(data: "SecureMessage".data(using: .utf8)!) - let smsgSalt=Data(sha.finalize()) - - let clientKey=NearbyConnection.hkdf(inputKeyMaterial: d2dClientKey, salt: smsgSalt, info: "ENC:2".data(using: .utf8)!, outputByteCount: 32).withUnsafeBytes({return [UInt8]($0)}) - let clientHmacKey=NearbyConnection.hkdf(inputKeyMaterial: d2dClientKey, salt: smsgSalt, info: "SIG:1".data(using: .utf8)!, outputByteCount: 32) - let serverKey=NearbyConnection.hkdf(inputKeyMaterial: d2dServerKey, salt: smsgSalt, info: "ENC:2".data(using: .utf8)!, outputByteCount: 32).withUnsafeBytes({return [UInt8]($0)}) - let serverHmacKey=NearbyConnection.hkdf(inputKeyMaterial: d2dServerKey, salt: smsgSalt, info: "SIG:1".data(using: .utf8)!, outputByteCount: 32) - - if isServer(){ - decryptKey=clientKey - recvHmacKey=clientHmacKey - encryptKey=serverKey - sendHmacKey=serverHmacKey - }else{ - decryptKey=serverKey - recvHmacKey=serverHmacKey - encryptKey=clientKey - sendHmacKey=clientHmacKey + let smsgSalt = Data(sha.finalize()) + + let clientKey = NearbyConnection.hkdf(inputKeyMaterial: d2dClientKey, salt: smsgSalt, info: "ENC:2".data(using: .utf8)!, outputByteCount: 32).withUnsafeBytes { [UInt8]($0) } + let clientHmacKey = NearbyConnection.hkdf(inputKeyMaterial: d2dClientKey, salt: smsgSalt, info: "SIG:1".data(using: .utf8)!, outputByteCount: 32) + let serverKey = NearbyConnection.hkdf(inputKeyMaterial: d2dServerKey, salt: smsgSalt, info: "ENC:2".data(using: .utf8)!, outputByteCount: 32).withUnsafeBytes { [UInt8]($0) } + let serverHmacKey = NearbyConnection.hkdf(inputKeyMaterial: d2dServerKey, salt: smsgSalt, info: "SIG:1".data(using: .utf8)!, outputByteCount: 32) + + if isServer() { + decryptKey = clientKey + recvHmacKey = clientHmacKey + encryptKey = serverKey + sendHmacKey = serverHmacKey + } else { + decryptKey = serverKey + recvHmacKey = serverHmacKey + encryptKey = clientKey + sendHmacKey = clientHmacKey } } - - internal func disconnect(){ - connection.send(content: nil, isComplete: true, completion: .contentProcessed({ error in + + func disconnect() { + connection.send(content: nil, isComplete: true, completion: .contentProcessed { _ in self.handleConnectionClosure() - })) - connectionClosed=true + }) + connectionClosed = true } - - internal func sendDisconnectionAndDisconnect() throws{ - var offlineFrame=Location_Nearby_Connections_OfflineFrame() + + func sendDisconnectionAndDisconnect() throws { + var offlineFrame = Location_Nearby_Connections_OfflineFrame() offlineFrame.version = .v1 offlineFrame.v1.type = .disconnection - offlineFrame.v1.disconnection=Location_Nearby_Connections_DisconnectionFrame() - - if encryptionDone{ + offlineFrame.v1.disconnection = Location_Nearby_Connections_DisconnectionFrame() + + if encryptionDone { try encryptAndSendOfflineFrame(offlineFrame) - }else{ - sendFrameAsync(try offlineFrame.serializedData()) + } else { + try sendFrameAsync(offlineFrame.serializedData()) } disconnect() } - - internal func sendUkey2Alert(type:Securegcm_Ukey2Alert.AlertType){ - var alert=Securegcm_Ukey2Alert() - alert.type=type - var msg=Securegcm_Ukey2Message() + + func sendUkey2Alert(type: Securegcm_Ukey2Alert.AlertType) { + var alert = Securegcm_Ukey2Alert() + alert.type = type + var msg = Securegcm_Ukey2Message() msg.messageType = .alert msg.messageData = try! alert.serializedData() sendFrameAsync(try! msg.serializedData()) disconnect() } - - internal func sendKeepAlive(ack:Bool){ - var offlineFrame=Location_Nearby_Connections_OfflineFrame() + + func sendKeepAlive(ack: Bool) { + var offlineFrame = Location_Nearby_Connections_OfflineFrame() offlineFrame.version = .v1 offlineFrame.v1.type = .keepAlive - offlineFrame.v1.keepAlive.ack=ack - - do{ - if encryptionDone{ + offlineFrame.v1.keepAlive.ack = ack + + do { + if encryptionDone { try encryptAndSendOfflineFrame(offlineFrame) - }else{ - sendFrameAsync(try offlineFrame.serializedData()) + } else { + try sendFrameAsync(offlineFrame.serializedData()) } - }catch{ + } catch { log("Error sending KEEP_ALIVE: \(error)") } } } -struct InternalFileInfo{ - let meta:FileMetadata - let payloadID:Int64 - let destinationURL:URL - var bytesTransferred:Int64=0 - var fileHandle:FileHandle? - var progress:Progress? - var created:Bool=false +struct InternalFileInfo { + let meta: FileMetadata + let payloadID: Int64 + let destinationURL: URL + var bytesTransferred: Int64 = 0 + var fileHandle: FileHandle? + var progress: Progress? + var created: Bool = false } diff --git a/NearbyShare/NearbyConnectionManager.swift b/NearbyShare/NearbyConnectionManager.swift index a51df22..75b661c 100644 --- a/NearbyShare/NearbyConnectionManager.swift +++ b/NearbyShare/NearbyConnectionManager.swift @@ -9,147 +9,145 @@ import Foundation import Network import System -public struct RemoteDeviceInfo{ - public let name:String - public let type:DeviceType - public var id:String? - - init(name: String, type: DeviceType, id: String? = nil) { - self.name = name - self.type = type - self.id = id - } - - init(info:EndpointInfo){ - self.name=info.name - self.type=info.deviceType - } - - public enum DeviceType:Int32{ - case unknown=0 - case phone - case tablet - case computer - - public static func fromRawValue(value:Int) -> DeviceType{ - switch value { - case 0: - return .unknown - case 1: - return .phone - case 2: - return .tablet - case 3: - return .computer - default: - return .unknown - } - } - } +public struct RemoteDeviceInfo { + public let name: String + public let type: DeviceType + public var id: String? + + init(name: String, type: DeviceType, id: String? = nil) { + self.name = name + self.type = type + self.id = id + } + + init(info: EndpointInfo) { + name = info.name + type = info.deviceType + } + + public enum DeviceType: Int32 { + case unknown = 0 + case phone + case tablet + case computer + + public static func fromRawValue(value: Int) -> DeviceType { + switch value { + case 0: + return .unknown + case 1: + return .phone + case 2: + return .tablet + case 3: + return .computer + default: + return .unknown + } + } + } } +public enum NearbyError: Error { + case protocolError(_ message: String) + case requiredFieldMissing(_ message: String) + case ukey2 + case inputOutput + case canceled(reason: CancellationReason) -public enum NearbyError:Error{ - case protocolError(_ message:String) - case requiredFieldMissing(_ message:String) - case ukey2 - case inputOutput - case canceled(reason:CancellationReason) - - public enum CancellationReason{ - case userRejected, userCanceled, notEnoughSpace, unsupportedType, timedOut - } + public enum CancellationReason { + case userRejected, userCanceled, notEnoughSpace, unsupportedType, timedOut + } } -public struct TransferMetadata{ - public let files:[FileMetadata] - public let id:String - public let pinCode:String? - public let textDescription:String? - - init(files: [FileMetadata], id: String, pinCode: String?, textDescription: String?=nil){ - self.files = files - self.id = id - self.pinCode = pinCode - self.textDescription = textDescription - } +public struct TransferMetadata { + public let files: [FileMetadata] + public let id: String + public let pinCode: String? + public let textDescription: String? + + init(files: [FileMetadata], id: String, pinCode: String?, textDescription: String? = nil) { + self.files = files + self.id = id + self.pinCode = pinCode + self.textDescription = textDescription + } } -public struct FileMetadata{ - public let name:String - public let size:Int64 - public let mimeType:String +public struct FileMetadata { + public let name: String + public let size: Int64 + public let mimeType: String } -struct FoundServiceInfo{ - let service:NWBrowser.Result - var device:RemoteDeviceInfo? +struct FoundServiceInfo { + let service: NWBrowser.Result + var device: RemoteDeviceInfo? } -struct OutgoingTransferInfo{ - let service:NWBrowser.Result - let device:RemoteDeviceInfo - let connection:OutboundNearbyConnection - let delegate:ShareExtensionDelegate +struct OutgoingTransferInfo { + let service: NWBrowser.Result + let device: RemoteDeviceInfo + let connection: OutboundNearbyConnection + let delegate: ShareExtensionDelegate } -struct EndpointInfo{ - let name:String - let deviceType:RemoteDeviceInfo.DeviceType - - init(name: String, deviceType: RemoteDeviceInfo.DeviceType){ - self.name = name - self.deviceType = deviceType - } - - init?(data:Data){ - guard data.count>17 else {return nil} - let deviceNameLength=Int(data[17]) - guard data.count>=deviceNameLength+18 else {return nil} - guard let deviceName=String(data: data[18..<(18+deviceNameLength)], encoding: .utf8) else {return nil} - let rawDeviceType:Int=Int(data[0] & 7) >> 1 - self.name=deviceName - self.deviceType=RemoteDeviceInfo.DeviceType.fromRawValue(value: rawDeviceType) - } - - func serialize()->Data{ - // 1 byte: Version(3 bits)|Visibility(1 bit)|Device Type(3 bits)|Reserved(1 bits) - // Device types: unknown=0, phone=1, tablet=2, laptop=3 - var endpointInfo:[UInt8]=[UInt8(deviceType.rawValue << 1)] - // 16 bytes: unknown random bytes - for _ in 0...15{ - endpointInfo.append(UInt8.random(in: 0...255)) - } - // Device name in UTF-8 prefixed with 1-byte length - var nameChars=[UInt8](name.utf8) - if nameChars.count>255{ - nameChars=[UInt8](nameChars[0..<255]) - } - endpointInfo.append(UInt8(nameChars.count)) - for ch in nameChars{ - endpointInfo.append(UInt8(ch)) - } - return Data(endpointInfo) - } +struct EndpointInfo { + let name: String + let deviceType: RemoteDeviceInfo.DeviceType + + init(name: String, deviceType: RemoteDeviceInfo.DeviceType) { + self.name = name + self.deviceType = deviceType + } + + init?(data: Data) { + guard data.count > 17 else { return nil } + let deviceNameLength = Int(data[17]) + guard data.count >= deviceNameLength + 18 else { return nil } + guard let deviceName = String(data: data[18 ..< (18 + deviceNameLength)], encoding: .utf8) else { return nil } + let rawDeviceType = Int(data[0] & 7) >> 1 + name = deviceName + deviceType = RemoteDeviceInfo.DeviceType.fromRawValue(value: rawDeviceType) + } + + func serialize() -> Data { + // 1 byte: Version(3 bits)|Visibility(1 bit)|Device Type(3 bits)|Reserved(1 bits) + // Device types: unknown=0, phone=1, tablet=2, laptop=3 + var endpointInfo: [UInt8] = [UInt8(deviceType.rawValue << 1)] + // 16 bytes: unknown random bytes + for _ in 0 ... 15 { + endpointInfo.append(UInt8.random(in: 0 ... 255)) + } + // Device name in UTF-8 prefixed with 1-byte length + var nameChars = [UInt8](name.utf8) + if nameChars.count > 255 { + nameChars = [UInt8](nameChars[0 ..< 255]) + } + endpointInfo.append(UInt8(nameChars.count)) + for ch in nameChars { + endpointInfo.append(UInt8(ch)) + } + return Data(endpointInfo) + } } -public protocol MainAppDelegate{ - func obtainUserConsent(for transfer:TransferMetadata, from device:RemoteDeviceInfo) - func incomingTransfer(id:String, didFinishWith error:Error?) +public protocol MainAppDelegate { + func obtainUserConsent(for transfer: TransferMetadata, from device: RemoteDeviceInfo) + func incomingTransfer(id: String, didFinishWith error: Error?) } -public protocol ShareExtensionDelegate:AnyObject{ - func addDevice(device:RemoteDeviceInfo) - func removeDevice(id:String) - func connectionWasEstablished(pinCode:String) - func connectionFailed(with error:Error) - func transferAccepted() - func transferProgress(progress:Double) - func transferFinished() +public protocol ShareExtensionDelegate: AnyObject { + func addDevice(device: RemoteDeviceInfo) + func removeDevice(id: String) + func connectionWasEstablished(pinCode: String) + func connectionFailed(with error: Error) + func transferAccepted() + func transferProgress(progress: Double) + func transferFinished() } -public class NearbyConnectionManager : NSObject, NetServiceDelegate, InboundNearbyConnectionDelegate, OutboundNearbyConnectionDelegate{ - +public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearbyConnectionDelegate, OutboundNearbyConnectionDelegate { public var checkSignature = true { didSet { if !checkSignature { @@ -157,127 +155,125 @@ public class NearbyConnectionManager : NSObject, NetServiceDelegate, InboundNear } } } - - private var tcpListener:NWListener; - public let endpointID:[UInt8]=generateEndpointID() + + private var tcpListener: NWListener + public let endpointID: [UInt8] = generateEndpointID() private var mdnsServices: [NetService] = [] - private var activeConnections:[String:InboundNearbyConnection]=[:] - private var foundServices:[String:FoundServiceInfo]=[:] - private var shareExtensionDelegates:[ShareExtensionDelegate]=[] - private var outgoingTransfers:[String:OutgoingTransferInfo]=[:] - public var mainAppDelegate:(any MainAppDelegate)? - private var discoveryRefCount=0 - + private var activeConnections: [String: InboundNearbyConnection] = [:] + private var foundServices: [String: FoundServiceInfo] = [:] + private var shareExtensionDelegates: [ShareExtensionDelegate] = [] + private var outgoingTransfers: [String: OutgoingTransferInfo] = [:] + public var mainAppDelegate: (any MainAppDelegate)? + private var discoveryRefCount = 0 + let serviceTypes = ["_FC9F5ED42C8A._tcp."] - + private var browsers: [NWBrowser] = [] - - public static let shared=NearbyConnectionManager() - - override init() { - tcpListener=try! NWListener(using: NWParameters(tls: .none)) - super.init() - } - - public func becomeVisible(){ - startTCPListener() - } - - private func startTCPListener(){ - tcpListener.stateUpdateHandler={(state:NWListener.State) in - if case .ready = state { - self.initMDNS() - } - } - tcpListener.newConnectionHandler={(connection:NWConnection) in - let id=UUID().uuidString - let conn=InboundNearbyConnection(connection: connection, id: id) - self.activeConnections[id]=conn - conn.delegate=self - conn.start() - } - tcpListener.start(queue: .global(qos: .utility)) - } - + + public static let shared = NearbyConnectionManager() + + override init() { + tcpListener = try! NWListener(using: NWParameters(tls: .none)) + super.init() + } + + public func becomeVisible() { + startTCPListener() + } + + private func startTCPListener() { + tcpListener.stateUpdateHandler = { (state: NWListener.State) in + if case .ready = state { + self.initMDNS() + } + } + tcpListener.newConnectionHandler = { (connection: NWConnection) in + let id = UUID().uuidString + let conn = InboundNearbyConnection(connection: connection, id: id) + self.activeConnections[id] = conn + conn.delegate = self + conn.start() + } + tcpListener.start(queue: .global(qos: .utility)) + } + private static func generateEndpointID() -> [UInt8] { let userDefaultsKey = UserDefaultsKeys.endpointID.rawValue - + // Try to retrieve from UserDefaults if let savedString = UserDefaults.standard.string(forKey: userDefaultsKey), - let savedData = savedString.data(using: .utf8) { + let savedData = savedString.data(using: .utf8) + { return [UInt8](savedData) } - + // Generate a new random ID var id: [UInt8] = [] let alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".compactMap { UInt8($0.asciiValue!) } - for _ in 0..<4 { // use 0..<4 instead of 0...3 for clarity - id.append(alphabet[Int.random(in: 0..= 0) - + + public func stopDeviceDiscovery() { + discoveryRefCount -= 1 + assert(discoveryRefCount >= 0) + if discoveryRefCount == 0 { for browser in browsers { browser.cancel() } - + browsers.removeAll() - } - } - - public func addShareExtensionDelegate(_ delegate:ShareExtensionDelegate){ - shareExtensionDelegates.append(delegate) - for service in foundServices.values{ - guard let device=service.device else {continue} - delegate.addDevice(device: device) - } - } - - public func removeShareExtensionDelegate(_ delegate:ShareExtensionDelegate){ - shareExtensionDelegates.removeAll(where: {$0===delegate}) - } - - public func cancelOutgoingTransfer(id:String){ - guard let transfer=outgoingTransfers[id] else {return} - transfer.connection.cancel() - } - - private func endpointID(for service:NWBrowser.Result)->String?{ - guard case let NWEndpoint.service(name: serviceName, type: _, domain: _, interface: _)=service.endpoint else {return nil} - guard let nameData=Data.dataFromUrlSafeBase64(serviceName) else {return nil} - guard nameData.count>=10 else {return nil} - let pcp=nameData[0] - guard pcp==0x23 else {return nil} - let endpointID=String(data: nameData.subdata(in: 1..<5), encoding: .ascii)! - let serviceIDHash=nameData.subdata(in: 5..<8) - guard serviceIDHash==Data([0xFC, 0x9F, 0x5E]) else {return nil} - return endpointID - } - - private func addFoundDevice(service:NWBrowser.Result){ + } + } + + public func addShareExtensionDelegate(_ delegate: ShareExtensionDelegate) { + shareExtensionDelegates.append(delegate) + for service in foundServices.values { + guard let device = service.device else { continue } + delegate.addDevice(device: device) + } + } + + public func removeShareExtensionDelegate(_ delegate: ShareExtensionDelegate) { + shareExtensionDelegates.removeAll(where: { $0 === delegate }) + } + + public func cancelOutgoingTransfer(id: String) { + guard let transfer = outgoingTransfers[id] else { return } + transfer.connection.cancel() + } + + private func endpointID(for service: NWBrowser.Result) -> String? { + guard case let NWEndpoint.service(name: serviceName, type: _, domain: _, interface: _) = service.endpoint else { return nil } + guard let nameData = Data.dataFromUrlSafeBase64(serviceName) else { return nil } + guard nameData.count >= 10 else { return nil } + let pcp = nameData[0] + guard pcp == 0x23 else { return nil } + let endpointID = String(data: nameData.subdata(in: 1 ..< 5), encoding: .ascii)! + let serviceIDHash = nameData.subdata(in: 5 ..< 8) + guard serviceIDHash == Data([0xFC, 0x9F, 0x5E]) else { return nil } + return endpointID + } + + private func addFoundDevice(service: NWBrowser.Result) { log("found service \(service)") - for interface in service.interfaces{ - if case .loopback=interface.type{ + for interface in service.interfaces { + if case .loopback = interface.type { log("ignoring localhost service") - return - } - } - guard let endpointID=endpointID(for: service) else {return} + return + } + } + guard let endpointID = endpointID(for: service) else { return } log("service name is valid, endpoint ID \(endpointID)") - var foundService=FoundServiceInfo(service: service) - - guard case let NWBrowser.Result.Metadata.bonjour(txtRecord)=service.metadata else {return} - guard let endpointInfoEncoded=txtRecord.dictionary["n"] else {return} - guard let endpointInfo=Data.dataFromUrlSafeBase64(endpointInfoEncoded) else {return} - guard endpointInfo.count>=19 else {return} - let deviceType=RemoteDeviceInfo.DeviceType.fromRawValue(value: (Int(endpointInfo[0]) >> 1) & 7) - let deviceNameLength=Int(endpointInfo[17]) - guard endpointInfo.count>=deviceNameLength+17 else {return} - guard let deviceName=String(data: endpointInfo.subdata(in: 18..<(18+deviceNameLength)), encoding: .utf8) else {return} - - let deviceInfo=RemoteDeviceInfo(name: deviceName, type: deviceType, id: endpointID) - foundService.device=deviceInfo - foundServices[endpointID]=foundService - for delegate in shareExtensionDelegates{ - delegate.addDevice(device: deviceInfo) - } - } - - private func removeFoundDevice(service:NWBrowser.Result){ - guard let endpointID=endpointID(for: service) else {return} - guard let _=foundServices.removeValue(forKey: endpointID) else {return} - for delegate in shareExtensionDelegates { - delegate.removeDevice(id: endpointID) - } - } - - public func startOutgoingTransfer(deviceID:String, delegate:ShareExtensionDelegate, urls:[URL]){ - guard let info=foundServices[deviceID] else {return} - let tcp=NWProtocolTCP.Options.init() - tcp.noDelay=true - let nwconn=NWConnection(to: info.service.endpoint, using: NWParameters(tls: .none, tcp: tcp)) - let conn=OutboundNearbyConnection(connection: nwconn, id: deviceID, urlsToSend: urls) - conn.delegate=self - let transfer=OutgoingTransferInfo(service: info.service, device: info.device!, connection: conn, delegate: delegate) - outgoingTransfers[deviceID]=transfer - conn.start() - } - - func outboundConnectionWasEstablished(connection: OutboundNearbyConnection) { - guard let transfer=outgoingTransfers[connection.id] else {return} - DispatchQueue.main.async { - transfer.delegate.connectionWasEstablished(pinCode: connection.pinCode!) - } - } - - func outboundConnectionTransferAccepted(connection: OutboundNearbyConnection) { - guard let transfer=outgoingTransfers[connection.id] else {return} - DispatchQueue.main.async { - transfer.delegate.transferAccepted() - } - } - - func outboundConnection(connection: OutboundNearbyConnection, transferProgress: Double) { - guard let transfer=outgoingTransfers[connection.id] else {return} - DispatchQueue.main.async { - transfer.delegate.transferProgress(progress: transferProgress) - } - } - - func outboundConnection(connection: OutboundNearbyConnection, failedWithError: Error) { - guard let transfer=outgoingTransfers[connection.id] else {return} - DispatchQueue.main.async { - transfer.delegate.connectionFailed(with: failedWithError) - } - outgoingTransfers.removeValue(forKey: connection.id) - } - - func outboundConnectionTransferFinished(connection: OutboundNearbyConnection) { - guard let transfer=outgoingTransfers[connection.id] else {return} - DispatchQueue.main.async { - transfer.delegate.transferFinished() - } - outgoingTransfers.removeValue(forKey: connection.id) - } - + var foundService = FoundServiceInfo(service: service) + + guard case let NWBrowser.Result.Metadata.bonjour(txtRecord) = service.metadata else { return } + guard let endpointInfoEncoded = txtRecord.dictionary["n"] else { return } + guard let endpointInfo = Data.dataFromUrlSafeBase64(endpointInfoEncoded) else { return } + guard endpointInfo.count >= 19 else { return } + let deviceType = RemoteDeviceInfo.DeviceType.fromRawValue(value: (Int(endpointInfo[0]) >> 1) & 7) + let deviceNameLength = Int(endpointInfo[17]) + guard endpointInfo.count >= deviceNameLength + 17 else { return } + guard let deviceName = String(data: endpointInfo.subdata(in: 18 ..< (18 + deviceNameLength)), encoding: .utf8) else { return } + + let deviceInfo = RemoteDeviceInfo(name: deviceName, type: deviceType, id: endpointID) + foundService.device = deviceInfo + foundServices[endpointID] = foundService + for delegate in shareExtensionDelegates { + delegate.addDevice(device: deviceInfo) + } + } + + private func removeFoundDevice(service: NWBrowser.Result) { + guard let endpointID = endpointID(for: service) else { return } + guard let _ = foundServices.removeValue(forKey: endpointID) else { return } + for delegate in shareExtensionDelegates { + delegate.removeDevice(id: endpointID) + } + } + + public func startOutgoingTransfer(deviceID: String, delegate: ShareExtensionDelegate, urls: [URL]) { + guard let info = foundServices[deviceID] else { return } + let tcp = NWProtocolTCP.Options() + tcp.noDelay = true + let nwconn = NWConnection(to: info.service.endpoint, using: NWParameters(tls: .none, tcp: tcp)) + let conn = OutboundNearbyConnection(connection: nwconn, id: deviceID, urlsToSend: urls) + conn.delegate = self + let transfer = OutgoingTransferInfo(service: info.service, device: info.device!, connection: conn, delegate: delegate) + outgoingTransfers[deviceID] = transfer + conn.start() + } + + func outboundConnectionWasEstablished(connection: OutboundNearbyConnection) { + guard let transfer = outgoingTransfers[connection.id] else { return } + DispatchQueue.main.async { + transfer.delegate.connectionWasEstablished(pinCode: connection.pinCode!) + } + } + + func outboundConnectionTransferAccepted(connection: OutboundNearbyConnection) { + guard let transfer = outgoingTransfers[connection.id] else { return } + DispatchQueue.main.async { + transfer.delegate.transferAccepted() + } + } + + func outboundConnection(connection: OutboundNearbyConnection, transferProgress: Double) { + guard let transfer = outgoingTransfers[connection.id] else { return } + DispatchQueue.main.async { + transfer.delegate.transferProgress(progress: transferProgress) + } + } + + func outboundConnection(connection: OutboundNearbyConnection, failedWithError: Error) { + guard let transfer = outgoingTransfers[connection.id] else { return } + DispatchQueue.main.async { + transfer.delegate.connectionFailed(with: failedWithError) + } + outgoingTransfers.removeValue(forKey: connection.id) + } + + func outboundConnectionTransferFinished(connection: OutboundNearbyConnection) { + guard let transfer = outgoingTransfers[connection.id] else { return } + DispatchQueue.main.async { + transfer.delegate.transferFinished() + } + outgoingTransfers.removeValue(forKey: connection.id) + } + public func getActiveConnectionsCount() -> Int { return activeConnections.count } } - diff --git a/NearbyShare/OutboundNearbyConnection.swift b/NearbyShare/OutboundNearbyConnection.swift index aee18ac..4914139 100644 --- a/NearbyShare/OutboundNearbyConnection.swift +++ b/NearbyShare/OutboundNearbyConnection.swift @@ -5,433 +5,433 @@ // Created by Grishka on 23.09.2023. // +import CommonCrypto +import CryptoKit import Foundation import Network -import CryptoKit -import CommonCrypto import System import UniformTypeIdentifiers -import SwiftECC import BigInt +import SwiftECC -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 - private var textPayloadID:Int64=0 - - 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) - if urlsToSend.count==1 && !urlsToSend[0].isFileURL{ - textPayloadID=Int64.random(in: Int64.min...Int64.max) - } - } - - 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{ - log("received \(frameData), state is \(currentState)") - switch currentState { - case .initial: - protocolError() - case .sentUkeyClientInit: - try processUkey2ServerInit(frame: try Securegcm_Ukey2Message(serializedBytes: frameData), raw: frameData) - case .sentUkeyClientFinish: - try processConnectionResponse(frame: try Location_Nearby_Connections_OfflineFrame(serializedBytes: frameData)) - default: - let smsg=try Securemessage_SecureMessage(serializedBytes: 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 { - log("Transfer canceled") - try sendDisconnectionAndDisconnect() - delegate?.outboundConnection(connection: self, failedWithError: NearbyError.canceled(reason: .userCanceled)) - return - } +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 + private var textPayloadID: Int64 = 0 + + 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) + if urlsToSend.count == 1 && !urlsToSend[0].isFileURL { + textPayloadID = Int64.random(in: Int64.min ... Int64.max) + } + } + + 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 { + log("received \(frameData), state is \(currentState)") + switch currentState { + case .initial: + protocolError() + case .sentUkeyClientInit: + try processUkey2ServerInit(frame: Securegcm_Ukey2Message(serializedBytes: frameData), raw: frameData) + case .sentUkeyClientFinish: + try processConnectionResponse(frame: Location_Nearby_Connections_OfflineFrame(serializedBytes: frameData)) + default: + let smsg = try Securemessage_SecureMessage(serializedBytes: 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 { + log("Transfer canceled") + try sendDisconnectionAndDisconnect() + delegate?.outboundConnection(connection: self, failedWithError: NearbyError.canceled(reason: .userCanceled)) + return + } log(frame.debugDescription) - 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(serializedBytes: 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(serializedBytes: 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{ - log("connection response: \(frame)") - 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("sharingNearbyFrame.v1.pairedKeyEncryption") } - 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("sharingNearbyFrame.v1.pairedKeyResult") } - - var introduction=Sharing_Nearby_Frame() - introduction.version = .v1 - introduction.v1.type = .introduction - if urlsToSend.count==1 && !urlsToSend[0].isFileURL{ - var meta=Sharing_Nearby_TextMetadata() - meta.type = .url - meta.textTitle=urlsToSend[0].host ?? "URL" - meta.size=Int64(urlsToSend[0].absoluteString.utf8.count) - meta.payloadID=textPayloadID - introduction.v1.introduction.textMetadata.append(meta) - }else{ - for url in urlsToSend{ - guard url.isFileURL 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{ - if #available(macOS 11.0, *){ - let type=UTType(typeID) - if let type=type, let mimeType=type.preferredMIMEType{ - meta.mimeType=mimeType - } - }else{ - if let mimeType=UTTypeCopyPreferredTagWithClass(typeID as CFString, kUTTagClassMIMEType){ - meta.mimeType=(mimeType.takeRetainedValue() as NSString) as String - } - } - } - 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 - } - } - log("sent introduction: \(introduction)") - 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("sharingNearbyFrame.v1.type==response")} - switch frame.v1.connectionResponse.status{ - case .accept: - currentState = .sendingFiles - delegate?.outboundConnectionTransferAccepted(connection: self) - if urlsToSend.count==1 && !urlsToSend[0].isFileURL{ - try sendURL() - }else{ - 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 sendURL() throws{ - try sendBytesPayload(data: Data(urlsToSend[0].absoluteString.utf8), id: textPayloadID) - delegate?.outboundConnectionTransferFinished(connection: self) - try sendDisconnectionAndDisconnect() - } - - private func sendNextFileChunk() throws{ - if cancelled{ - return - } - if currentTransfer==nil || currentTransfer?.currentOffset==currentTransfer?.totalBytes{ - if currentTransfer != nil && currentTransfer?.handle != nil{ - try currentTransfer?.handle?.close() - } - if queue.isEmpty{ - log("Disconnecting because all files have been transferred") - try sendDisconnectionAndDisconnect() - delegate?.outboundConnectionTransferFinished(connection: self) - return - } - currentTransfer=queue.removeFirst() - } - - let fileBuffer:Data - if #available(macOS 10.15.4, *) { - guard let _fileBuffer=try currentTransfer!.handle!.read(upToCount: 512*1024) else{ - throw NearbyError.inputOutput - } - fileBuffer=_fileBuffer - } else { - fileBuffer=currentTransfer!.handle!.readData(ofLength: 512*1024) - } - - 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() - } - }) - log("sent file chunk, current transfer: \(String(describing: currentTransfer))") - 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) - log("sent EOF, current transfer: \(String(describing: currentTransfer))") - } - } - - private static func sanitizeFileName(name:String)->String{ - return name.replacingOccurrences(of: "[\\/\\\\?%\\*:\\|\"<>=]", with: "_", options: .regularExpression) - } + 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] + try sendFrameAsync(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(serializedBytes: 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(serializedBytes: 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 + try sendFrameAsync(resp.serializedData()) + + encryptionDone = true + delegate?.outboundConnectionWasEstablished(connection: self) + } + + private func processConnectionResponse(frame: Location_Nearby_Connections_OfflineFrame) throws { + log("connection response: \(frame)") + 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("sharingNearbyFrame.v1.pairedKeyEncryption") } + 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("sharingNearbyFrame.v1.pairedKeyResult") } + + var introduction = Sharing_Nearby_Frame() + introduction.version = .v1 + introduction.v1.type = .introduction + if urlsToSend.count == 1 && !urlsToSend[0].isFileURL { + var meta = Sharing_Nearby_TextMetadata() + meta.type = .url + meta.textTitle = urlsToSend[0].host ?? "URL" + meta.size = Int64(urlsToSend[0].absoluteString.utf8.count) + meta.payloadID = textPayloadID + introduction.v1.introduction.textMetadata.append(meta) + } else { + for url in urlsToSend { + guard url.isFileURL 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 { + if #available(macOS 11.0, *) { + let type = UTType(typeID) + if let type = type, let mimeType = type.preferredMIMEType { + meta.mimeType = mimeType + } + } else { + if let mimeType = UTTypeCopyPreferredTagWithClass(typeID as CFString, kUTTagClassMIMEType) { + meta.mimeType = (mimeType.takeRetainedValue() as NSString) as String + } + } + } + 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) + try queue.append(OutgoingFileTransfer(url: url, payloadID: meta.payloadID, handle: FileHandle(forReadingFrom: url), totalBytes: meta.size, currentOffset: 0)) + introduction.v1.introduction.fileMetadata.append(meta) + totalBytesToSend += meta.size + } + } + log("sent introduction: \(introduction)") + 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("sharingNearbyFrame.v1.type==response") } + switch frame.v1.connectionResponse.status { + case .accept: + currentState = .sendingFiles + delegate?.outboundConnectionTransferAccepted(connection: self) + if urlsToSend.count == 1 && !urlsToSend[0].isFileURL { + try sendURL() + } else { + 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 sendURL() throws { + try sendBytesPayload(data: Data(urlsToSend[0].absoluteString.utf8), id: textPayloadID) + delegate?.outboundConnectionTransferFinished(connection: self) + try sendDisconnectionAndDisconnect() + } + + private func sendNextFileChunk() throws { + if cancelled { + return + } + if currentTransfer == nil || currentTransfer?.currentOffset == currentTransfer?.totalBytes { + if currentTransfer != nil && currentTransfer?.handle != nil { + try currentTransfer?.handle?.close() + } + if queue.isEmpty { + log("Disconnecting because all files have been transferred") + try sendDisconnectionAndDisconnect() + delegate?.outboundConnectionTransferFinished(connection: self) + return + } + currentTransfer = queue.removeFirst() + } + + let fileBuffer: Data + if #available(macOS 10.15.4, *) { + guard let _fileBuffer = try currentTransfer!.handle!.read(upToCount: 512 * 1024) else { + throw NearbyError.inputOutput + } + fileBuffer = _fileBuffer + } else { + fileBuffer = currentTransfer!.handle!.readData(ofLength: 512 * 1024) + } + + 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() + } + }) + log("sent file chunk, current transfer: \(String(describing: currentTransfer))") + 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) + log("sent EOF, current transfer: \(String(describing: currentTransfer))") + } + } + + 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 +private 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) +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) }