mirror of
https://github.com/leonboe1/QuickDrop.git
synced 2026-11-11 02:51:11 +00:00
Refactoring
This commit is contained in:
1 parent
0842559f49
commit
d2fc94df75
37 files changed
+4047
-186
No files matched your search
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<SHA256>.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<SHA256>.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<SHA256>.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<SHA256>.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<SHA256>.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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+101
-179
@@ -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 = "<group>"; };
|
||||
69DA9A2C29E18CB500A442DA /* securegcm.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = securegcm.pb.swift; sourceTree = "<group>"; };
|
||||
69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = securemessage.pb.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
EB2F9BFA2DA04B67004010A2 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = "<group>"; };
|
||||
EB36435F2D73A29F009CDFF8 /* AudioManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioManager.swift; sourceTree = "<group>"; };
|
||||
@@ -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 = "<group>";
|
||||
@@ -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 = "<group>";
|
||||
};
|
||||
EB5C37DD2E377B430052EFD7 /* Model */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EB3B73F92DC8FF3A00369951 /* ProtobufSource */,
|
||||
69DA9A2729E18CB500A442DA /* Protobuf */,
|
||||
EB5C36E82E3546100052EFD7 /* DataModel.swift */,
|
||||
);
|
||||
path = Model;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 */;
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
import AudioToolbox
|
||||
import BezelNotification
|
||||
import Cocoa
|
||||
import NearbyShare
|
||||
import Network
|
||||
import StoreKit
|
||||
import SwiftUI
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
import SwiftUI
|
||||
import StoreKit
|
||||
import NearbyShare
|
||||
|
||||
let plusViewWidth: CGFloat = 440
|
||||
let plusViewHeight: CGFloat = 200
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
import AudioToolbox
|
||||
import BezelNotification
|
||||
import Cocoa
|
||||
import NearbyShare
|
||||
import Network
|
||||
import StoreKit
|
||||
import SwiftUI
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
import SwiftUI
|
||||
import AppKit
|
||||
import NearbyShare
|
||||
|
||||
struct SettingsView: View {
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import NearbyShare
|
||||
|
||||
struct TutorialView: View {
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import NearbyShare
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
struct WelcomeScreen: View {
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
import Cocoa
|
||||
import Foundation
|
||||
import NearbyShare
|
||||
import SwiftUI
|
||||
|
||||
class ShareViewController: NSViewController, ShareExtensionDelegate {
|
||||
|
||||
Reference in new issue
Block a user