diff --git a/NearbyShare/Connection/DataModel.swift b/NearbyShare/Model/DataModel.swift similarity index 100% rename from NearbyShare/Connection/DataModel.swift rename to NearbyShare/Model/DataModel.swift diff --git a/NearbyShare/Protobuf/device_to_device_messages.pb.swift b/NearbyShare/Model/Protobuf/device_to_device_messages.pb.swift similarity index 100% rename from NearbyShare/Protobuf/device_to_device_messages.pb.swift rename to NearbyShare/Model/Protobuf/device_to_device_messages.pb.swift diff --git a/NearbyShare/Protobuf/offline_wire_formats.pb.swift b/NearbyShare/Model/Protobuf/offline_wire_formats.pb.swift similarity index 100% rename from NearbyShare/Protobuf/offline_wire_formats.pb.swift rename to NearbyShare/Model/Protobuf/offline_wire_formats.pb.swift diff --git a/NearbyShare/Protobuf/securegcm.pb.swift b/NearbyShare/Model/Protobuf/securegcm.pb.swift similarity index 100% rename from NearbyShare/Protobuf/securegcm.pb.swift rename to NearbyShare/Model/Protobuf/securegcm.pb.swift diff --git a/NearbyShare/Protobuf/securemessage.pb.swift b/NearbyShare/Model/Protobuf/securemessage.pb.swift similarity index 100% rename from NearbyShare/Protobuf/securemessage.pb.swift rename to NearbyShare/Model/Protobuf/securemessage.pb.swift diff --git a/NearbyShare/Protobuf/ukey.pb.swift b/NearbyShare/Model/Protobuf/ukey.pb.swift similarity index 100% rename from NearbyShare/Protobuf/ukey.pb.swift rename to NearbyShare/Model/Protobuf/ukey.pb.swift diff --git a/NearbyShare/Protobuf/wire_format.pb.swift b/NearbyShare/Model/Protobuf/wire_format.pb.swift similarity index 100% rename from NearbyShare/Protobuf/wire_format.pb.swift rename to NearbyShare/Model/Protobuf/wire_format.pb.swift diff --git a/NearbyShare/ProtobufSource/device_to_device_messages.proto b/NearbyShare/Model/ProtobufSource/device_to_device_messages.proto similarity index 100% rename from NearbyShare/ProtobufSource/device_to_device_messages.proto rename to NearbyShare/Model/ProtobufSource/device_to_device_messages.proto diff --git a/NearbyShare/ProtobufSource/offline_wire_formats.proto b/NearbyShare/Model/ProtobufSource/offline_wire_formats.proto similarity index 100% rename from NearbyShare/ProtobufSource/offline_wire_formats.proto rename to NearbyShare/Model/ProtobufSource/offline_wire_formats.proto diff --git a/NearbyShare/ProtobufSource/securegcm.proto b/NearbyShare/Model/ProtobufSource/securegcm.proto similarity index 100% rename from NearbyShare/ProtobufSource/securegcm.proto rename to NearbyShare/Model/ProtobufSource/securegcm.proto diff --git a/NearbyShare/ProtobufSource/securemessage.proto b/NearbyShare/Model/ProtobufSource/securemessage.proto similarity index 100% rename from NearbyShare/ProtobufSource/securemessage.proto rename to NearbyShare/Model/ProtobufSource/securemessage.proto diff --git a/NearbyShare/ProtobufSource/ukey.proto b/NearbyShare/Model/ProtobufSource/ukey.proto similarity index 100% rename from NearbyShare/ProtobufSource/ukey.proto rename to NearbyShare/Model/ProtobufSource/ukey.proto diff --git a/NearbyShare/ProtobufSource/wire_format.proto b/NearbyShare/Model/ProtobufSource/wire_format.proto similarity index 100% rename from NearbyShare/ProtobufSource/wire_format.proto rename to NearbyShare/Model/ProtobufSource/wire_format.proto diff --git a/NearbyShareFramework/Connection/DataModel.swift b/NearbyShareFramework/Connection/DataModel.swift new file mode 100644 index 0000000..1d0927f --- /dev/null +++ b/NearbyShareFramework/Connection/DataModel.swift @@ -0,0 +1,188 @@ +// +// DataModel.swift +// QuickDrop +// +// Created by Leon Böttger on 26.07.25. +// + +import Foundation +import Network + + +protocol InboundNearbyConnectionDelegate { + func obtainUserConsent(for transfer: TransferMetadata, from device: RemoteDeviceInfo, connection: InboundNearbyConnection) + func connectionWasTerminated(connection: InboundNearbyConnection, error: Error?) +} + + +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) +} + + +public protocol MainAppDelegate { + func obtainUserConsent(for transfer: TransferMetadata, from device: RemoteDeviceInfo) + func incomingTransfer(id: String, didFinishWith error: Error?) + func showFirewallAlert() + func showCopiedToClipboardAlert() + func showUnsupportedFileAlert(for: RemoteDeviceInfo?) +} + + +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 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 struct TransferMetadata { + public let files: [FileMetadata] + public let id: String + public let pinCode: String? + public let textDescription: String? + public let type: TransferType + + init(files: [FileMetadata], id: String, pinCode: String?, textDescription: String? = nil, transferType: TransferType = .file) { + self.files = files + self.id = id + self.pinCode = pinCode + self.textDescription = textDescription + self.type = transferType + } + + public enum TransferType { + case file + case text + case url + } +} + + +public struct FileMetadata { + public let name: String + public let size: Int64 + public let mimeType: String +} + + +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 enum NearbyError: Error { + case protocolError(_ message: String) + case requiredFieldMissing(_ message: String) + case ukey2 + case packetFilterError + case inputOutput + case canceled(reason: CancellationReason) + + public enum CancellationReason { + case userRejected, userCanceled, notEnoughSpace, unsupportedType, timedOut + + public func localizedDescription() -> String { + switch self { + case .userRejected: + "TransferDeclined".localized() + case .userCanceled: + "TransferCanceled".localized() + case .notEnoughSpace: + "NotEnoughSpace".localized() + case .unsupportedType: + "UnsupportedType".localized() + case .timedOut: + "TransferTimedOut".localized() + } + } + } +} diff --git a/NearbyShareFramework/Connection/InboundNearbyConnection.swift b/NearbyShareFramework/Connection/InboundNearbyConnection.swift new file mode 100644 index 0000000..dd3c832 --- /dev/null +++ b/NearbyShareFramework/Connection/InboundNearbyConnection.swift @@ -0,0 +1,534 @@ +// +// InboundNearbyConnection.swift +// QuickDrop +// +// Created by Grishka on 08.04.2023. +// + +import AppKit +import CommonCrypto +import CryptoKit +import Foundation +import Network +import System +import BigInt +import SwiftECC + +class InboundNearbyConnection: NearbyConnection { + + private var filesToBeReceived: [Int64: InternalFileInfo] = [:] + private var currentState: State = .initial + private var cipherCommitment: Data? + private var textPayloadID: Int64 = 0 + private var isPlainTextTransfer = false + + public var wasRejected = false + public var delegate: InboundNearbyConnectionDelegate? + + 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 disconnect() { + super.disconnect() + currentState = .disconnected + deletePartiallyReceivedFiles() + + DispatchQueue.main.async { + self.delegate?.connectionWasTerminated(connection: self, error: self.lastError) + + SaveFilesManager.shared.movePendingFilesToTarget() + SaveFilesManager.shared.stopAccessingSecurityScopedResource() + } + } + + + override func processReceivedFrame(frameData: Data) { + + if currentState != .receivingFiles { + log("[InboundNearbyConnection] Received frame in state \(currentState)...") + } + + do { + switch currentState { + case .initial: + let frame = try Location_Nearby_Connections_OfflineFrame(serializedBytes: frameData) + try processConnectionRequestFrame(frame) + case .receivedConnectionRequest: + let msg = try Securegcm_Ukey2Message(serializedBytes: frameData) + ukeyClientInitMsgData = frameData + try processUkey2ClientInit(msg) + case .sentUkeyServerInit: + let msg = try Securegcm_Ukey2Message(serializedBytes: frameData) + try processUkey2ClientFinish(msg, raw: frameData) + case .receivedUkeyClientFinish: + let frame = try Location_Nearby_Connections_OfflineFrame(serializedBytes: frameData) + try processConnectionResponseFrame(frame) + default: + + var smsg: Securemessage_SecureMessage? = nil + + do { + smsg = try Securemessage_SecureMessage(serializedBytes: frameData) + } catch { + log("[InboundNearbyConnection] Error deserializing secure message (probably due to packet filter)") + lastError = NearbyError.packetFilterError + protocolError() + } + + if let smsg = smsg { + try decryptAndProcessReceivedSecureMessage(smsg) + } + } + } catch { + lastError = error + log("[InboundNearbyConnection] Error receiving frame: \(error) in state \(currentState).") + protocolError() + } + } + + + override func processTransferSetupFrame(_ frame: Sharing_Nearby_Frame) throws { + if frame.hasV1 && frame.v1.hasType, case .cancel = frame.v1.type { + log("[InboundNearbyConnection] Transfer canceled") + try sendDisconnectionAndDisconnect() + return + } + switch currentState { + case .sentConnectionResponse: + try processPairedKeyEncryptionFrame(frame) + case .sentPairedKeyResult: + try processPairedKeyResultFrame(frame) + case .receivedPairedKeyResult: + try processIntroductionFrame(frame) + default: + log("[InboundNearbyConnection] Unexpected connection state in processTransferSetupFrame: \(currentState)") + 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 = filesToBeReceived[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 { + do { + try fileInfo.fileHandle?.write(contentsOf: frame.payloadChunk.body) + filesToBeReceived[id]!.bytesTransferred += Int64(frame.payloadChunk.body.count) + fileInfo.progress?.completedUnitCount = filesToBeReceived[id]!.bytesTransferred + + // only for logging + self.bytesTransferred += Int64(frame.payloadChunk.body.count) + } catch { + log("[InboundNearbyConnection] Error occurred during writing file: \(error.localizedDescription)") + + throw NearbyError.protocolError(error.localizedDescription) + } + } + else if (frame.payloadChunk.flags & 1) == 1 { + try fileInfo.fileHandle?.close() + filesToBeReceived[id]!.fileHandle = nil + fileInfo.progress?.unpublish() + SaveFilesManager.shared.registerFileFinishedDownloading(fileInfo.destinationURL) + + filesToBeReceived.removeValue(forKey: id) + + if filesToBeReceived.isEmpty { + log("[InboundNearbyConnection] All files received, sending disconnection frame and disconnecting.") + try sendDisconnectionAndDisconnect() + } + } + } + + + override func processBytesPayload(payload: Data, id: Int64) throws -> Bool { + if id == textPayloadID { + if let urlStr = String(data: payload, encoding: .utf8) { + + if isPlainTextTransfer { + // paste to clipboard + let pasteboard = NSPasteboard.general + pasteboard.clearContents() + pasteboard.setString(urlStr, forType: .string) + + NearbyConnectionManager.shared.mainAppDelegate?.showCopiedToClipboardAlert() + } + else if let url = URL(string: urlStr) { + NSWorkspace.shared.open(url) + } + } + + log("[InboundNearbyConnection] Received text payload. Disconnecting...") + try sendDisconnectionAndDisconnect() + return true + } + else if let fileInfo = filesToBeReceived[id] { + fileInfo.fileHandle?.write(payload) + filesToBeReceived[id]!.bytesTransferred += Int64(payload.count) + fileInfo.progress?.completedUnitCount = filesToBeReceived[id]!.bytesTransferred + try fileInfo.fileHandle?.close() + filesToBeReceived[id]!.fileHandle = nil + fileInfo.progress?.unpublish() + filesToBeReceived.removeValue(forKey: id) + SaveFilesManager.shared.registerFileFinishedDownloading(fileInfo.destinationURL) + + log("[InboundNearbyConnection] Received file payload. Disconnecting...") + try sendDisconnectionAndDisconnect() + return true + } + return false + } + + + 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(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("clientInit ukey2message.type|data") } + guard case .clientInit = msg.messageType else { + sendUkey2Alert(type: .badMessageType) + log("[InboundNearbyConnection] Unsupported message type: \(msg.messageType)") + throw NearbyError.ukey2 + } + let clientInit: Securegcm_Ukey2ClientInit + do { + clientInit = try Securegcm_Ukey2ClientInit(serializedBytes: msg.messageData) + } catch { + sendUkey2Alert(type: .badMessageData) + log("[InboundNearbyConnection] Failed to parse clientInit: \(error)") + throw NearbyError.ukey2 + } + guard clientInit.version == 1 else { + sendUkey2Alert(type: .badVersion) + log("[InboundNearbyConnection] Unsupported clientInit version: \(clientInit.version)") + throw NearbyError.ukey2 + } + guard clientInit.random.count == 32 else { + sendUkey2Alert(type: .badRandom) + log("[InboundNearbyConnection] Unsupported clientInit random: \(clientInit.random.count)") + 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) + log("[InboundNearbyConnection] Unsupported clientInit handshakeCipher: \(clientInit.cipherCommitments)") + throw NearbyError.ukey2 + } + guard clientInit.nextProtocol == "AES_256_CBC-HMAC_SHA256" else { + sendUkey2Alert(type: .badNextProtocol) + log("[InboundNearbyConnection] Unsupported clientInit nextProtocol: \(clientInit.nextProtocol)") + 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("clientFinish ukey2message.type|data") } + guard case .clientFinish = msg.messageType else { + log("[InboundNearbyConnection] Unexpected message type \(msg.messageType)") + throw NearbyError.ukey2 + } + + var sha = SHA512() + sha.update(data: raw) + guard cipherCommitment == Data(sha.finalize()) else { + log("[InboundNearbyConnection] Invalid cipherCommitment in clientFinish") + 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 { + 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() + 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 + + 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 { + log("[InboundNearbyConnection] Unhandled offline frame plaintext: \(frame)") + } + } + + + 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() + 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("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)" + } + counter += 1 + } while FileManager.default.fileExists(atPath: path) + dest = URL(fileURLWithPath: path) + } + return dest + } + + + 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 { + 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) + filesToBeReceived[file.payloadID] = info + } + let metadata = TransferMetadata(files: filesToBeReceived.map { $0.value.meta }, id: id, pinCode: pinCode) + DispatchQueue.main.async { + self.delegate?.obtainUserConsent(for: metadata, from: self.remoteDeviceInfo!, connection: self) + } + } + else if let textMetadata = frame.v1.introduction.textMetadata.first { + + if textMetadata.type == .url || textMetadata.type == .text { + + let isClipboardText = textMetadata.type == .text + + let metadata = TransferMetadata(files: [], id: id, pinCode: pinCode, textDescription: textMetadata.textTitle, transferType: isClipboardText ? .text : .url) + textPayloadID = textMetadata.payloadID + + if isClipboardText{ + isPlainTextTransfer = true + } + + DispatchQueue.main.async { + self.delegate?.obtainUserConsent(for: metadata, from: self.remoteDeviceInfo!, connection: self) + } + } + else { + rejectDueToUnsupportedFileType(frame) + } + } else { + rejectDueToUnsupportedFileType(frame) + } + } + + + func rejectDueToUnsupportedFileType(_ frame: Sharing_Nearby_Frame) { + + log("[InboundNearbyConnection] Rejecting transfer due to unsupported file type. Frame is \(frame.debugDescription)") + + NearbyConnectionManager.shared.mainAppDelegate?.showUnsupportedFileAlert(for: remoteDeviceInfo) + rejectTransfer(with: .unsupportedAttachmentType) + } + + + func submitUserConsent(accepted: Bool, storeInTemp: Bool = false) { + DispatchQueue.global(qos: .utility).async { + if accepted { + self.acceptTransfer(storeInTemp: storeInTemp) + } else { + self.rejectTransfer() + } + } + } + + + private func acceptTransfer(storeInTemp: Bool) { + if currentState == .disconnected { + log("[InboundNearbyConnection] Detected timeout, not accepting transfer") + return + } + + do { + if storeInTemp { + try FileManager.default.createDirectory(at: SaveFilesManager.shared.tempDirectory, withIntermediateDirectories: true) + } + + for (id, file) in filesToBeReceived { + 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) + filesToBeReceived[id]!.fileHandle = handle + + let progress = Progress() + progress.fileURL = targetURL + progress.totalUnitCount = file.meta.size + progress.kind = .file + progress.isPausable = false + progress.publish() + filesToBeReceived[id]!.progress = progress + filesToBeReceived[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 { + lastError = error + protocolError() + } + } + + + private func rejectTransfer(with reason: Sharing_Nearby_ConnectionResponseFrame.Status = .reject) { + + self.wasRejected = true + + log("[InboundNearbyConnection] Rejecting transfer because of \( reason)") + + var frame = Sharing_Nearby_Frame() + frame.version = .v1 + frame.v1.type = .response + frame.v1.connectionResponse.status = reason + do { + try sendTransferSetupFrame(frame) + try sendDisconnectionAndDisconnect() + } catch { + log("[InboundNearbyConnection] Error \(error)") + protocolError() + } + } + + + private func deletePartiallyReceivedFiles() { + for (_, file) in filesToBeReceived { + + if let progress = file.progress { + progress.unpublish() + } + + guard file.created else { continue } + do { + try FileManager.default.removeItem(at: file.destinationURL) + } + catch { + // if it fails, we don't care. Could be because file was not created yet + } + } + } +} diff --git a/NearbyShareFramework/Connection/NearbyConnection.swift b/NearbyShareFramework/Connection/NearbyConnection.swift new file mode 100644 index 0000000..4a60e38 --- /dev/null +++ b/NearbyShareFramework/Connection/NearbyConnection.swift @@ -0,0 +1,592 @@ +// +// NearbyConnection.swift +// QuickDrop +// +// Created by Grishka on 09.04.2023. +// + +import CommonCrypto +import CryptoKit +import Foundation +import Network +import System + +import BigInt +import SwiftECC + +class NearbyConnection { + + private let maxFrameLength = 5 * 1024 * 1024 + private static let dispatchQueue = DispatchQueue(label: "com.leonboettger.quickdrop.queue", qos: .utility) + + let id: String + let connection: NWConnection + + var remoteDeviceInfo: RemoteDeviceInfo? + var encryptionDone: Bool = false + var lastError: Error? + var bytesTransferred: Int64 = 0 + + private var payloadBuffers: [Int64: NSMutableData] = [:] + private var connectionClosed: Bool = false + + private var inactivityTimer: DispatchSourceTimer? + private let timeoutInterval: TimeInterval = 50 + + // UKEY2-related state + var publicKey: ECPublicKey? + var privateKey: ECPrivateKey? + var ukeyClientInitMsgData: Data? + var ukeyServerInitMsgData: Data? + + // SecureMessage encryption keys + var decryptKey: [UInt8]? + var encryptKey: [UInt8]? + var recvHmacKey: SymmetricKey? + var sendHmacKey: SymmetricKey? + + // SecureMessage sequence numbers + private var serverSeq: Int32 = 0 + private var clientSeq: Int32 = 0 + + private(set) var pinCode: String? + + + init(connection: NWConnection, id: String) { + self.connection = connection + self.id = id + } + + + func start() { + connection.stateUpdateHandler = { state in + + if !self.connectionClosed { + if case .ready = state { + self.connectionReady() + self.receiveFrameAsync() + } else if case let .failed(err) = state { + + // If connection reset by peer, it could still be a valid file transfer that has not been processed yet. => Wait + if err == .posix(.ECONNRESET) { + NearbyConnection.dispatchQueue.asyncAfter(deadline: .now() + 0.5) { + + // If already closed, ignore the error, as file transfer has been processed + if !self.connectionClosed { + recordErrorAndDisconnect(err: err) + } + } + } + else { + recordErrorAndDisconnect(err: err) + } + } + } + else { + log("[NearbyConnection] Connection already closed, ignoring state update: \(state)") + } + } + + connection.start(queue: NearbyConnection.dispatchQueue) + + func recordErrorAndDisconnect(err: NWError) { + self.lastError = err + log("[NearbyConnection] Connection Error: \(err)") + + // If the error is a connection reset, it could be due to firewall issues. + if err == .posix(.ENOTCONN) { + ConnectionFailureTracker.shared.recordFailure { + NearbyConnectionManager.shared.mainAppDelegate?.showFirewallAlert() + } + } + + if err == .posix(.ENETDOWN) { + self.lastError = NearbyError.protocolError("Error.NetworkDown".localized()) + } + + self.disconnect() + } + } + + + func connectionReady() {} + + + func protocolError() { + log("[NearbyConnection] Protocol error: \(String(describing: lastError))") + disconnect() + } + + + func processReceivedFrame(frameData _: Data) { + fatalError() + } + + + func processTransferSetupFrame(_: Sharing_Nearby_Frame) throws { + fatalError() + } + + + func isServer() -> Bool { + fatalError() + } + + + func processFileChunk(frame _: Location_Nearby_Connections_PayloadTransferFrame) throws { + protocolError() + } + + + func processBytesPayload(payload _: Data, id _: Int64) throws -> Bool { + return false + } + + + private func receiveFrameAsync() { + connection.receive(minimumIncompleteLength: 4, maximumLength: 4) { content, _, isComplete, error in + if self.connectionClosed { + return + } + if isComplete { + log("[NearbyConnection] Connection closed by peer during receiveFrameAsync") + self.lastError = NearbyError.protocolError("Error.ClosedByPeer".localized()) + self.disconnect() + return + } + if !(error == nil) { + self.lastError = error + self.protocolError() + return + } + guard let content = content else { + assertionFailure() + return + } + let frameLength = UInt32(content[0]) << 24 | UInt32(content[1]) << 16 | UInt32(content[2]) << 8 | UInt32(content[3]) + guard frameLength < self.maxFrameLength else { + self.lastError = NearbyError.protocolError("Unexpected packet length") + self.protocolError() + return + } + + self.startInactivityTimer() + self.receiveFrameAsync(length: frameLength) + } + } + + + private func receiveFrameAsync(length: UInt32) { + connection.receive(minimumIncompleteLength: Int(length), maximumLength: Int(length)) { content, _, isComplete, _ in + if self.connectionClosed { + return + } + if isComplete { + log("[NearbyConnection] Connection closed by peer during receiveFrameAsync") + self.lastError = NearbyError.protocolError("Error.ClosedByPeer".localized()) + self.disconnect() + return + } + guard let content = content else { + self.protocolError() + return + } + + self.startInactivityTimer() + self.processReceivedFrame(frameData: content) + self.receiveFrameAsync() + } + } + + + func sendFrameAsync(_ frame: Data, completion: (() -> Void)? = nil) { + if connectionClosed { + return + } + 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), + ]) + lengthPrefixedData.append(frame) + connection.send(content: lengthPrefixedData, completion: .contentProcessed { _ in + if let completion = completion { + completion() + } + }) + } + + + 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), + encryptKey, kCCKeySizeAES256, + [UInt8](iv), + serializedMsg, serializedMsg.count, + $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() + hb.header.encryptionScheme = .aes256Cbc + hb.header.signatureScheme = .hmacSha256 + 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!)) + try sendFrameAsync(smsg.serializedData(), completion: completion) + } + + + func sendTransferSetupFrame(_ frame: Sharing_Nearby_Frame) throws { + log("[NearbyConnection] Sending transfer setup frame.") + try sendBytesPayload(data: frame.serializedData(), id: Int64.random(in: Int64.min ... Int64.max)) + } + + + 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.payloadHeader.type = .bytes + 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.type = .payloadTransfer + wrapper.v1.payloadTransfer = transfer + try encryptAndSendOfflineFrame(wrapper) + + transfer.payloadChunk.flags = 1 // .lastChunk + transfer.payloadChunk.offset = Int64(transfer.payloadChunk.body.count) + transfer.payloadChunk.clearBody() + wrapper.v1.payloadTransfer = transfer + try encryptAndSendOfflineFrame(wrapper) + } + + + func decryptAndProcessReceivedSecureMessage(_ smsg: Securemessage_SecureMessage) throws { + guard smsg.hasSignature, smsg.hasHeaderAndBody else { throw NearbyError.requiredFieldMissing("secureMessage.signature|headerAndBody") } + + 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 = 0 + decryptedData.withUnsafeMutableBytes { + let status = CCCrypt( + CCOperation(kCCDecrypt), + CCAlgorithm(kCCAlgorithmAES128), + CCOptions(kCCOptionPKCS7Padding), + decryptKey, kCCKeySizeAES256, + [UInt8](headerAndBody.header.iv), + [UInt8](headerAndBody.body), headerAndBody.body.count, + $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 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 { + + guard offlineFrame.v1.hasPayloadTransfer else { throw NearbyError.requiredFieldMissing("offlineFrame.v1.payloadTransfer") } + + 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 { + log("[NearbyConnection] Payload transfer chunk is missing offset or flags, likely canceled.") + throw NearbyError.canceled(reason: .userCanceled) + } + + if case .bytes = header.type { + + let payloadID = header.id + + if header.totalSize > maxFrameLength { + + payloadBuffers.removeValue(forKey: payloadID) + throw NearbyError.protocolError("Payload too large (\(header.totalSize) bytes)") + } + + if payloadBuffers[payloadID] == nil { + + payloadBuffers[payloadID] = NSMutableData(capacity: Int(header.totalSize)) + } + + 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 { + payloadBuffers.removeValue(forKey: payloadID) + 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 { + + try processFileChunk(frame: payloadTransfer) + } + } + else if offlineFrame.hasV1, offlineFrame.v1.hasType, case .keepAlive = offlineFrame.v1.type { + + log("[NearbyConnection] Sent keep-alive, \(self.bytesTransferred) bytes sent") + sendKeepAlive(ack: true) + } else { + + log("[NearbyConnection] Unhandled offline frame encrypted: \(offlineFrame)") + } + } + + + 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)) + } + + + 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) } + } + + + 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 ..< length) + } + + + static func hkdf(inputKeyMaterial: SymmetricKey, salt: Data, info: Data, outputByteCount: Int) -> SymmetricKey { + return HKDF.deriveKey(inputKeyMaterial: inputKeyMaterial, salt: salt, info: info, outputByteCount: outputByteCount) + } + + + 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) + } + 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() + sha.update(data: dhs) + 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([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 { [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 + } + } + + + func disconnect() { + log("[NearbyConnection] Disconnecting from connection.") + + connection.stateUpdateHandler = nil + inactivityTimer?.cancel() + inactivityTimer = nil + connectionClosed = true + connection.send(content: nil, isComplete: true, completion: .contentProcessed { _ in }) + } + + + 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 { + try encryptAndSendOfflineFrame(offlineFrame) + } else { + try sendFrameAsync(offlineFrame.serializedData()) + } + + log("[NearbyConnection] Sent disconnection frame during sendDisconnectionAndDisconnect") + disconnect() + } + + + 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()) + + log("[NearbyConnection] Sent UKEY2 alert: \(type)") + disconnect() + } + + + 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 { + try encryptAndSendOfflineFrame(offlineFrame) + } else { + try sendFrameAsync(offlineFrame.serializedData()) + } + } catch { + log("[NearbyConnection] Error sending KEEP_ALIVE: \(error)") + } + } + + + func startInactivityTimer() { + + // Cancel previous timer if any + inactivityTimer?.cancel() + + inactivityTimer = DispatchSource.makeTimerSource(queue: NearbyConnection.dispatchQueue) + inactivityTimer?.schedule(deadline: .now() + timeoutInterval) + inactivityTimer?.setEventHandler { [weak self] in + + guard let self = self else { return } + + if !self.connectionClosed { + log("[NearbyConnection] Connection timeout: No message received in \(self.timeoutInterval) seconds") + self.lastError = NearbyError.canceled(reason: .timedOut) + self.disconnect() + } + } + + inactivityTimer?.resume() + } + + + // -- MARK: - Internal Data Model + + 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 + } +} + +public extension String { + func localized() -> String { + let localizedString = NSLocalizedString(self, comment: "") + return localizedString + } +} diff --git a/NearbyShareFramework/Connection/NearbyConnectionManager.swift b/NearbyShareFramework/Connection/NearbyConnectionManager.swift new file mode 100644 index 0000000..c1f29ee --- /dev/null +++ b/NearbyShareFramework/Connection/NearbyConnectionManager.swift @@ -0,0 +1,323 @@ +// +// NearbyConnectionManager.swift +// QuickDrop +// +// Created by Grishka on 08.04.2023. +// + +import Foundation +import Network +import System + + +public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearbyConnectionDelegate, OutboundNearbyConnectionDelegate { + + private let sleepManager = SleepManager.shared + private var tcpListener: NWListener + private var mdnsServices: [NetService] = [] + private var incomingConnections: [String: InboundNearbyConnection] = [:] + private var foundServices: [String: FoundServiceInfo] = [:] + private var shareExtensionDelegates: [ShareExtensionDelegate] = [] + private var outgoingTransfers: [String: OutgoingTransferInfo] = [:] + private var startedDeviceDiscovery = false + private var browsers: [NWBrowser] = [] + private let serviceTypes = ["_FC9F5ED42C8A._tcp."] + + public let endpointID: [UInt8] = generateEndpointID() + public var mainAppDelegate: (any MainAppDelegate)? + 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.incomingConnections[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) + { + return [UInt8](savedData) + } + + // Generate a new random ID + var id: [UInt8] = [] + let alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".compactMap { UInt8($0.asciiValue!) } + for _ in 0 ..< 4 { + id.append(alphabet[Int.random(in: 0 ..< alphabet.count)]) + } + + // Save to UserDefaults as String + let idString = String(bytes: id, encoding: .utf8) ?? "" + UserDefaults.standard.set(idString, forKey: userDefaultsKey) + + return id + } + + + private func initMDNS() { + let nameBytes: [UInt8] = [ + 0x23, // PCP + endpointID[0], endpointID[1], endpointID[2], endpointID[3], + 0xFC, 0x9F, 0x5E, // Service ID hash + 0, 0, + ] + + let name = Data(nameBytes).urlSafeBase64EncodedString() + let endpointInfo = EndpointInfo(name: Host.current().localizedName!, deviceType: .computer) + + let port = Int32(tcpListener.port!.rawValue) + + mdnsServices = serviceTypes.map { serviceType in + let service = NetService(domain: "", type: serviceType, name: name, port: port) + service.delegate = self + service.setTXTRecord(NetService.data(fromTXTRecord: [ + "n": endpointInfo.serialize().urlSafeBase64EncodedString().data(using: .utf8)!, + ])) + service.publish() + return service + } + } + + + func obtainUserConsent(for transfer: TransferMetadata, from device: RemoteDeviceInfo, connection _: InboundNearbyConnection) { + mainAppDelegate?.obtainUserConsent(for: transfer, from: device) + } + + + func connectionWasTerminated(connection: InboundNearbyConnection, error: Error?) { + incomingConnections.removeValue(forKey: connection.id) + + if !connection.wasRejected { + mainAppDelegate?.incomingTransfer(id: connection.id, didFinishWith: error) + } + } + + + public func submitUserConsent(transferID: String, accept: Bool, storeInTemp: Bool = false) { + guard let conn = incomingConnections[transferID] else { return } + + log("[NearbyConnectionManager] Submitting user consent for transfer, accepted: \(accept), store in temp: \(storeInTemp)") + conn.submitUserConsent(accepted: accept, storeInTemp: storeInTemp) + } + + + public func startDeviceDiscovery() { + + if !startedDeviceDiscovery { + startedDeviceDiscovery = true + foundServices.removeAll() + + log("[NearbyConnectionManager] Starting device discovery") + + if browsers.isEmpty { + for type in serviceTypes { + log("[NearbyConnectionManager] Starting browser for type \(type)") + + let browser = NWBrowser(for: .bonjourWithTXTRecord(type: type, domain: nil), using: .tcp) + browser.browseResultsChangedHandler = { _, changes in + for change in changes { + switch change { + case let .added(res): + self.addFoundDevice(service: res) + case let .removed(res): + self.removeFoundDevice(service: res) + default: + log("[NearbyConnectionManager] Ignoring change \(change)") + } + } + } + browser.start(queue: .main) + browsers.append(browser) + } + } + } + } + + + public func stopDeviceDiscovery() { + if startedDeviceDiscovery { + + log("Stopping device discovery") + + for browser in browsers { + browser.cancel() + } + + browsers.removeAll() + startedDeviceDiscovery = false + } + } + + + 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("[NearbyConnectionManager] Found Service \(service)") + for interface in service.interfaces { + if case .loopback = interface.type { + return + } + } + guard let endpointID = endpointID(for: service) else { return } + 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], textToSend: String?) { + 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, textToSend: textToSend) + 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 incomingConnections.count + outgoingTransfers.count + } + + + // -- MARK: - Internal Data Model + + private struct FoundServiceInfo { + let service: NWBrowser.Result + var device: RemoteDeviceInfo? + } + + + private struct OutgoingTransferInfo { + let service: NWBrowser.Result + let device: RemoteDeviceInfo + let connection: OutboundNearbyConnection + let delegate: ShareExtensionDelegate + } +} diff --git a/NearbyShareFramework/Connection/OutboundNearbyConnection.swift b/NearbyShareFramework/Connection/OutboundNearbyConnection.swift new file mode 100644 index 0000000..7c83375 --- /dev/null +++ b/NearbyShareFramework/Connection/OutboundNearbyConnection.swift @@ -0,0 +1,497 @@ +// +// OutboundNearbyConnection.swift +// NearbyShare +// +// Created by Grishka on 23.09.2023. +// + +import CommonCrypto +import CryptoKit +import Foundation +import Network +import System +import UniformTypeIdentifiers +import BigInt +import SwiftECC + + +class OutboundNearbyConnection: NearbyConnection { + private var currentState: State = .initial + private let urlsToSend: [URL] + private let textToSend: String? + private var ukeyClientFinishMsgData: Data? + private var queue: [OutgoingFileTransfer] = [] + private var currentTransfer: OutgoingFileTransfer? + private var totalBytesToSend: Int64 = 0 + private var totalBytesSent: Int64 = 0 + private var cancelled: Bool = false + private var textPayloadID: Int64 = 0 + + public var delegate: OutboundNearbyConnectionDelegate? + + enum State { + case initial, sentUkeyClientInit, sentUkeyClientFinish, sentPairedKeyEncryption, sentPairedKeyResult, sentIntroduction, sendingFiles + } + + init(connection: NWConnection, id: String, urlsToSend: [URL], textToSend: String?) { + + self.urlsToSend = urlsToSend + self.textToSend = textToSend + + super.init(connection: connection, id: id) + + if hasURL() || textToSend != nil { + 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() + } + } + } + + + override func disconnect() { + super.disconnect() + + if let error = lastError { + DispatchQueue.main.async { + self.delegate?.outboundConnection(connection: self, failedWithError: error) + } + } + } + + + 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) { + + if currentState != .sendingFiles { + log("[OutboundNearbyConnection] Received frame in state \(currentState)...") + } + + do { + 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 { + + log("[OutboundNearbyConnection] Error occured while processing frame with data \(frameData.hex): \(error)") + + if case NearbyError.ukey2 = error { + // do nothing + } + 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("[OutboundNearbyConnection] Transfer canceled") + try sendDisconnectionAndDisconnect() + delegate?.outboundConnection(connection: self, failedWithError: NearbyError.canceled(reason: .userCanceled)) + return + } + + 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("[OutboundNearbyConnection] 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) + log("[OutboundNearbyConnection] Invalid message type: \(frame.messageType)") + throw NearbyError.ukey2 + } + let serverInit = try Securegcm_Ukey2ServerInit(serializedBytes: frame.messageData) + guard serverInit.version == 1 else { + sendUkey2Alert(type: .badVersion) + log("[OutboundNearbyConnection] Invalid version: \(serverInit.version)") + throw NearbyError.ukey2 + } + guard serverInit.random.count == 32 else { + sendUkey2Alert(type: .badRandom) + log("[OutboundNearbyConnection] Invalid random: \(serverInit.random.count)") + throw NearbyError.ukey2 + } + guard serverInit.handshakeCipher == .p256Sha512 else { + sendUkey2Alert(type: .badHandshakeCipher) + log("[OutboundNearbyConnection] Invalid handshake cipher: \(serverInit.handshakeCipher)") + 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 { + 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 let textToSend = textToSend { + var meta = Sharing_Nearby_TextMetadata() + meta.type = .text + meta.textTitle = textToSend + meta.size = Int64(textToSend.utf8.count) + meta.payloadID = textPayloadID + introduction.v1.introduction.textMetadata.append(meta) + + } else if hasURL() { + 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 { + let type = UTType(typeID) + if let type = type, let mimeType = type.preferredMIMEType { + meta.mimeType = mimeType + } + } + if meta.mimeType.starts(with: "image/") { + meta.type = .image + } else if meta.mimeType.starts(with: "video/") { + meta.type = .video + } else if meta.mimeType.starts(with: "audio/") { + meta.type = .audio + } else if url.pathExtension.lowercased() == "apk" { + meta.type = .app + } else { + meta.type = .unknown + } + meta.payloadID = Int64.random(in: Int64.min ... Int64.max) + 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("[OutboundNearbyConnection] 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 let textToSend = textToSend { + try sendText(text: textToSend) + } + if hasURL() { + 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 hasURL() -> Bool { + urlsToSend.count == 1 && !urlsToSend[0].isFileURL + } + + + private func sendURL() throws { + try sendText(text: urlsToSend[0].absoluteString) + } + + + private func sendText(text: String) throws { + try sendBytesPayload(data: Data(text.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("[OutboundNearbyConnection] Disconnecting because all files have been transferred") + try sendDisconnectionAndDisconnect() + delegate?.outboundConnectionTransferFinished(connection: self) + return + } + currentTransfer = queue.removeFirst() + } + + let fileBuffer: Data + guard let _fileBuffer = try currentTransfer!.handle!.read(upToCount: 512 * 1024) else { + throw NearbyError.inputOutput + } + fileBuffer = _fileBuffer + + 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() + } + }) + totalBytesSent += Int64(fileBuffer.count) + + // only for logging + self.bytesTransferred += Int64(fileBuffer.count) + + startInactivityTimer() + 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("[OutboundNearbyConnection] Sent EOF, current transfer: \(String(describing: currentTransfer))") + } + } + + + private static func sanitizeFileName(name: String) -> String { + return name.replacingOccurrences(of: "[\\/\\\\?%\\*:\\|\"<>=]", with: "_", options: .regularExpression) + } + + + // -- MARK: - Internal Data Model + + private struct OutgoingFileTransfer { + let url: URL + let payloadID: Int64 + let handle: FileHandle? + let totalBytes: Int64 + var currentOffset: Int64 + } +} diff --git a/NearbyShareFramework/ProtobufSource/device_to_device_messages.proto b/NearbyShareFramework/ProtobufSource/device_to_device_messages.proto new file mode 100644 index 0000000..5600373 --- /dev/null +++ b/NearbyShareFramework/ProtobufSource/device_to_device_messages.proto @@ -0,0 +1,81 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package securegcm; + +import "securemessage.proto"; + +option optimize_for = LITE_RUNTIME; +option java_package = "com.google.security.cryptauth.lib.securegcm"; +option java_outer_classname = "DeviceToDeviceMessagesProto"; +option objc_class_prefix = "SGCM"; + +// Used by protocols between devices +message DeviceToDeviceMessage { + // the payload of the message + optional bytes message = 1; + + // the sequence number of the message - must be increasing. + optional int32 sequence_number = 2; +} + +// sent as the first message from initiator to responder +// in an unauthenticated Diffie-Hellman Key Exchange +message InitiatorHello { + // The session public key to send to the responder + optional securemessage.GenericPublicKey public_dh_key = 1; + + // The protocol version + optional int32 protocol_version = 2 [default = 0]; +} + +// sent inside the header of the first message from the responder to the +// initiator in an unauthenticated Diffie-Hellman Key Exchange +message ResponderHello { + // The session public key to send to the initiator + optional securemessage.GenericPublicKey public_dh_key = 1; + + // The protocol version + optional int32 protocol_version = 2 [default = 0]; +} + +// Type of curve +enum Curve { ED_25519 = 1; } + +// A convenience proto for encoding curve points in affine representation +message EcPoint { + required Curve curve = 1; + + // x and y are encoded in big-endian two's complement + // client MUST verify (x,y) is a valid point on the specified curve + required bytes x = 2; + required bytes y = 3; +} + +message SpakeHandshakeMessage { + // Each flow in the protocol bumps this counter + optional int32 flow_number = 1; + + // Some (but not all) SPAKE flows send a point on an elliptic curve + optional EcPoint ec_point = 2; + + // Some (but not all) SPAKE flows send a hash value + optional bytes hash_value = 3; + + // The last flow of a SPAKE protocol can send an optional payload, + // since the key exchange is already complete on the sender's side. + optional bytes payload = 4; +} diff --git a/NearbyShareFramework/ProtobufSource/offline_wire_formats.proto b/NearbyShareFramework/ProtobufSource/offline_wire_formats.proto new file mode 100644 index 0000000..c112004 --- /dev/null +++ b/NearbyShareFramework/ProtobufSource/offline_wire_formats.proto @@ -0,0 +1,403 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package location.nearby.connections; + +option optimize_for = LITE_RUNTIME; +option java_outer_classname = "OfflineWireFormatsProto"; +option java_package = "com.google.location.nearby.connections.proto"; +option objc_class_prefix = "GNCP"; + +message OfflineFrame { + enum Version { + UNKNOWN_VERSION = 0; + V1 = 1; + } + optional Version version = 1; + + // Right now there's only 1 version, but if there are more, exactly one of + // the following fields will be set. + optional V1Frame v1 = 2; +} + +message V1Frame { + enum FrameType { + UNKNOWN_FRAME_TYPE = 0; + CONNECTION_REQUEST = 1; + CONNECTION_RESPONSE = 2; + PAYLOAD_TRANSFER = 3; + BANDWIDTH_UPGRADE_NEGOTIATION = 4; + KEEP_ALIVE = 5; + DISCONNECTION = 6; + PAIRED_KEY_ENCRYPTION = 7; + } + optional FrameType type = 1; + + // Exactly one of the following fields will be set. + optional ConnectionRequestFrame connection_request = 2; + optional ConnectionResponseFrame connection_response = 3; + optional PayloadTransferFrame payload_transfer = 4; + optional BandwidthUpgradeNegotiationFrame bandwidth_upgrade_negotiation = 5; + optional KeepAliveFrame keep_alive = 6; + optional DisconnectionFrame disconnection = 7; + optional PairedKeyEncryptionFrame paired_key_encryption = 8; +} + +message ConnectionRequestFrame { + // Should always match cs/symbol:location.nearby.proto.connections.Medium + // LINT.IfChange + enum Medium { + UNKNOWN_MEDIUM = 0; + MDNS = 1 [deprecated = true]; + BLUETOOTH = 2; + WIFI_HOTSPOT = 3; + BLE = 4; + WIFI_LAN = 5; + WIFI_AWARE = 6; + NFC = 7; + WIFI_DIRECT = 8; + WEB_RTC = 9; + BLE_L2CAP = 10; + USB = 11; + } + // LINT.ThenChange(//depot/google3/third_party/nearby/proto/connections_enums.proto) + + optional string endpoint_id = 1; + optional string endpoint_name = 2; + optional bytes handshake_data = 3; + // A random number generated for each outgoing connection that is presently + // used to act as a tiebreaker when 2 devices connect to each other + // simultaneously; this can also be used for other initialization-scoped + // things in the future. + optional int32 nonce = 4; + // The mediums this device supports upgrading to. This list should be filtered + // by both the strategy and this device's individual limitations. + repeated Medium mediums = 5; + optional bytes endpoint_info = 6; + optional MediumMetadata medium_metadata = 7; + optional int32 keep_alive_interval_millis = 8; + optional int32 keep_alive_timeout_millis = 9; + // The type of {@link Device} object. + optional int32 device_type = 10 [default = 0]; + // The bytes of serialized {@link Device} object. + optional bytes device_info = 11; +} + +message ConnectionResponseFrame { + // This doesn't need to send back endpoint_id and endpoint_name (like + // the ConnectionRequestFrame does) because those have already been + // transmitted out-of-band, at the time this endpoint was discovered. + + // One of: + // + // - ConnectionsStatusCodes.STATUS_OK + // - ConnectionsStatusCodes.STATUS_CONNECTION_REJECTED. + optional int32 status = 1 [deprecated = true]; + optional bytes handshake_data = 2; + + // Used to replace the status integer parameter with a meaningful enum item. + // Map ConnectionsStatusCodes.STATUS_OK to ACCEPT and + // ConnectionsStatusCodes.STATUS_CONNECTION_REJECTED to REJECT. + // Flag: connection_replace_status_with_response_connectionResponseFrame + enum ResponseStatus { + UNKNOWN_RESPONSE_STATUS = 0; + ACCEPT = 1; + REJECT = 2; + } + optional ResponseStatus response = 3; + optional OsInfo os_info = 4; + // A bitmask value to indicate which medium supports Multiplex transmission + // feature. Each supporting medium could utilize one bit starting from the + // least significant bit in this field. eq. BT utilizes the LSB bit which 0x01 + // means bt supports multiplex while 0x00 means not. Refer to ClientProxy.java + // for the bit usages. + optional int32 multiplex_socket_bitmask = 5; + optional int32 nearby_connections_version = 6; +} + +message PayloadTransferFrame { + enum PacketType { + UNKNOWN_PACKET_TYPE = 0; + DATA = 1; + CONTROL = 2; + } + + message PayloadHeader { + enum PayloadType { + UNKNOWN_PAYLOAD_TYPE = 0; + BYTES = 1; + FILE = 2; + STREAM = 3; + } + optional int64 id = 1; + optional PayloadType type = 2; + optional int64 total_size = 3; + optional bool is_sensitive = 4; + optional string file_name = 5; + optional string parent_folder = 6; + } + + // Accompanies DATA packets. + message PayloadChunk { + enum Flags { + LAST_CHUNK = 0x1; + } + optional int32 flags = 1; + optional int64 offset = 2; + optional bytes body = 3; + } + + // Accompanies CONTROL packets. + message ControlMessage { + enum EventType { + UNKNOWN_EVENT_TYPE = 0; + PAYLOAD_ERROR = 1; + PAYLOAD_CANCELED = 2; + PAYLOAD_RECEIVED_ACK = 3; + } + + optional EventType event = 1; + optional int64 offset = 2; + } + + optional PacketType packet_type = 1; + optional PayloadHeader payload_header = 2; + + // Exactly one of the following fields will be set, depending on the type. + optional PayloadChunk payload_chunk = 3; + optional ControlMessage control_message = 4; +} + +message BandwidthUpgradeNegotiationFrame { + enum EventType { + UNKNOWN_EVENT_TYPE = 0; + UPGRADE_PATH_AVAILABLE = 1; + LAST_WRITE_TO_PRIOR_CHANNEL = 2; + SAFE_TO_CLOSE_PRIOR_CHANNEL = 3; + CLIENT_INTRODUCTION = 4; + UPGRADE_FAILURE = 5; + CLIENT_INTRODUCTION_ACK = 6; + } + + // Accompanies UPGRADE_PATH_AVAILABLE and UPGRADE_FAILURE events. + message UpgradePathInfo { + // Should always match cs/symbol:location.nearby.proto.connections.Medium + enum Medium { + UNKNOWN_MEDIUM = 0; + MDNS = 1 [deprecated = true]; + BLUETOOTH = 2; + WIFI_HOTSPOT = 3; + BLE = 4; + WIFI_LAN = 5; + WIFI_AWARE = 6; + NFC = 7; + WIFI_DIRECT = 8; + WEB_RTC = 9; + // 10 is reserved. + USB = 11; + } + + // Accompanies Medium.WIFI_HOTSPOT. + message WifiHotspotCredentials { + optional string ssid = 1; + optional string password = 2; + optional int32 port = 3; + optional string gateway = 4 [default = "0.0.0.0"]; + // This field can be a band or frequency + optional int32 frequency = 5 [default = -1]; + } + + // Accompanies Medium.WIFI_LAN. + message WifiLanSocket { + optional bytes ip_address = 1; + optional int32 wifi_port = 2; + } + + // Accompanies Medium.BLUETOOTH. + message BluetoothCredentials { + optional string service_name = 1; + optional string mac_address = 2; + } + + // Accompanies Medium.WIFI_AWARE. + message WifiAwareCredentials { + optional string service_id = 1; + optional bytes service_info = 2; + optional string password = 3; + } + + // Accompanies Medium.WIFI_DIRECT. + message WifiDirectCredentials { + optional string ssid = 1; + optional string password = 2; + optional int32 port = 3; + optional int32 frequency = 4; + optional string gateway = 5 [default = "0.0.0.0"]; + } + + // Accompanies Medium.WEB_RTC + message WebRtcCredentials { + optional string peer_id = 1; + optional LocationHint location_hint = 2; + } + + optional Medium medium = 1; + + // Exactly one of the following fields will be set. + optional WifiHotspotCredentials wifi_hotspot_credentials = 2; + optional WifiLanSocket wifi_lan_socket = 3; + optional BluetoothCredentials bluetooth_credentials = 4; + optional WifiAwareCredentials wifi_aware_credentials = 5; + optional WifiDirectCredentials wifi_direct_credentials = 6; + optional WebRtcCredentials web_rtc_credentials = 8; + + // Disable Encryption for this upgrade medium to improve throughput. + optional bool supports_disabling_encryption = 7; + + // An ack will be sent after the CLIENT_INTRODUCTION frame. + optional bool supports_client_introduction_ack = 9; + } + + // Accompanies CLIENT_INTRODUCTION events. + message ClientIntroduction { + optional string endpoint_id = 1; + optional bool supports_disabling_encryption = 2; + } + + // Accompanies CLIENT_INTRODUCTION_ACK events. + message ClientIntroductionAck {} + + optional EventType event_type = 1; + + // Exactly one of the following fields will be set. + optional UpgradePathInfo upgrade_path_info = 2; + optional ClientIntroduction client_introduction = 3; + optional ClientIntroductionAck client_introduction_ack = 4; +} + +message KeepAliveFrame { + // And ack will be sent after receiving KEEP_ALIVE frame. + optional bool ack = 1; +} + +// Informs the remote side to immediately severe the socket connection. +// Used in bandwidth upgrades to get around a race condition, but may be used +// in other situations to trigger a faster disconnection event than waiting for +// socket closed on the remote side. +message DisconnectionFrame { + // Apply safe-to-disconnect protocol if true. + optional bool request_safe_to_disconnect = 1; + + // Ack of receiving Disconnection frame will be sent to the sender + // frame. + optional bool ack_safe_to_disconnect = 2; +} + +// A paired key encryption packet sent between devices, contains signed data. +message PairedKeyEncryptionFrame { + // The encrypted data (raw authentication token for the established + // connection) in byte array format. + optional bytes signed_data = 1; +} + +message MediumMetadata { + // True if local device supports 5GHz. + optional bool supports_5_ghz = 1; + // WiFi LAN BSSID, in the form of a six-byte MAC address: XX:XX:XX:XX:XX:XX + optional string bssid = 2; + // IP address, in network byte order: the highest order byte of the address is + // in byte[0]. + optional bytes ip_address = 3; + // True if local device supports 6GHz. + optional bool supports_6_ghz = 4; + // True if local device has mobile radio. + optional bool mobile_radio = 5; + // The frequency of the WiFi LAN AP(in MHz). Or -1 is not associated with an + // AP over WiFi, -2 represents the active network uses an Ethernet transport. + optional int32 ap_frequency = 6 [default = -1]; + // Available channels on the local device. + optional AvailableChannels available_channels = 7; + // Usable WiFi Direct client channels on the local device. + optional WifiDirectCliUsableChannels wifi_direct_cli_usable_channels = 8; + // Usable WiFi LAN channels on the local device. + optional WifiLanUsableChannels wifi_lan_usable_channels = 9; + // Usable WiFi Aware channels on the local device. + optional WifiAwareUsableChannels wifi_aware_usable_channels = 10; + // Usable WiFi Hotspot STA channels on the local device. + optional WifiHotspotStaUsableChannels wifi_hotspot_sta_usable_channels = 11; +} + +// Available channels on the local device. +message AvailableChannels { + repeated int32 channels = 1 [packed = true]; +} + +// Usable WiFi Direct client channels on the local device. +message WifiDirectCliUsableChannels { + repeated int32 channels = 1 [packed = true]; +} + +// Usable WiFi LAN channels on the local device. +message WifiLanUsableChannels { + repeated int32 channels = 1 [packed = true]; +} + +// Usable WiFi Aware channels on the local device. +message WifiAwareUsableChannels { + repeated int32 channels = 1 [packed = true]; +} + +// Usable WiFi Hotspot STA channels on the local device. +message WifiHotspotStaUsableChannels { + repeated int32 channels = 1 [packed = true]; +} + +// LocationHint is used to specify a location as well as format. +message LocationHint { + // Location is the location, provided in the format specified by format. + optional string location = 1; + + // the format of location. + optional LocationStandard.Format format = 2; +} + +message LocationStandard { + enum Format { + UNKNOWN = 0; + // E164 country codes: + // https://en.wikipedia.org/wiki/List_of_country_calling_codes + // e.g. +1 for USA + E164_CALLING = 1; + + // ISO 3166-1 alpha-2 country codes: + // https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 + ISO_3166_1_ALPHA_2 = 2; + } +} + +// Device capability for OS information. +message OsInfo { + enum OsType { + UNKNOWN_OS_TYPE = 0; + ANDROID = 1; + CHROME_OS = 2; + WINDOWS = 3; + APPLE = 4; + LINUX = 100; // g3 test environment + } + + optional OsType type = 1; +} diff --git a/NearbyShareFramework/ProtobufSource/securegcm.proto b/NearbyShareFramework/ProtobufSource/securegcm.proto new file mode 100644 index 0000000..0325f06 --- /dev/null +++ b/NearbyShareFramework/ProtobufSource/securegcm.proto @@ -0,0 +1,308 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package securegcm; + +option optimize_for = LITE_RUNTIME; +option java_package = "com.google.security.cryptauth.lib.securegcm"; +option java_outer_classname = "SecureGcmProto"; +option objc_class_prefix = "SGCM"; + +// Message used only during enrollment +// Field numbers should be kept in sync with DeviceInfo in: +// java/com/google/security/cryptauth/backend/services/common/common.proto +message GcmDeviceInfo { + // This field's name does not match the one in DeviceInfo for legacy reasons. + // Consider using long_device_id and device_type instead when enrolling + // non-android devices. + optional fixed64 android_device_id = 1; + + // Used for device_address of DeviceInfo field 2, but for GCM capable devices. + optional bytes gcm_registration_id = 102; + + // Used for device_address of DeviceInfo field 2, but for iOS devices. + optional bytes apn_registration_id = 202; + + // Does the user have notifications enabled for the given device address. + optional bool notification_enabled = 203 [default = true]; + + // Used for device_address of DeviceInfo field 2, a Bluetooth Mac address for + // the device (e.g., to be used with EasyUnlock) + optional string bluetooth_mac_address = 302; + + // SHA-256 hash of the device master key (from the key exchange). + // Differs from DeviceInfo field 3, which contains the actual master key. + optional bytes device_master_key_hash = 103; + + // A SecureMessage.EcP256PublicKey + required bytes user_public_key = 4; + + // device's model name + // (e.g., an android.os.Build.MODEL or UIDevice.model) + optional string device_model = 7; + + // device's locale + optional string locale = 8; + + // The handle for user_public_key (and implicitly, a master key) + optional bytes key_handle = 9; + + // The initial counter value for the device, sent by the device + optional int64 counter = 12 [default = 0]; + + // The Operating System version on the device + // (e.g., an android.os.Build.DISPLAY or UIDevice.systemVersion) + optional string device_os_version = 13; + + // The Operating System version number on the device + // (e.g., an android.os.Build.VERSION.SDK_INT) + optional int64 device_os_version_code = 14; + + // The Operating System release on the device + // (e.g., an android.os.Build.VERSION.RELEASE) + optional string device_os_release = 15; + + // The Operating System codename on the device + // (e.g., an android.os.Build.VERSION.CODENAME or UIDevice.systemName) + optional string device_os_codename = 16; + + // The software version running on the device + // (e.g., Authenticator app version string) + optional string device_software_version = 17; + + // The software version number running on the device + // (e.g., Authenticator app version code) + optional int64 device_software_version_code = 18; + + // Software package information if applicable + // (e.g., com.google.android.apps.authenticator2) + optional string device_software_package = 19; + + // Size of the display in thousandths of an inch (e.g., 7000 mils = 7 in) + optional int32 device_display_diagonal_mils = 22; + + // For Authzen capable devices, their Authzen protocol version + optional int32 device_authzen_version = 24; + + // Not all devices have device identifiers that fit in 64 bits. + optional bytes long_device_id = 29; + + // The device manufacturer name + // (e.g., android.os.Build.MANUFACTURER) + optional string device_manufacturer = 31; + + // Used to indicate which type of device this is. + optional DeviceType device_type = 32 [default = ANDROID]; + + // Fields corresponding to screenlock type/features and hardware features + // should be numbered in the 400 range. + + // Is this device using a secure screenlock (e.g., pattern or pin unlock) + optional bool using_secure_screenlock = 400 [default = false]; + + // Is auto-unlocking the screenlock (e.g., when at "home") supported? + optional bool auto_unlock_screenlock_supported = 401 [default = false]; + + // Is auto-unlocking the screenlock (e.g., when at "home") enabled? + optional bool auto_unlock_screenlock_enabled = 402 [default = false]; + + // Does the device have a Bluetooth (classic) radio? + optional bool bluetooth_radio_supported = 403 [default = false]; + + // Is the Bluetooth (classic) radio on? + optional bool bluetooth_radio_enabled = 404 [default = false]; + + // Does the device hardware support a mobile data connection? + optional bool mobile_data_supported = 405 [default = false]; + + // Does the device support tethering? + optional bool tethering_supported = 406 [default = false]; + + // Does the device have a BLE radio? + optional bool ble_radio_supported = 407 [default = false]; + + // Is the device a "Pixel Experience" Android device? + optional bool pixel_experience = 408 [default = false]; + + // Is the device running in the ARC++ container on a chromebook? + optional bool arc_plus_plus = 409 [default = false]; + + // Is the value set in |using_secure_screenlock| reliable? On some Android + // devices, the platform API to get the screenlock state is not trustworthy. + // See b/32212161. + optional bool is_screenlock_state_flaky = 410 [default = false]; + + // A list of multi-device software features supported by the device. + repeated SoftwareFeature supported_software_features = 411; + + // A list of multi-device software features currently enabled (active) on the + // device. + repeated SoftwareFeature enabled_software_features = 412; + + // The enrollment session id this is sent with + optional bytes enrollment_session_id = 1000; + + // A copy of the user's OAuth token + optional string oauth_token = 1001; +} + +// This enum is used by iOS devices as values for device_display_diagonal_mils +// in GcmDeviceInfo. There is no good way to calculate it on those devices. +enum AppleDeviceDiagonalMils { + // This is the mils diagonal on an iPhone 5. + APPLE_PHONE = 4000; + // This is the mils diagonal on an iPad mini. + APPLE_PAD = 7900; +} + +// This should be kept in sync with DeviceType in: +// java/com/google/security/cryptauth/backend/services/common/common_enums.proto +enum DeviceType { + UNKNOWN = 0; + ANDROID = 1; + CHROME = 2; + IOS = 3; + BROWSER = 4; + OSX = 5; +} + +// MultiDevice features which may be supported and enabled on a device. See +enum SoftwareFeature { + UNKNOWN_FEATURE = 0; + BETTER_TOGETHER_HOST = 1; + BETTER_TOGETHER_CLIENT = 2; + EASY_UNLOCK_HOST = 3; + EASY_UNLOCK_CLIENT = 4; + MAGIC_TETHER_HOST = 5; + MAGIC_TETHER_CLIENT = 6; + SMS_CONNECT_HOST = 7; + SMS_CONNECT_CLIENT = 8; +} + +// A list of "reasons" that can be provided for calling server-side APIs. +// This is particularly important for calls that can be triggered by different +// kinds of events. Please try to keep reasons as generic as possible, so that +// codes can be re-used by various callers in a sensible fashion. +enum InvocationReason { + REASON_UNKNOWN = 0; + // First run of the software package invoking this call + REASON_INITIALIZATION = 1; + // Ordinary periodic actions (e.g. monthly master key rotation) + REASON_PERIODIC = 2; + // Slow-cycle periodic action (e.g. yearly keypair rotation???) + REASON_SLOW_PERIODIC = 3; + // Fast-cycle periodic action (e.g. daily sync for Smart Lock users) + REASON_FAST_PERIODIC = 4; + // Expired state (e.g. expired credentials, or cached entries) was detected + REASON_EXPIRATION = 5; + // An unexpected protocol failure occurred (so attempting to repair state) + REASON_FAILURE_RECOVERY = 6; + // A new account has been added to the device + REASON_NEW_ACCOUNT = 7; + // An existing account on the device has been changed + REASON_CHANGED_ACCOUNT = 8; + // The user toggled the state of a feature (e.g. Smart Lock enabled via BT) + REASON_FEATURE_TOGGLED = 9; + // A "push" from the server caused this action (e.g. a sync tickle) + REASON_SERVER_INITIATED = 10; + // A local address change triggered this (e.g. GCM registration id changed) + REASON_ADDRESS_CHANGE = 11; + // A software update has triggered this + REASON_SOFTWARE_UPDATE = 12; + // A manual action by the user triggered this (e.g. commands sent via adb) + REASON_MANUAL = 13; + // A custom key has been invalidated on the device (e.g. screen lock is + // disabled). + REASON_CUSTOM_KEY_INVALIDATION = 14; + // Periodic action triggered by auth_proximity + REASON_PROXIMITY_PERIODIC = 15; +} + +enum Type { + ENROLLMENT = 0; + TICKLE = 1; + TX_REQUEST = 2; + TX_REPLY = 3; + TX_SYNC_REQUEST = 4; + TX_SYNC_RESPONSE = 5; + TX_PING = 6; + DEVICE_INFO_UPDATE = 7; + TX_CANCEL_REQUEST = 8; + + // DEPRECATED (can be re-used after Aug 2015) + PROXIMITYAUTH_PAIRING = 10; + + // The kind of identity assertion generated by a "GCM V1" device (i.e., + // an Android phone that has registered with us a public and a symmetric + // key) + GCMV1_IDENTITY_ASSERTION = 11; + + // Device-to-device communications are protected by an unauthenticated + // Diffie-Hellman exchange. The InitiatorHello message is simply the + // initiator's public DH key, and is not encoded as a SecureMessage, so + // it doesn't have a tag. + // The ResponderHello message (which is sent by the responder + // to the initiator), on the other hand, carries a payload that is protected + // by the derived shared key. It also contains the responder's + // public DH key. ResponderHelloAndPayload messages have the + // DEVICE_TO_DEVICE_RESPONDER_HELLO tag. + DEVICE_TO_DEVICE_RESPONDER_HELLO_PAYLOAD = 12; + + // Device-to-device communications are protected by an unauthenticated + // Diffie-Hellman exchange. Once the initiator and responder + // agree on a shared key (through Diffie-Hellman), they will use messages + // tagged with DEVICE_TO_DEVICE_MESSAGE to exchange data. + DEVICE_TO_DEVICE_MESSAGE = 13; + + // Notification to let a device know it should contact a nearby device. + DEVICE_PROXIMITY_CALLBACK = 14; + + // Device-to-device communications are protected by an unauthenticated + // Diffie-Hellman exchange. During device-to-device authentication, the first + // message from initiator (the challenge) is signed and put into the payload + // of the message sent back to the initiator. + UNLOCK_KEY_SIGNED_CHALLENGE = 15; + + // Specialty (corp only) features + LOGIN_NOTIFICATION = 101; +} + +message GcmMetadata { + required Type type = 1; + optional int32 version = 2 [default = 0]; +} + +message Tickle { + // Time after which this tickle should expire + optional fixed64 expiry_time = 1; +} + +message LoginNotificationInfo { + // Time at which the server received the login notification request. + optional fixed64 creation_time = 2; + + // Must correspond to user_id in LoginNotificationRequest, if set. + optional string email = 3; + + // Host where the user's credentials were used to login, if meaningful. + optional string host = 4; + + // Location from where the user's credentials were used, if meaningful. + optional string source = 5; + + // Type of login, e.g. ssh, gnome-screensaver, or web. + optional string event_type = 6; +} diff --git a/NearbyShareFramework/ProtobufSource/securemessage.proto b/NearbyShareFramework/ProtobufSource/securemessage.proto new file mode 100644 index 0000000..5118d35 --- /dev/null +++ b/NearbyShareFramework/ProtobufSource/securemessage.proto @@ -0,0 +1,126 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Proto definitions for SecureMessage format + +syntax = "proto2"; + +package securemessage; + +option optimize_for = LITE_RUNTIME; +option java_package = "com.google.security.cryptauth.lib.securemessage"; +option java_outer_classname = "SecureMessageProto"; +option objc_class_prefix = "SMSG"; + +message SecureMessage { + // Must contain a HeaderAndBody message + required bytes header_and_body = 1; + // Signature of header_and_body + required bytes signature = 2; +} + +// Supported "signature" schemes (both symmetric key and public key based) +enum SigScheme { + HMAC_SHA256 = 1; + ECDSA_P256_SHA256 = 2; + // Not recommended -- use ECDSA_P256_SHA256 instead + RSA2048_SHA256 = 3; +} + +// Supported encryption schemes +enum EncScheme { + // No encryption + NONE = 1; + AES_256_CBC = 2; +} + +message Header { + required SigScheme signature_scheme = 1; + required EncScheme encryption_scheme = 2; + // Identifies the verification key + optional bytes verification_key_id = 3; + // Identifies the decryption key + optional bytes decryption_key_id = 4; + // Encryption may use an IV + optional bytes iv = 5; + // Arbitrary per-protocol public data, to be sent with the plain-text header + optional bytes public_metadata = 6; + // The length of some associated data this is not sent in this SecureMessage, + // but which will be bound to the signature. + optional uint32 associated_data_length = 7 [default = 0]; +} + +message HeaderAndBody { + // Public data about this message (to be bound in the signature) + required Header header = 1; + // Payload data + required bytes body = 2; +} + +// Must be kept wire-format compatible with HeaderAndBody. Provides the +// SecureMessage code with a consistent wire-format representation that +// remains stable irrespective of protobuf implementation choices. This +// low-level representation of a HeaderAndBody should not be used by +// any code outside of the SecureMessage library implementation/tests. +message HeaderAndBodyInternal { + // A raw (wire-format) byte encoding of a Header, suitable for hashing + required bytes header = 1; + // Payload data + required bytes body = 2; +} + +// ------- +// The remainder of the messages defined here are provided only for +// convenience. They are not needed for SecureMessage proper, but are +// commonly useful wherever SecureMessage might be applied. +// ------- + +// A list of supported public key types +enum PublicKeyType { + EC_P256 = 1; + RSA2048 = 2; + // 2048-bit MODP group 14, from RFC 3526 + DH2048_MODP = 3; +} + +// A convenience proto for encoding NIST P-256 elliptic curve public keys +message EcP256PublicKey { + // x and y are encoded in big-endian two's complement (slightly wasteful) + // Client MUST verify (x,y) is a valid point on NIST P256 + required bytes x = 1; + required bytes y = 2; +} + +// A convenience proto for encoding RSA public keys with small exponents +message SimpleRsaPublicKey { + // Encoded in big-endian two's complement + required bytes n = 1; + optional int32 e = 2 [default = 65537]; +} + +// A convenience proto for encoding Diffie-Hellman public keys, +// for use only when Elliptic Curve based key exchanges are not possible. +// (Note that the group parameters must be specified separately) +message DhPublicKey { + // Big-endian two's complement encoded group element + required bytes y = 1; +} + +message GenericPublicKey { + required PublicKeyType type = 1; + optional EcP256PublicKey ec_p256_public_key = 2; + optional SimpleRsaPublicKey rsa2048_public_key = 3; + // Use only as a last resort + optional DhPublicKey dh2048_public_key = 4; +} diff --git a/NearbyShareFramework/ProtobufSource/ukey.proto b/NearbyShareFramework/ProtobufSource/ukey.proto new file mode 100644 index 0000000..327d8d3 --- /dev/null +++ b/NearbyShareFramework/ProtobufSource/ukey.proto @@ -0,0 +1,105 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto2"; + +package securegcm; + +option optimize_for = LITE_RUNTIME; +option java_package = "com.google.security.cryptauth.lib.securegcm"; +option java_outer_classname = "UkeyProto"; + +message Ukey2Message { + enum Type { + UNKNOWN_DO_NOT_USE = 0; + ALERT = 1; + CLIENT_INIT = 2; + SERVER_INIT = 3; + CLIENT_FINISH = 4; + } + + optional Type message_type = 1; // Identifies message type + optional bytes message_data = 2; // Actual message, to be parsed according to + // message_type +} + +message Ukey2Alert { + enum AlertType { + // Framing errors + BAD_MESSAGE = 1; // The message could not be deserialized + BAD_MESSAGE_TYPE = 2; // message_type has an undefined value + INCORRECT_MESSAGE = 3; // message_type received does not correspond to + // expected type at this stage of the protocol + BAD_MESSAGE_DATA = 4; // Could not deserialize message_data as per + // value inmessage_type + + // ClientInit and ServerInit errors + BAD_VERSION = 100; // version is invalid; server cannot find + // suitable version to speak with client. + BAD_RANDOM = 101; // Random data is missing or of incorrect + // length + BAD_HANDSHAKE_CIPHER = 102; // No suitable handshake ciphers were found + BAD_NEXT_PROTOCOL = 103; // The next protocol is missing, unknown, or + // unsupported + BAD_PUBLIC_KEY = 104; // The public key could not be parsed + + // Other errors + INTERNAL_ERROR = 200; // An internal error has occurred. error_message + // may contain additional details for logging + // and debugging. + } + + optional AlertType type = 1; + optional string error_message = 2; +} + +enum Ukey2HandshakeCipher { + RESERVED = 0; + P256_SHA512 = 100; // NIST P-256 used for ECDH, SHA512 used for + // commitment + CURVE25519_SHA512 = 200; // Curve 25519 used for ECDH, SHA512 used for + // commitment +} + +message Ukey2ClientInit { + optional int32 version = 1; // highest supported version for rollback + // protection + optional bytes random = 2; // random bytes for replay/reuse protection + + // One commitment (hash of ClientFinished containing public key) per supported + // cipher + message CipherCommitment { + optional Ukey2HandshakeCipher handshake_cipher = 1; + optional bytes commitment = 2; + } + repeated CipherCommitment cipher_commitments = 3; + + // Next protocol that the client wants to speak. + optional string next_protocol = 4; +} + +message Ukey2ServerInit { + optional int32 version = 1; // highest supported version for rollback + // protection + optional bytes random = 2; // random bytes for replay/reuse protection + + // Selected Cipher and corresponding public key + optional Ukey2HandshakeCipher handshake_cipher = 3; + optional bytes public_key = 4; +} + +message Ukey2ClientFinished { + optional bytes public_key = 1; // public key matching selected handshake + // cipher +} diff --git a/NearbyShareFramework/ProtobufSource/wire_format.proto b/NearbyShareFramework/ProtobufSource/wire_format.proto new file mode 100644 index 0000000..6853e22 --- /dev/null +++ b/NearbyShareFramework/ProtobufSource/wire_format.proto @@ -0,0 +1,236 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Brought from: //depot/google3/location/nearby/sharing/proto/wire_format.proto +// At CL 317565061 + +syntax = "proto2"; + +package sharing.nearby; + +// Required in Chrome. +option optimize_for = LITE_RUNTIME; + +// File metadata. Does not include the actual bytes of the file. +// NEXT_ID=6 +message FileMetadata { + enum Type { + UNKNOWN = 0; + IMAGE = 1; + VIDEO = 2; + APP = 3; + AUDIO = 4; + } + + // The human readable name of this file (eg. 'Cookbook.pdf'). + optional string name = 1; + + // The type of file (eg. 'IMAGE' from 'dog.jpg'). Specifying a type helps + // provide a richer experience on the receiving side. + optional Type type = 2 [default = UNKNOWN]; + + // The FILE payload id that will be sent as a follow up containing the actual + // bytes of the file. + optional int64 payload_id = 3; + + // The total size of the file. + optional int64 size = 4; + + // The mimeType of file (eg. 'image/jpeg' from 'dog.jpg'). Specifying a + // mimeType helps provide a richer experience on receiving side. + optional string mime_type = 5 [default = "application/octet-stream"]; + + // A uuid for the attachment. Should be unique across all attachments. + optional int64 id = 6; +} + +// NEXT_ID=5 +message TextMetadata { + enum Type { + UNKNOWN = 0; + TEXT = 1; + // Open with browsers. + URL = 2; + // Open with map apps. + ADDRESS = 3; + // Dial. + PHONE_NUMBER = 4; + } + + // The title of the text content. + optional string text_title = 2; + + // The type of text (phone number, url, address, or plain text). + optional Type type = 3 [default = UNKNOWN]; + + // The BYTE payload id that will be sent as a follow up containing the actual + // bytes of the text. + optional int64 payload_id = 4; + + // The size of the text content. + optional int64 size = 5; + + // A uuid for the attachment. Should be unique across all attachments. + optional int64 id = 6; +} + +// NEXT_ID=5 +message WifiCredentialsMetadata { + enum SecurityType { + UNKNOWN_SECURITY_TYPE = 0; + OPEN = 1; + WPA_PSK = 2; + WEP = 3; + } + + // The Wifi network name. This will be sent in introduction. + optional string ssid = 2; + + // The security type of network (OPEN, WPA_PSK, WEP). + optional SecurityType security_type = 3 [default = UNKNOWN_SECURITY_TYPE]; + + // The BYTE payload id that will be sent as a follow up containing the + // password. + optional int64 payload_id = 4; + + // A uuid for the attachment. Should be unique across all attachments. + optional int64 id = 5; +} + +// A frame used when sending messages over the wire. +// NEXT_ID=3 +message Frame { + enum Version { + UNKNOWN_VERSION = 0; + V1 = 1; + } + optional Version version = 1; + + // Right now there's only 1 version, but if there are more, exactly one of + // the following fields will be set. + optional V1Frame v1 = 2; +} + +// NEXT_ID=7 +message V1Frame { + enum FrameType { + UNKNOWN_FRAME_TYPE = 0; + INTRODUCTION = 1; + RESPONSE = 2; + PAIRED_KEY_ENCRYPTION = 3; + PAIRED_KEY_RESULT = 4; + CERTIFICATE_INFO = 5; + CANCEL = 6; + } + + optional FrameType type = 1; + + // Exactly one of the following fields will be set. + optional IntroductionFrame introduction = 2; + optional ConnectionResponseFrame connection_response = 3; + optional PairedKeyEncryptionFrame paired_key_encryption = 4; + optional PairedKeyResultFrame paired_key_result = 5; + optional CertificateInfoFrame certificate_info = 6; +} + +// An introduction packet sent by the sending side. Contains a list of files +// they'd like to share. +// NEXT_ID=4 +message IntroductionFrame { + repeated FileMetadata file_metadata = 1; + repeated TextMetadata text_metadata = 2; + // The required app package to open the content. May be null. + optional string required_package = 3; + repeated WifiCredentialsMetadata wifi_credentials_metadata = 4; +} + +// A response packet sent by the receiving side. Accepts or rejects the list of +// files. +// NEXT_ID=2 +message ConnectionResponseFrame { + enum Status { + UNKNOWN = 0; + ACCEPT = 1; + REJECT = 2; + NOT_ENOUGH_SPACE = 3; + UNSUPPORTED_ATTACHMENT_TYPE = 4; + TIMED_OUT = 5; + } + + // The receiving side's response. + optional Status status = 1; +} + +// A paired key encryption packet sent between devices, contains signed data. +// NEXT_ID=3 +message PairedKeyEncryptionFrame { + // The encrypted data in byte array format. + optional bytes signed_data = 1; + + // The hash of a certificate id. + optional bytes secret_id_hash = 2; + + // An optional encrypted data in byte array format. + optional bytes optional_signed_data = 3; +} + +// A paired key verification result packet sent between devices. +// NEXT_ID=2 +message PairedKeyResultFrame { + enum Status { + UNKNOWN = 0; + SUCCESS = 1; + FAIL = 2; + UNABLE = 3; + } + + // The verification result. + optional Status status = 1; +} + +// A package containing certificate info to be shared to remote device offline. +// NEXT_ID=2 +message CertificateInfoFrame { + // The public certificates to be shared with remote devices. + repeated PublicCertificate public_certificate = 1; +} + +// A public certificate from the local device. +// NEXT_ID=8 +message PublicCertificate { + // The unique id of the public certificate. + optional bytes secret_id = 1; + + // A bytes representation of a Secret Key owned by contact, to decrypt the + // metadata_key stored within the advertisement. + optional bytes authenticity_key = 2; + + // A bytes representation a public key of X509Certificate, owned by contact, + // to decrypt encrypted UKEY2 (from Nearby Connections API) as a hand shake in + // contact verification phase. + optional bytes public_key = 3; + + // The time in millis from epoch when this certificate becomes effective. + optional int64 start_time = 4; + + // The time in millis from epoch when this certificate expires. + optional int64 end_time = 5; + + // The encrypted metadata in bytes, contains personal information of the + // device/user who created this certificate. Needs to be decrypted into bytes, + // and converted back to EncryptedMetadata object to access fields. + optional bytes encrypted_metadata_bytes = 6; + + // The tag for verifying metadata_encryption_key. + optional bytes metadata_encryption_key_tag = 7; +} + +// NEXT_ID=3 +message WifiCredentials { + // Wi-Fi password. + optional string password = 1; + // True if the network is a hidden network that is not broadcasting its SSID. + // Default is false. + optional bool hidden_ssid = 2 [default = false]; +} \ No newline at end of file diff --git a/NearbyShareFramework/Util/ConnectionFailureTracker.swift b/NearbyShareFramework/Util/ConnectionFailureTracker.swift new file mode 100644 index 0000000..d728a21 --- /dev/null +++ b/NearbyShareFramework/Util/ConnectionFailureTracker.swift @@ -0,0 +1,41 @@ +// +// ConnectionFailureTracker.swift +// QuickDrop +// +// Created by Leon Böttger on 15.05.25. +// + +import Foundation + +class ConnectionFailureTracker { + static let shared = ConnectionFailureTracker() + + private var failureTimestamps: [Date] = [] + private var lastAlertShownAt: Date? + + private let queue = DispatchQueue(label: "ConnectionFailureTrackerQueue") + + private init() {} + + func recordFailure(onTrigger: @escaping () -> Void) { + queue.async { + let now = Date() + + // Remove old timestamps older than 10 seconds + self.failureTimestamps = self.failureTimestamps.filter { now.timeIntervalSince($0) < 10 } + self.failureTimestamps.append(now) + + // Check if alert can be shown + let canShowAlert = self.failureTimestamps.count >= 2 && + (self.lastAlertShownAt == nil || now.timeIntervalSince(self.lastAlertShownAt!) >= 60) + + if canShowAlert { + self.lastAlertShownAt = now + self.failureTimestamps.removeAll() + DispatchQueue.main.async { + onTrigger() + } + } + } + } +} diff --git a/NearbyShareFramework/Util/Helpers.swift b/NearbyShareFramework/Util/Helpers.swift new file mode 100644 index 0000000..4ac8d26 --- /dev/null +++ b/NearbyShareFramework/Util/Helpers.swift @@ -0,0 +1,100 @@ +// +// Helpers.swift +// QuickDrop +// +// Created by Leon Böttger on 04.04.25. +// + +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 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) + } + + + var hex: String { + return map { String(format: "%02hhx", $0) }.joined() + } +} + + +extension String { + var dataFromHex: Data { + var data = Data(capacity: count / 2) + var index = startIndex + while index < endIndex { + let nextIndex = self.index(index, offsetBy: 2) + if nextIndex <= endIndex, + let byte = UInt8(self[index ..< nextIndex], radix: 16) + { + data.append(byte) + } else { + break // or handle invalid hex gracefully + } + index = nextIndex + } + return data + } +} + + +public enum UserDefaultsKeys: String, CaseIterable { + case isEligibleForIap = "isEligibleForIap" + case appLaunchedBefore = "ShowedWelcomeScreen" + case plusVersion = "isPlusVersion" + case transmissionCount = "reviewRequestCountKey" + case automaticallyAcceptFiles = "automaticallyAcceptFiles" + case saveFolderBookmark = "saveFolderBookmark" + case openFinderAfterReceiving = "openFinderAfterReceiving" + case endpointID = "endpointID" +} + + +public func isPlusVersion() -> Bool { + return UserDefaults.standard.bool(forKey: UserDefaultsKeys.plusVersion.rawValue) +} + + +public func isFileTransferRestricted() -> Bool { + (!isPlusVersion() && transmissionCount() > 1) +} + + +public func transmissionCount() -> Int { + return UserDefaults.standard.integer(forKey: UserDefaultsKeys.transmissionCount.rawValue) +} diff --git a/NearbyShareFramework/Util/Logger.swift b/NearbyShareFramework/Util/Logger.swift new file mode 100644 index 0000000..6d35119 --- /dev/null +++ b/NearbyShareFramework/Util/Logger.swift @@ -0,0 +1,133 @@ +// +// Logger.swift +// QuickDrop +// +// Created by Leon Böttger on 26.02.25. +// + +import Foundation +import os + +fileprivate let loggerQueue = DispatchQueue(label: "loggerQueue", qos: .utility) + +/// Manages logging functionality +final class LogManager { + + /// The URL of the log file + let logFileURL: URL? + + /// Shared instance + public static let sharedInstance = LogManager() + + @Published var loggingEnabled = true + + + /// Private initializer + private init() { + // Use the App Group container + let directory = getAppGroupDirectory() + + // Create file name from date + let formatter = DateFormatter() + formatter.dateFormat = "dd-MM-yyyy" + let dateString = formatter.string(from: Date()) + let fileName = "\(dateString).log" + + // Full path in app group container + self.logFileURL = directory?.appendingPathComponent(fileName) + } + + + /// Writes a specified string to the log file. + func writeToFile(string: String) { + guard let logFileURL = logFileURL else { return } + + let formatter = DateFormatter() + formatter.dateFormat = "HH:mm:ss" + let timestamp = formatter.string(from: Date()) + guard let data = (timestamp + " - " + string + "\n").data(using: .utf8) else { return } + + loggerQueue.async { + let fileManager = FileManager.default + + if !fileManager.fileExists(atPath: logFileURL.path) { + // Clean up older logs + self.deleteLogs() + // Create file atomically + try? data.write(to: logFileURL, options: .atomicWrite) + return + } + + // Atomic append with file locking using FileHandle + do { + let fileHandle = try FileHandle(forWritingTo: logFileURL) + defer { try? fileHandle.close() } + + // Locking is handled by the serial dispatch queue + try fileHandle.seekToEnd() + try fileHandle.write(contentsOf: data) + } catch { + print("[Logger] Failed to write to log file: \(error.localizedDescription)") + } + } + } + + + /// Gets the contents of the log file. + func getLogString() -> String { + guard let logFileURL = logFileURL else { return "" } + do { + return try String(contentsOf: logFileURL, encoding: .utf8) + } catch { + log("[LUI] Error reading file: \(error.localizedDescription)") + return "" + } + } + + + /// Deletes all log files in the shared container. + func deleteLogs() { + log("[LUI] Deleting all logs...") + + guard let directory = getAppGroupDirectory() else { return } + let fileManager = FileManager.default + + guard let allFiles = try? fileManager.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil) else { + return + } + + let logFiles = allFiles.filter { $0.pathExtension == "log" } + for file in logFiles { + do { + log("[LUI] Removing \(file.lastPathComponent)") + try fileManager.removeItem(at: file) + } catch { + print("[Logger] Failed to delete file: \(error.localizedDescription)") + } + } + } +} + + +/// Logs a specified string +public func log(_ text: String) { + let logger = LogManager.sharedInstance + + if logger.loggingEnabled { + loggerQueue.async { + print(text) +#if !os(tvOS) + let isAppExtension = Bundle.main.bundlePath.hasSuffix(".appex") + let prefix = isAppExtension ? "[EXT]" : "[APP]" + logger.writeToFile(string: "\(prefix) \(text)") +#endif + } + } +} + + +/// Returns the shared App Group directory +fileprivate func getAppGroupDirectory() -> URL? { + let groupIdentifier = "group.com.leonboettger.neardrop" + return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: groupIdentifier) +} diff --git a/NearbyShareFramework/Util/SaveFilesManager.swift b/NearbyShareFramework/Util/SaveFilesManager.swift new file mode 100644 index 0000000..73b3e71 --- /dev/null +++ b/NearbyShareFramework/Util/SaveFilesManager.swift @@ -0,0 +1,220 @@ +// +// SaveFilesManager.swift +// QuickDrop +// +// Created by Leon Böttger on 26.07.25. +// + +import Foundation +import AppKit +import ImageIO + +public class SaveFilesManager { + + private init() { + // remove old temp directory + + let tempPath = FileManager.default.temporaryDirectory + let fileManager = FileManager.default + + do { + let contents = try fileManager.contentsOfDirectory(at: tempPath, includingPropertiesForKeys: nil) + + var didSomething = false + + for item in contents { + didSomething = true + try fileManager.removeItem(at: item) + } + + if didSomething { + log("Temporary directory cleared.") + } + } catch { + log("Failed to list contents of temp directory: \(error)") + } + } + + public static let shared = SaveFilesManager() + + let tempDirectory: URL = FileManager.default.temporaryDirectory.appendingPathComponent("Pending") + private var securityScopeUrl: URL? + + private var filesFinishedDownloading = [URL]() + private var filesFinishedDownloadingSinceLastRun = [URL]() + + + public func registerFileFinishedDownloading(_ fileURL: URL) { + filesFinishedDownloading.append(fileURL) + filesFinishedDownloadingSinceLastRun.append(fileURL) + + // read out EXIF timestamp and manually set file creation date to it + applyEXIFTimestamps(at: fileURL) + } + + + public func movePendingFilesToTarget() { + + if isPlusVersion() { + do { + let fileManager = FileManager.default + let files = try fileManager.contentsOfDirectory(at: tempDirectory, includingPropertiesForKeys: nil) + + let target = getSaveDirectory() + var loggedExecution = false + + for file in files { + + if !loggedExecution { + log("Moving pending files to target directory") + loggedExecution = true + } + + let fileName = file.lastPathComponent + let destinationURL = target.appendingPathComponent(fileName) + + if !filesFinishedDownloading.contains(destinationURL) { + log("File \(file) not finished downloading, skipping") + continue + } + + log("Moving file: \(file.lastPathComponent) to \(destinationURL.lastPathComponent)") + + do { + 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) + } + catch { + log("Error moving file: \(error)") + } + } + log("Moved all pending files to target directory") + } + catch { + // Pending directory doesn't exist or is empty + } + } + + if !filesFinishedDownloadingSinceLastRun.isEmpty && !isFileTransferRestricted() { + + if UserDefaults.standard.bool(forKey: UserDefaultsKeys.openFinderAfterReceiving.rawValue) { + log("Opening \(filesFinishedDownloadingSinceLastRun.count) file(s) in Finder.") + NSWorkspace.shared.activateFileViewerSelecting(filesFinishedDownloadingSinceLastRun) + } + + // Clear the list of finished files + filesFinishedDownloadingSinceLastRun.removeAll() + } + + stopAccessingSecurityScopedResource() + } + + + public func stopAccessingSecurityScopedResource() { + guard let url = securityScopeUrl else { + return + } + + log("Stopping access to security scoped resource: \(url)") + url.stopAccessingSecurityScopedResource() + 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)") + + securityScopeUrl = url + return url + } + } else { + log("Bookmark is stale, using default downloads folder.") + } + + } catch { + log("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 { + fatalError("Failed to get downloads directory: \(error)") + } + } + + + /// Returns the EXIF original date of an image file, adjusted to the current time zone. + private func exifOriginalDate(from url: URL) -> Date? { + guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil), + let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any], + let exif = properties[kCGImagePropertyExifDictionary] as? [CFString: Any], + let dateString = exif[kCGImagePropertyExifDateTimeOriginal] as? String + else { + return nil + } + + let formatter = DateFormatter() + formatter.dateFormat = "yyyy:MM:dd HH:mm:ss" + formatter.timeZone = TimeZone.current // Use the current device time zone + return formatter.date(from: dateString) + } + + + /// Applies EXIF timestamps to a given file or directory. + private func applyEXIFTimestamps(at url: URL) { + let fm = FileManager.default + var isDir: ObjCBool = false + guard fm.fileExists(atPath: url.path, isDirectory: &isDir) else { return } + + if isDir.boolValue { + // It's a directory — enumerate files + if let enumerator = fm.enumerator(at: url, includingPropertiesForKeys: [.isRegularFileKey]) { + for case let fileURL as URL in enumerator { + guard (try? fileURL.resourceValues(forKeys: [.isRegularFileKey]))?.isRegularFile == true else { continue } + applyEXIFTimestamp(to: fileURL) + } + } + } else { + // It's a single file + applyEXIFTimestamp(to: url) + } + } + + + /// Applies EXIF timestamp (if available) to a single file + private func applyEXIFTimestamp(to fileURL: URL) { + guard let exifDate = exifOriginalDate(from: fileURL) else { + return + } + + // Set the "creation date" + try? FileManager.default.setAttributes([.creationDate: exifDate], ofItemAtPath: fileURL.path) + } +} diff --git a/NearbyShareFramework/Util/SleepManager.swift b/NearbyShareFramework/Util/SleepManager.swift new file mode 100644 index 0000000..01da003 --- /dev/null +++ b/NearbyShareFramework/Util/SleepManager.swift @@ -0,0 +1,59 @@ +// +// SleepManager.swift +// QuickDrop +// +// Created by Leon Böttger on 03.05.25. +// + +import Foundation +import IOKit +import IOKit.pwr_mgt + +class SleepManager { + public static let shared = SleepManager() + private var assertionID: IOPMAssertionID = 0 + private var timer: Timer? + + private init() { + // Start periodic check + timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in + self?.updateSleepState() + } + + RunLoop.main.add(timer!, forMode: .common) + } + + private func disableSleep() { + if assertionID != 0 { + return + } + + log("[SleepManager] Enabling Wakelock") + + IOPMAssertionCreateWithName( + kIOPMAssertionTypePreventUserIdleDisplaySleep as CFString, + IOPMAssertionLevel(kIOPMAssertionLevelOn), + "QuickDrop Data Transfer" as CFString, + &assertionID + ) + } + + private func enableSleep() { + if assertionID == 0 { + return + } + + log("[SleepManager] Disabling Wakelock") + IOPMAssertionRelease(assertionID) + assertionID = 0 + } + + private func updateSleepState() { + let activeConnections = NearbyConnectionManager.shared.getActiveConnectionsCount() + if activeConnections > 0 { + disableSleep() + } else { + enableSleep() + } + } +} diff --git a/QuickDrop.xcodeproj/project.pbxproj b/QuickDrop.xcodeproj/project.pbxproj index 6f5e99c..5dc89e5 100644 --- a/QuickDrop.xcodeproj/project.pbxproj +++ b/QuickDrop.xcodeproj/project.pbxproj @@ -9,43 +9,55 @@ /* Begin PBXBuildFile section */ 691F53BB2ABB70840089FD92 /* DeviceListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691F53B92ABB70840089FD92 /* DeviceListCell.swift */; }; 691F53BC2ABB70840089FD92 /* DeviceListCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 691F53BA2ABB70840089FD92 /* DeviceListCell.xib */; }; - 691F53BE2ABF03820089FD92 /* OutboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */; }; 699DEBA62AB0573200115D22 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 699DEBA52AB0573200115D22 /* ShareViewController.swift */; }; 699DEBA92AB0573200115D22 /* ShareViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 699DEBA72AB0573200115D22 /* ShareViewController.xib */; }; 699DEBAE2AB0573200115D22 /* QuickDropShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 699DEBA12AB0573200115D22 /* QuickDropShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 69D2C32F29E7898C00EC7E30 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 69D2C32E29E7898C00EC7E30 /* MainMenu.xib */; }; 69DA9A1229E0BF5100A442DA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1129E0BF5100A442DA /* AppDelegate.swift */; }; 69DA9A1429E0BF5200A442DA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 69DA9A1329E0BF5200A442DA /* Assets.xcassets */; }; - 69DCF48A2AB70E8C00CBE2CC /* wire_format.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2929E18CB500A442DA /* wire_format.pb.swift */; }; - 69DCF48B2AB70E8C00CBE2CC /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; }; - 69DCF48C2AB70E8C00CBE2CC /* device_to_device_messages.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2A29E18CB500A442DA /* device_to_device_messages.pb.swift */; }; - 69DCF48D2AB70E8C00CBE2CC /* offline_wire_formats.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2B29E18CB500A442DA /* offline_wire_formats.pb.swift */; }; - 69DCF48E2AB70E8C00CBE2CC /* securegcm.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2C29E18CB500A442DA /* securegcm.pb.swift */; }; - 69DCF48F2AB70E8C00CBE2CC /* securemessage.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */; }; - 69DCF4902AB70E9700CBE2CC /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; }; - 69DCF4912AB70E9700CBE2CC /* NearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698DFAE529E2F91A0064F247 /* NearbyConnection.swift */; }; - 69DCF4922AB70E9700CBE2CC /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; }; - 69DCF4942AB8BF7B00CBE2CC /* libNearbyShare.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 69DCF4812AB70D0600CBE2CC /* libNearbyShare.dylib */; }; - 69DCF4952AB8BF7B00CBE2CC /* libNearbyShare.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 69DCF4812AB70D0600CBE2CC /* libNearbyShare.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - 69DCF49A2AB8C58500CBE2CC /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = 69DCF4992AB8C58500CBE2CC /* SwiftProtobuf */; }; - 69DCF49C2AB8C58500CBE2CC /* SwiftECC in Frameworks */ = {isa = PBXBuildFile; productRef = 69DCF49B2AB8C58500CBE2CC /* SwiftECC */; }; - 69DCF49D2AB8C9A500CBE2CC /* libNearbyShare.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 69DCF4812AB70D0600CBE2CC /* libNearbyShare.dylib */; }; - 69DCF49E2AB8C9A500CBE2CC /* libNearbyShare.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 69DCF4812AB70D0600CBE2CC /* libNearbyShare.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - EB2B41E42DC63ACC0070FC39 /* SleepManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2B41E32DC63AC80070FC39 /* SleepManager.swift */; }; - EB2F9BFB2DA04B69004010A2 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2F9BFA2DA04B67004010A2 /* Helpers.swift */; }; EB2F9BFE2DA05F48004010A2 /* BezelNotification in Frameworks */ = {isa = PBXBuildFile; productRef = EB2F9BFD2DA05F48004010A2 /* BezelNotification */; }; EB3643602D73A2A2009CDFF8 /* AudioManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB36435F2D73A29F009CDFF8 /* AudioManager.swift */; }; EB5C36932E34EBBD0052EFD7 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36922E34EBBD0052EFD7 /* Localizable.xcstrings */; }; EB5C36942E34EBBD0052EFD7 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36922E34EBBD0052EFD7 /* Localizable.xcstrings */; }; EB5C36E42E34EC0E0052EFD7 /* License.txt in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36E32E34EC0E0052EFD7 /* License.txt */; }; EB5C36E52E34EC0E0052EFD7 /* License.txt in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36E32E34EC0E0052EFD7 /* License.txt */; }; - EB5C36E72E3544FF0052EFD7 /* SaveFilesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E62E3544FB0052EFD7 /* SaveFilesManager.swift */; }; - EB5C36E92E3546120052EFD7 /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E82E3546100052EFD7 /* DataModel.swift */; }; - EB6E743E2DD5C77700331466 /* ConnectionFailureTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB6E743D2DD5C76F00331466 /* ConnectionFailureTracker.swift */; }; + EB5C37B72E377A6F0052EFD7 /* ConnectionFailureTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB6E743D2DD5C76F00331466 /* ConnectionFailureTracker.swift */; }; + EB5C37B82E377A6F0052EFD7 /* SleepManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2B41E32DC63AC80070FC39 /* SleepManager.swift */; }; + EB5C37B92E377A6F0052EFD7 /* SaveFilesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E62E3544FB0052EFD7 /* SaveFilesManager.swift */; }; + EB5C37BA2E377A6F0052EFD7 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2F9BFA2DA04B67004010A2 /* Helpers.swift */; }; + EB5C37BB2E377A700052EFD7 /* ConnectionFailureTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB6E743D2DD5C76F00331466 /* ConnectionFailureTracker.swift */; }; + EB5C37BC2E377A700052EFD7 /* SleepManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2B41E32DC63AC80070FC39 /* SleepManager.swift */; }; + EB5C37BD2E377A700052EFD7 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2F9BFA2DA04B67004010A2 /* Helpers.swift */; }; + EB5C37BE2E377A700052EFD7 /* SaveFilesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E62E3544FB0052EFD7 /* SaveFilesManager.swift */; }; + EB5C37BF2E377A780052EFD7 /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; }; + EB5C37C02E377A780052EFD7 /* securemessage.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */; }; + EB5C37C12E377A780052EFD7 /* offline_wire_formats.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2B29E18CB500A442DA /* offline_wire_formats.pb.swift */; }; + EB5C37C22E377A780052EFD7 /* device_to_device_messages.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2A29E18CB500A442DA /* device_to_device_messages.pb.swift */; }; + EB5C37C32E377A780052EFD7 /* wire_format.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2929E18CB500A442DA /* wire_format.pb.swift */; }; + EB5C37C42E377A780052EFD7 /* securegcm.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2C29E18CB500A442DA /* securegcm.pb.swift */; }; + EB5C37C52E377A780052EFD7 /* securemessage.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */; }; + EB5C37C62E377A780052EFD7 /* securegcm.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2C29E18CB500A442DA /* securegcm.pb.swift */; }; + EB5C37C72E377A780052EFD7 /* wire_format.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2929E18CB500A442DA /* wire_format.pb.swift */; }; + EB5C37C82E377A780052EFD7 /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; }; + EB5C37C92E377A780052EFD7 /* device_to_device_messages.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2A29E18CB500A442DA /* device_to_device_messages.pb.swift */; }; + EB5C37CA2E377A780052EFD7 /* offline_wire_formats.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2B29E18CB500A442DA /* offline_wire_formats.pb.swift */; }; + EB5C37CB2E377A7F0052EFD7 /* NearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698DFAE529E2F91A0064F247 /* NearbyConnection.swift */; }; + EB5C37CC2E377A7F0052EFD7 /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E82E3546100052EFD7 /* DataModel.swift */; }; + EB5C37CD2E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */; }; + EB5C37CE2E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; }; + EB5C37CF2E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; }; + EB5C37D02E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */; }; + EB5C37D12E377A7F0052EFD7 /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E82E3546100052EFD7 /* DataModel.swift */; }; + EB5C37D22E377A7F0052EFD7 /* NearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698DFAE529E2F91A0064F247 /* NearbyConnection.swift */; }; + EB5C37D32E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; }; + EB5C37D42E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; }; + EB5C37D62E377ACF0052EFD7 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37D52E377ACF0052EFD7 /* SwiftProtobuf */; }; + EB5C37D82E377ACF0052EFD7 /* SwiftECC in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37D72E377ACF0052EFD7 /* SwiftECC */; }; + EB5C37DA2E377AD60052EFD7 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37D92E377AD60052EFD7 /* SwiftProtobuf */; }; + EB5C37DC2E377AD60052EFD7 /* SwiftECC in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37DB2E377AD60052EFD7 /* SwiftECC */; }; EB6F62D72D91F4AF00534EC2 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB6F62D62D91F4AC00534EC2 /* SettingsView.swift */; }; EB7CD1452D6FB0C300CB0CB5 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB7CD1442D6FB0C100CB0CB5 /* Logger.swift */; }; EB7CD1462D6FB0E800CB0CB5 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB7CD1442D6FB0C100CB0CB5 /* Logger.swift */; }; - EB7CD1472D6FB0E800CB0CB5 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB7CD1442D6FB0C100CB0CB5 /* Logger.swift */; }; EB7F54122D78E96000989564 /* TutorialView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB7F54112D78E95C00989564 /* TutorialView.swift */; }; EB7F54142D78E9E700989564 /* LicenseWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB7F54132D78E9E400989564 /* LicenseWindow.swift */; }; EB96ED8B2D7F7CF7004B3FF1 /* SmallSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB96ED8A2D7F7CF4004B3FF1 /* SmallSheetView.swift */; }; @@ -53,7 +65,6 @@ EB96ED8D2D7F7EA3004B3FF1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 69DA9A1329E0BF5200A442DA /* Assets.xcassets */; }; EB96ED8F2D7F8105004B3FF1 /* Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB96ED8E2D7F8103004B3FF1 /* Localized.swift */; }; EB96ED902D7F8114004B3FF1 /* Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB96ED8E2D7F8103004B3FF1 /* Localized.swift */; }; - EBC05C592E32C22200EECC18 /* Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB96ED8E2D7F8103004B3FF1 /* Localized.swift */; }; EBC05C5B2E33937200EECC18 /* ErrorAlertHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBC05C5A2E33936C00EECC18 /* ErrorAlertHandler.swift */; }; EBC05C5C2E3394EF00EECC18 /* ErrorAlertHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBC05C5A2E33936C00EECC18 /* ErrorAlertHandler.swift */; }; EBC05C712E33B57D00EECC18 /* Converter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBC05C6D2E33B57D00EECC18 /* Converter.swift */; }; @@ -85,20 +96,6 @@ remoteGlobalIDString = 699DEBA02AB0573200115D22; remoteInfo = ShareExtension; }; - 69DCF4962AB8BF7B00CBE2CC /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 69DA9A0629E0BF5100A442DA /* Project object */; - proxyType = 1; - remoteGlobalIDString = 69DCF4802AB70D0600CBE2CC; - remoteInfo = NearbyShare; - }; - 69DCF49F2AB8C9A500CBE2CC /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 69DA9A0629E0BF5100A442DA /* Project object */; - proxyType = 1; - remoteGlobalIDString = 69DCF4802AB70D0600CBE2CC; - remoteInfo = NearbyShare; - }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -129,7 +126,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 69DCF4952AB8BF7B00CBE2CC /* libNearbyShare.dylib in Embed Libraries */, ); name = "Embed Libraries"; runOnlyForDeploymentPostprocessing = 0; @@ -140,7 +136,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 69DCF49E2AB8C9A500CBE2CC /* libNearbyShare.dylib in Embed Libraries */, ); name = "Embed Libraries"; runOnlyForDeploymentPostprocessing = 0; @@ -172,7 +167,6 @@ 69DA9A2B29E18CB500A442DA /* offline_wire_formats.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = offline_wire_formats.pb.swift; sourceTree = ""; }; 69DA9A2C29E18CB500A442DA /* securegcm.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = securegcm.pb.swift; sourceTree = ""; }; 69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = securemessage.pb.swift; sourceTree = ""; }; - 69DCF4812AB70D0600CBE2CC /* libNearbyShare.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = libNearbyShare.dylib; sourceTree = BUILT_PRODUCTS_DIR; }; EB2B41E32DC63AC80070FC39 /* SleepManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SleepManager.swift; sourceTree = ""; }; EB2F9BFA2DA04B67004010A2 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; EB36435F2D73A29F009CDFF8 /* AudioManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioManager.swift; sourceTree = ""; }; @@ -215,7 +209,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 69DCF49D2AB8C9A500CBE2CC /* libNearbyShare.dylib in Frameworks */, + EB5C37D82E377ACF0052EFD7 /* SwiftECC in Frameworks */, + EB5C37D62E377ACF0052EFD7 /* SwiftProtobuf in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -224,16 +219,8 @@ buildActionMask = 2147483647; files = ( EB2F9BFE2DA05F48004010A2 /* BezelNotification in Frameworks */, - 69DCF4942AB8BF7B00CBE2CC /* libNearbyShare.dylib in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 69DCF47F2AB70D0600CBE2CC /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 69DCF49C2AB8C58500CBE2CC /* SwiftECC in Frameworks */, - 69DCF49A2AB8C58500CBE2CC /* SwiftProtobuf in Frameworks */, + EB5C37DC2E377AD60052EFD7 /* SwiftECC in Frameworks */, + EB5C37DA2E377AD60052EFD7 /* SwiftProtobuf in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -278,7 +265,6 @@ children = ( 69DA9A0E29E0BF5100A442DA /* QuickDrop.app */, 699DEBA12AB0573200115D22 /* QuickDropShareExtension.appex */, - 69DCF4812AB70D0600CBE2CC /* libNearbyShare.dylib */, ); name = Products; sourceTree = ""; @@ -314,9 +300,8 @@ 69DCF4822AB70D0600CBE2CC /* NearbyShare */ = { isa = PBXGroup; children = ( + EB5C37DD2E377B430052EFD7 /* Model */, EB5C371C2E3669010052EFD7 /* Util */, - EB3B73F92DC8FF3A00369951 /* ProtobufSource */, - 69DA9A2729E18CB500A442DA /* Protobuf */, EB5C371D2E3669150052EFD7 /* Connection */, ); path = NearbyShare; @@ -352,7 +337,6 @@ children = ( 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */, 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */, - EB5C36E82E3546100052EFD7 /* DataModel.swift */, 691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */, 698DFAE529E2F91A0064F247 /* NearbyConnection.swift */, ); @@ -378,6 +362,16 @@ path = Util; sourceTree = ""; }; + EB5C37DD2E377B430052EFD7 /* Model */ = { + isa = PBXGroup; + children = ( + EB3B73F92DC8FF3A00369951 /* ProtobufSource */, + 69DA9A2729E18CB500A442DA /* Protobuf */, + EB5C36E82E3546100052EFD7 /* DataModel.swift */, + ); + path = Model; + sourceTree = ""; + }; EBC05C6A2E33B55100EECC18 /* View */ = { isa = PBXGroup; children = ( @@ -443,16 +437,6 @@ }; /* End PBXGroup section */ -/* Begin PBXHeadersBuildPhase section */ - 69DCF47D2AB70D0600CBE2CC /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXHeadersBuildPhase section */ - /* Begin PBXNativeTarget section */ 699DEBA02AB0573200115D22 /* QuickDropShareExtension */ = { isa = PBXNativeTarget; @@ -466,7 +450,6 @@ buildRules = ( ); dependencies = ( - 69DCF4A02AB8C9A500CBE2CC /* PBXTargetDependency */, ); name = QuickDropShareExtension; productName = ShareExtension; @@ -488,37 +471,17 @@ ); dependencies = ( 699DEBAD2AB0573200115D22 /* PBXTargetDependency */, - 69DCF4972AB8BF7B00CBE2CC /* PBXTargetDependency */, ); name = QuickDrop; packageProductDependencies = ( EB2F9BFD2DA05F48004010A2 /* BezelNotification */, + EB5C37D92E377AD60052EFD7 /* SwiftProtobuf */, + EB5C37DB2E377AD60052EFD7 /* SwiftECC */, ); productName = NearDrop; productReference = 69DA9A0E29E0BF5100A442DA /* QuickDrop.app */; productType = "com.apple.product-type.application"; }; - 69DCF4802AB70D0600CBE2CC /* NearbyShare */ = { - isa = PBXNativeTarget; - buildConfigurationList = 69DCF4892AB70D0600CBE2CC /* Build configuration list for PBXNativeTarget "NearbyShare" */; - buildPhases = ( - 69DCF47D2AB70D0600CBE2CC /* Headers */, - 69DCF47E2AB70D0600CBE2CC /* Sources */, - 69DCF47F2AB70D0600CBE2CC /* Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = NearbyShare; - packageProductDependencies = ( - 69DCF4992AB8C58500CBE2CC /* SwiftProtobuf */, - 69DCF49B2AB8C58500CBE2CC /* SwiftECC */, - ); - productName = NearbyShare; - productReference = 69DCF4812AB70D0600CBE2CC /* libNearbyShare.dylib */; - productType = "com.apple.product-type.library.dynamic"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -536,9 +499,6 @@ CreatedOnToolsVersion = 14.3; LastSwiftMigration = 1430; }; - 69DCF4802AB70D0600CBE2CC = { - CreatedOnToolsVersion = 14.3.1; - }; }; }; buildConfigurationList = 69DA9A0929E0BF5100A442DA /* Build configuration list for PBXProject "QuickDrop" */; @@ -577,7 +537,6 @@ targets = ( 69DA9A0D29E0BF5100A442DA /* QuickDrop */, 699DEBA02AB0573200115D22 /* QuickDropShareExtension */, - 69DCF4802AB70D0600CBE2CC /* NearbyShare */, ); }; /* End PBXProject section */ @@ -623,10 +582,25 @@ EB96ED8B2D7F7CF7004B3FF1 /* SmallSheetView.swift in Sources */, EBCC27F92D80C9F90079C478 /* Zip.swift in Sources */, EB7CD1462D6FB0E800CB0CB5 /* Logger.swift in Sources */, + EB5C37C52E377A780052EFD7 /* securemessage.pb.swift in Sources */, + EB5C37C62E377A780052EFD7 /* securegcm.pb.swift in Sources */, + EB5C37C72E377A780052EFD7 /* wire_format.pb.swift in Sources */, + EB5C37C82E377A780052EFD7 /* ukey.pb.swift in Sources */, + EB5C37C92E377A780052EFD7 /* device_to_device_messages.pb.swift in Sources */, + EB5C37CA2E377A780052EFD7 /* offline_wire_formats.pb.swift in Sources */, EBC05C5C2E3394EF00EECC18 /* ErrorAlertHandler.swift in Sources */, EBC05C7B2E33B58100EECC18 /* ActiveNetworkInterface.swift in Sources */, EBC05C7C2E33B58100EECC18 /* ApIsolationTester.swift in Sources */, 691F53BB2ABB70840089FD92 /* DeviceListCell.swift in Sources */, + EB5C37BB2E377A700052EFD7 /* ConnectionFailureTracker.swift in Sources */, + EB5C37BC2E377A700052EFD7 /* SleepManager.swift in Sources */, + EB5C37D02E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */, + EB5C37D12E377A7F0052EFD7 /* DataModel.swift in Sources */, + EB5C37D22E377A7F0052EFD7 /* NearbyConnection.swift in Sources */, + EB5C37D32E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */, + EB5C37D42E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */, + EB5C37BD2E377A700052EFD7 /* Helpers.swift in Sources */, + EB5C37BE2E377A700052EFD7 /* SaveFilesManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -636,11 +610,26 @@ files = ( EBD2A2172D2860C100BCB8E2 /* WelcomeScreen.swift in Sources */, EB3643602D73A2A2009CDFF8 /* AudioManager.swift in Sources */, + EB5C37B72E377A6F0052EFD7 /* ConnectionFailureTracker.swift in Sources */, + EB5C37B82E377A6F0052EFD7 /* SleepManager.swift in Sources */, + EB5C37B92E377A6F0052EFD7 /* SaveFilesManager.swift in Sources */, + EB5C37BA2E377A6F0052EFD7 /* Helpers.swift in Sources */, EBC05C5B2E33937200EECC18 /* ErrorAlertHandler.swift in Sources */, EBC05C712E33B57D00EECC18 /* Converter.swift in Sources */, EBC05C722E33B57D00EECC18 /* Helper.swift in Sources */, EBC05C732E33B57D00EECC18 /* PlusView.swift in Sources */, + EB5C37BF2E377A780052EFD7 /* ukey.pb.swift in Sources */, + EB5C37C02E377A780052EFD7 /* securemessage.pb.swift in Sources */, + EB5C37C12E377A780052EFD7 /* offline_wire_formats.pb.swift in Sources */, + EB5C37C22E377A780052EFD7 /* device_to_device_messages.pb.swift in Sources */, + EB5C37C32E377A780052EFD7 /* wire_format.pb.swift in Sources */, + EB5C37C42E377A780052EFD7 /* securegcm.pb.swift in Sources */, EBC05C782E33B58100EECC18 /* ActiveNetworkInterface.swift in Sources */, + EB5C37CB2E377A7F0052EFD7 /* NearbyConnection.swift in Sources */, + EB5C37CC2E377A7F0052EFD7 /* DataModel.swift in Sources */, + EB5C37CD2E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */, + EB5C37CE2E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */, + EB5C37CF2E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */, EBC05C792E33B58100EECC18 /* ApIsolation6Tester.swift in Sources */, EBC05C7A2E33B58100EECC18 /* ApIsolationTester.swift in Sources */, EB96ED8C2D7F7E7B004B3FF1 /* SmallSheetView.swift in Sources */, @@ -657,30 +646,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 69DCF47E2AB70D0600CBE2CC /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 69DCF48D2AB70E8C00CBE2CC /* offline_wire_formats.pb.swift in Sources */, - EBC05C592E32C22200EECC18 /* Localized.swift in Sources */, - EB2F9BFB2DA04B69004010A2 /* Helpers.swift in Sources */, - 69DCF48F2AB70E8C00CBE2CC /* securemessage.pb.swift in Sources */, - 69DCF4912AB70E9700CBE2CC /* NearbyConnection.swift in Sources */, - EB6E743E2DD5C77700331466 /* ConnectionFailureTracker.swift in Sources */, - 69DCF4922AB70E9700CBE2CC /* NearbyConnectionManager.swift in Sources */, - 69DCF48E2AB70E8C00CBE2CC /* securegcm.pb.swift in Sources */, - 691F53BE2ABF03820089FD92 /* OutboundNearbyConnection.swift in Sources */, - 69DCF48C2AB70E8C00CBE2CC /* device_to_device_messages.pb.swift in Sources */, - EB7CD1472D6FB0E800CB0CB5 /* Logger.swift in Sources */, - 69DCF4902AB70E9700CBE2CC /* InboundNearbyConnection.swift in Sources */, - 69DCF48B2AB70E8C00CBE2CC /* ukey.pb.swift in Sources */, - EB2B41E42DC63ACC0070FC39 /* SleepManager.swift in Sources */, - EB5C36E92E3546120052EFD7 /* DataModel.swift in Sources */, - EB5C36E72E3544FF0052EFD7 /* SaveFilesManager.swift in Sources */, - 69DCF48A2AB70E8C00CBE2CC /* wire_format.pb.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -689,16 +654,6 @@ target = 699DEBA02AB0573200115D22 /* QuickDropShareExtension */; targetProxy = 699DEBAC2AB0573200115D22 /* PBXContainerItemProxy */; }; - 69DCF4972AB8BF7B00CBE2CC /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 69DCF4802AB70D0600CBE2CC /* NearbyShare */; - targetProxy = 69DCF4962AB8BF7B00CBE2CC /* PBXContainerItemProxy */; - }; - 69DCF4A02AB8C9A500CBE2CC /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 69DCF4802AB70D0600CBE2CC /* NearbyShare */; - targetProxy = 69DCF49F2AB8C9A500CBE2CC /* PBXContainerItemProxy */; - }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -990,40 +945,6 @@ }; name = Release; }; - 69DCF4872AB70D0600CBE2CC /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - DEAD_CODE_STRIPPING = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@executable_path/../Frameworks"; - EXECUTABLE_PREFIX = lib; - LD_DYLIB_INSTALL_NAME = "$(DYLIB_INSTALL_NAME_BASE:standardizepath)/$(EXECUTABLE_PATH)"; - MACOSX_DEPLOYMENT_TARGET = 11.0; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - 69DCF4882AB70D0600CBE2CC /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - DEAD_CODE_STRIPPING = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@executable_path/../Frameworks"; - EXECUTABLE_PREFIX = lib; - LD_DYLIB_INSTALL_NAME = "$(DYLIB_INSTALL_NAME_BASE:standardizepath)/$(EXECUTABLE_PATH)"; - MACOSX_DEPLOYMENT_TARGET = 11.0; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -1054,15 +975,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 69DCF4892AB70D0600CBE2CC /* Build configuration list for PBXNativeTarget "NearbyShare" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 69DCF4872AB70D0600CBE2CC /* Debug */, - 69DCF4882AB70D0600CBE2CC /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ @@ -1093,21 +1005,31 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 69DCF4992AB8C58500CBE2CC /* SwiftProtobuf */ = { - isa = XCSwiftPackageProductDependency; - package = 69DA9A2429E189EF00A442DA /* XCRemoteSwiftPackageReference "swift-protobuf" */; - productName = SwiftProtobuf; - }; - 69DCF49B2AB8C58500CBE2CC /* SwiftECC */ = { - isa = XCSwiftPackageProductDependency; - package = 69DA9A3429E1994C00A442DA /* XCRemoteSwiftPackageReference "SwiftECC" */; - productName = SwiftECC; - }; EB2F9BFD2DA05F48004010A2 /* BezelNotification */ = { isa = XCSwiftPackageProductDependency; package = EB2F9BFC2DA05F48004010A2 /* XCRemoteSwiftPackageReference "BezelNotification" */; productName = BezelNotification; }; + EB5C37D52E377ACF0052EFD7 /* SwiftProtobuf */ = { + isa = XCSwiftPackageProductDependency; + package = 69DA9A2429E189EF00A442DA /* XCRemoteSwiftPackageReference "swift-protobuf" */; + productName = SwiftProtobuf; + }; + EB5C37D72E377ACF0052EFD7 /* SwiftECC */ = { + isa = XCSwiftPackageProductDependency; + package = 69DA9A3429E1994C00A442DA /* XCRemoteSwiftPackageReference "SwiftECC" */; + productName = SwiftECC; + }; + EB5C37D92E377AD60052EFD7 /* SwiftProtobuf */ = { + isa = XCSwiftPackageProductDependency; + package = 69DA9A2429E189EF00A442DA /* XCRemoteSwiftPackageReference "swift-protobuf" */; + productName = SwiftProtobuf; + }; + EB5C37DB2E377AD60052EFD7 /* SwiftECC */ = { + isa = XCSwiftPackageProductDependency; + package = 69DA9A3429E1994C00A442DA /* XCRemoteSwiftPackageReference "SwiftECC" */; + productName = SwiftECC; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 69DA9A0629E0BF5100A442DA /* Project object */; diff --git a/QuickDrop/AppDelegate.swift b/QuickDrop/AppDelegate.swift index 46b70c9..7f47ed1 100644 --- a/QuickDrop/AppDelegate.swift +++ b/QuickDrop/AppDelegate.swift @@ -8,7 +8,6 @@ import AudioToolbox import BezelNotification import Cocoa -import NearbyShare import Network import StoreKit import SwiftUI diff --git a/QuickDrop/Plus/PlusView.swift b/QuickDrop/Plus/PlusView.swift index 6858df0..41e674b 100644 --- a/QuickDrop/Plus/PlusView.swift +++ b/QuickDrop/Plus/PlusView.swift @@ -7,7 +7,6 @@ import SwiftUI import StoreKit -import NearbyShare let plusViewWidth: CGFloat = 440 let plusViewHeight: CGFloat = 200 diff --git a/QuickDrop/Util/ErrorAlertHandler.swift b/QuickDrop/Util/ErrorAlertHandler.swift index 78ebc96..4bedf22 100644 --- a/QuickDrop/Util/ErrorAlertHandler.swift +++ b/QuickDrop/Util/ErrorAlertHandler.swift @@ -8,7 +8,6 @@ import AudioToolbox import BezelNotification import Cocoa -import NearbyShare import Network import StoreKit import SwiftUI diff --git a/QuickDrop/View/SettingsView.swift b/QuickDrop/View/SettingsView.swift index 8e6bade..f301601 100644 --- a/QuickDrop/View/SettingsView.swift +++ b/QuickDrop/View/SettingsView.swift @@ -7,7 +7,6 @@ import SwiftUI import AppKit -import NearbyShare struct SettingsView: View { diff --git a/QuickDrop/View/TutorialView.swift b/QuickDrop/View/TutorialView.swift index 2494dd1..34970fd 100644 --- a/QuickDrop/View/TutorialView.swift +++ b/QuickDrop/View/TutorialView.swift @@ -6,7 +6,6 @@ // import SwiftUI -import NearbyShare struct TutorialView: View { diff --git a/QuickDrop/View/WelcomeScreen.swift b/QuickDrop/View/WelcomeScreen.swift index 0dd95aa..e0fd570 100644 --- a/QuickDrop/View/WelcomeScreen.swift +++ b/QuickDrop/View/WelcomeScreen.swift @@ -6,7 +6,6 @@ // import SwiftUI -import NearbyShare import UniformTypeIdentifiers struct WelcomeScreen: View { diff --git a/ShareExtension/ShareViewController.swift b/ShareExtension/ShareViewController.swift index a5fce8f..ffa358a 100644 --- a/ShareExtension/ShareViewController.swift +++ b/ShareExtension/ShareViewController.swift @@ -7,7 +7,6 @@ import Cocoa import Foundation -import NearbyShare import SwiftUI class ShareViewController: NSViewController, ShareExtensionDelegate {