mirror of
https://github.com/grishka/NearDrop.git
synced 2026-04-03 01:36:15 +02:00
354 lines
13 KiB
Swift
354 lines
13 KiB
Swift
//
|
|
// InboundNearbyConnection.swift
|
|
// NearDrop
|
|
//
|
|
// Created by Grishka on 08.04.2023.
|
|
//
|
|
|
|
import Foundation
|
|
import Network
|
|
import CryptoKit
|
|
import CommonCrypto
|
|
import System
|
|
|
|
import SwiftECC
|
|
import BigInt
|
|
|
|
class InboundNearbyConnection: NearbyConnection{
|
|
|
|
private var currentState:State = .initial
|
|
public var delegate:InboundNearbyConnectionDelegate?
|
|
private var cipherCommitment:Data?
|
|
|
|
enum State{
|
|
case initial, receivedConnectionRequest, sentUkeyServerInit, receivedUkeyClientFinish, sentConnectionResponse, sentPairedKeyResult, receivedPairedKeyResult, waitingForUserConsent, receivingFiles, disconnected
|
|
}
|
|
|
|
override init(connection: NWConnection, id:String) {
|
|
super.init(connection: connection, id: id)
|
|
}
|
|
|
|
override func handleConnectionClosure() {
|
|
super.handleConnectionClosure()
|
|
currentState = .disconnected
|
|
do{
|
|
try deletePartiallyReceivedFiles()
|
|
}catch{
|
|
print("Error deleting partially received files: \(error)")
|
|
}
|
|
DispatchQueue.main.async {
|
|
self.delegate?.connectionWasTerminated(connection: self, error: self.lastError)
|
|
}
|
|
}
|
|
|
|
override internal func processReceivedFrame(frameData:Data){
|
|
do{
|
|
switch currentState {
|
|
case .initial:
|
|
let frame=try Location_Nearby_Connections_OfflineFrame(serializedData: frameData)
|
|
try processConnectionRequestFrame(frame)
|
|
case .receivedConnectionRequest:
|
|
let msg=try Securegcm_Ukey2Message(serializedData: frameData)
|
|
ukeyClientInitMsgData=frameData
|
|
try processUkey2ClientInit(msg)
|
|
case .sentUkeyServerInit:
|
|
let msg=try Securegcm_Ukey2Message(serializedData: frameData)
|
|
try processUkey2ClientFinish(msg, raw: frameData)
|
|
case .receivedUkeyClientFinish:
|
|
let frame=try Location_Nearby_Connections_OfflineFrame(serializedData: frameData)
|
|
try processConnectionResponseFrame(frame)
|
|
default:
|
|
let smsg=try Securemessage_SecureMessage(serializedData: frameData)
|
|
try decryptAndProcessReceivedSecureMessage(smsg)
|
|
}
|
|
}catch{
|
|
lastError=error
|
|
print("Deserialization error: \(error) in state \(currentState)")
|
|
#if !DEBUG
|
|
protocolError()
|
|
#endif
|
|
}
|
|
}
|
|
|
|
override internal func processTransferSetupFrame(_ frame:Sharing_Nearby_Frame) throws{
|
|
if frame.hasV1 && frame.v1.hasType, case .cancel = frame.v1.type {
|
|
print("Transfer canceled")
|
|
try sendDisconnectionAndDisconnect()
|
|
return
|
|
}
|
|
switch currentState{
|
|
case .sentConnectionResponse:
|
|
try processPairedKeyEncryptionFrame(frame)
|
|
case .sentPairedKeyResult:
|
|
try processPairedKeyResultFrame(frame)
|
|
case .receivedPairedKeyResult:
|
|
try processIntroductionFrame(frame)
|
|
default:
|
|
print("Unexpected connection state in processTransferSetupFrame: \(currentState)")
|
|
print(frame)
|
|
}
|
|
}
|
|
|
|
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{
|
|
try fileInfo.fileHandle?.write(contentsOf: frame.payloadChunk.body)
|
|
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
|
|
fileInfo.progress?.unpublish()
|
|
transferredFiles.removeValue(forKey: id)
|
|
if transferredFiles.isEmpty{
|
|
try sendDisconnectionAndDisconnect()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func processConnectionRequestFrame(_ frame:Location_Nearby_Connections_OfflineFrame) throws{
|
|
guard frame.hasV1 && frame.v1.hasConnectionRequest && frame.v1.connectionRequest.hasEndpointInfo else { throw NearbyError.requiredFieldMissing }
|
|
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))
|
|
currentState = .receivedConnectionRequest
|
|
}
|
|
|
|
private func processUkey2ClientInit(_ msg:Securegcm_Ukey2Message) throws{
|
|
guard msg.hasMessageType, msg.hasMessageData else { throw NearbyError.requiredFieldMissing }
|
|
guard case .clientInit = msg.messageType else{
|
|
sendUkey2Alert(type: .badMessageType)
|
|
throw NearbyError.ukey2
|
|
}
|
|
let clientInit:Securegcm_Ukey2ClientInit
|
|
do{
|
|
clientInit=try Securegcm_Ukey2ClientInit(serializedData: msg.messageData)
|
|
}catch{
|
|
sendUkey2Alert(type: .badMessageData)
|
|
throw NearbyError.ukey2
|
|
}
|
|
guard clientInit.version==1 else{
|
|
sendUkey2Alert(type: .badVersion)
|
|
throw NearbyError.ukey2
|
|
}
|
|
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
|
|
break
|
|
}
|
|
}
|
|
guard found else{
|
|
sendUkey2Alert(type: .badHandshakeCipher)
|
|
throw NearbyError.ukey2
|
|
}
|
|
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)
|
|
serverInit.handshakeCipher = .p256Sha512
|
|
|
|
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()
|
|
serverInitMsg.messageType = .serverInit
|
|
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{
|
|
guard msg.hasMessageType, msg.hasMessageData else { throw NearbyError.requiredFieldMissing }
|
|
guard case .clientFinish = msg.messageType else { throw NearbyError.ukey2 }
|
|
|
|
var sha=SHA512()
|
|
sha.update(data: raw)
|
|
guard cipherCommitment==Data(sha.finalize()) else { throw NearbyError.ukey2 }
|
|
|
|
let clientFinish=try Securegcm_Ukey2ClientFinished(serializedData: msg.messageData)
|
|
guard clientFinish.hasPublicKey else {throw NearbyError.requiredFieldMissing }
|
|
let clientKey=try Securemessage_GenericPublicKey(serializedData: clientFinish.publicKey)
|
|
|
|
try finalizeKeyExchange(peerKey: clientKey)
|
|
|
|
currentState = .receivedUkeyClientFinish
|
|
}
|
|
|
|
private func processConnectionResponseFrame(_ frame:Location_Nearby_Connections_OfflineFrame) throws{
|
|
guard frame.hasV1, frame.v1.hasType else { throw NearbyError.requiredFieldMissing }
|
|
if case .connectionResponse = frame.v1.type {
|
|
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
|
|
|
|
var pairedEncryption=Sharing_Nearby_Frame()
|
|
pairedEncryption.version = .v1
|
|
pairedEncryption.v1=Sharing_Nearby_V1Frame()
|
|
pairedEncryption.v1.type = .pairedKeyEncryption
|
|
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)
|
|
try sendTransferSetupFrame(pairedEncryption)
|
|
currentState = .sentConnectionResponse
|
|
} else {
|
|
print("Unhandled offline frame plaintext: \(frame)")
|
|
}
|
|
}
|
|
|
|
private func processPairedKeyEncryptionFrame(_ frame:Sharing_Nearby_Frame) throws{
|
|
guard frame.hasV1, frame.v1.hasPairedKeyEncryption else { throw NearbyError.requiredFieldMissing }
|
|
var pairedResult=Sharing_Nearby_Frame()
|
|
pairedResult.version = .v1
|
|
pairedResult.v1=Sharing_Nearby_V1Frame()
|
|
pairedResult.v1.type = .pairedKeyResult
|
|
pairedResult.v1.pairedKeyResult=Sharing_Nearby_PairedKeyResultFrame()
|
|
pairedResult.v1.pairedKeyResult.status = .unable
|
|
try sendTransferSetupFrame(pairedResult)
|
|
currentState = .sentPairedKeyResult
|
|
}
|
|
|
|
private func processPairedKeyResultFrame(_ frame:Sharing_Nearby_Frame) throws{
|
|
guard frame.hasV1, frame.v1.hasPairedKeyResult else { throw NearbyError.requiredFieldMissing }
|
|
currentState = .receivedPairedKeyResult
|
|
}
|
|
|
|
private func processIntroductionFrame(_ frame:Sharing_Nearby_Frame) throws{
|
|
guard frame.hasV1, frame.v1.hasIntroduction else { throw NearbyError.requiredFieldMissing }
|
|
currentState = .waitingForUserConsent
|
|
let downloadsDirectory=(try FileManager.default.url(for: .downloadsDirectory, in: .userDomainMask, appropriateFor: nil, create: true)).resolvingSymlinksInPath()
|
|
for file in frame.v1.introduction.fileMetadata{
|
|
var dest=downloadsDirectory.appendingPathComponent(file.name)
|
|
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)
|
|
}
|
|
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)
|
|
DispatchQueue.main.async {
|
|
self.delegate?.obtainUserConsent(for: metadata, from: self.remoteDeviceInfo!, connection: self)
|
|
}
|
|
}
|
|
|
|
func submitUserConsent(accepted:Bool){
|
|
DispatchQueue.global(qos: .utility).async {
|
|
if accepted{
|
|
self.acceptTransfer()
|
|
}else{
|
|
self.rejectTransfer()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func acceptTransfer(){
|
|
do{
|
|
for (id, file) in transferredFiles{
|
|
FileManager.default.createFile(atPath: file.destinationURL.path, contents: nil)
|
|
let handle=try FileHandle(forWritingTo: file.destinationURL)
|
|
transferredFiles[id]!.fileHandle=handle
|
|
let progress=Progress()
|
|
progress.fileURL=file.destinationURL
|
|
progress.totalUnitCount=file.meta.size
|
|
progress.kind = .file
|
|
progress.isPausable=false
|
|
progress.publish()
|
|
transferredFiles[id]!.progress=progress
|
|
transferredFiles[id]!.created=true
|
|
}
|
|
|
|
var frame=Sharing_Nearby_Frame()
|
|
frame.version = .v1
|
|
frame.v1.type = .response
|
|
frame.v1.connectionResponse.status = .accept
|
|
currentState = .receivingFiles
|
|
try sendTransferSetupFrame(frame)
|
|
}catch{
|
|
print("Error \(error)")
|
|
protocolError()
|
|
}
|
|
}
|
|
|
|
private func rejectTransfer(){
|
|
var frame=Sharing_Nearby_Frame()
|
|
frame.version = .v1
|
|
frame.v1.type = .response
|
|
frame.v1.connectionResponse.status = .reject
|
|
do{
|
|
try sendTransferSetupFrame(frame)
|
|
try sendDisconnectionAndDisconnect()
|
|
}catch{
|
|
print("Error \(error)")
|
|
protocolError()
|
|
}
|
|
}
|
|
|
|
private func deletePartiallyReceivedFiles() throws{
|
|
for (_, file) in transferredFiles{
|
|
guard file.created else { continue }
|
|
try FileManager.default.removeItem(at: file.destinationURL)
|
|
}
|
|
}
|
|
}
|
|
|
|
protocol InboundNearbyConnectionDelegate{
|
|
func obtainUserConsent(for transfer:TransferMetadata, from device:RemoteDeviceInfo, connection:InboundNearbyConnection)
|
|
func connectionWasTerminated(connection:InboundNearbyConnection, error:Error?)
|
|
}
|