diff --git a/NearbyShare/Connection/NearbyConnection.swift b/NearbyShare/Connection/NearbyConnection.swift index 70f899d..b7f5fcc 100644 --- a/NearbyShare/Connection/NearbyConnection.swift +++ b/NearbyShare/Connection/NearbyConnection.swift @@ -57,6 +57,7 @@ class NearbyConnection { private var clientSeq: Int32 = 0 private(set) var pinCode: String? + private(set) var authKey: SymmetricKey? init(connection: NWConnection, id: String) { @@ -471,6 +472,7 @@ class NearbyConnection { 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) + authKey = authString pinCode = NearbyConnection.pinCodeFromAuthKey(authString) let salt = Data([0x82, 0xAA, 0x55, 0xA0, 0xD3, 0x97, 0xF8, 0x83, 0x46, 0xCA, 0x1C, diff --git a/NearbyShare/Connection/NearbyConnectionManager.swift b/NearbyShare/Connection/NearbyConnectionManager.swift index 2ebfe39..84f927b 100644 --- a/NearbyShare/Connection/NearbyConnectionManager.swift +++ b/NearbyShare/Connection/NearbyConnectionManager.swift @@ -5,14 +5,16 @@ // Created by Grishka on 08.04.2023. // -import Foundation -import Network -import System - #if os(iOS) import UIKit #endif +import Foundation +import Network +import System +import SwiftECC +import CryptoKit + public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearbyConnectionDelegate, OutboundNearbyConnectionDelegate { private let sleepManager = SleepManager.shared @@ -25,6 +27,11 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb private var startedDeviceDiscovery = false private var browsers: [NWBrowser] = [] private let serviceTypes = ["_FC9F5ED42C8A._tcp."] + private var qrCodePublicKey: ECPublicKey? + private var qrCodePrivateKey: ECPrivateKey? + private var qrCodeAdvertisingToken: Data? + private var qrCodeNameEncryptionKey: SymmetricKey? + private var qrCodeData: Data? public let endpointID: [UInt8] = generateEndpointID() public var mainAppDelegate: (any MainAppDelegate)? @@ -109,7 +116,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb func getEndpointInfo() -> EndpointInfo { #if os(macOS) - let endpointInfo = EndpointInfo(name: Host.current().localizedName!, deviceType: .computer) + let endpointInfo = EndpointInfo(name: Host.current().localizedName ?? "Mac", deviceType: .computer) #else let endpointInfo = EndpointInfo(name: UIDevice.current.name, deviceType: .phone) #endif @@ -230,22 +237,56 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb } 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) + guard let endpointInfoData = Data.dataFromUrlSafeBase64(endpointInfoEncoded) else { return } + guard var endpointInfo = EndpointInfo(data: endpointInfoData) else { return } + var deviceInfo: RemoteDeviceInfo? + + if let _ = endpointInfo.name { + deviceInfo = addFoundDevice(foundService: &foundService, endpointInfo: endpointInfo, endpointID: endpointID) + } + + if let qrData = endpointInfo.qrCodeData, let qrCodeAdvertisingToken = qrCodeAdvertisingToken, let qrCodeNameEncryptionKey = qrCodeNameEncryptionKey { + + log("[NearbyConnectionManager] Device has QR data: \(qrData.base64EncodedString()), advertising token is \(qrCodeAdvertisingToken.base64EncodedString())") + + if qrData == qrCodeAdvertisingToken { + if let deviceInfo = deviceInfo { + for delegate in shareExtensionDelegates { + delegate.startTransferWithQrCode(device: deviceInfo) + } + } + } + else if qrData.count > 28 { + do { + let box = try AES.GCM.SealedBox(combined: qrData) + let decryptedName = try AES.GCM.open(box, using: qrCodeNameEncryptionKey, authenticating: qrCodeAdvertisingToken) + guard let name = String.init(data: decryptedName, encoding: .utf8) else { return } + endpointInfo.name = name + let deviceInfo = addFoundDevice(foundService: &foundService, endpointInfo: endpointInfo, endpointID: endpointID) + for delegate in shareExtensionDelegates { + delegate.startTransferWithQrCode(device: deviceInfo) + } + } catch { + log("[NearbyConnectionManager] Error decrypting QR code data of an invisible device: \(error)") + } + } + } + } + + + private func addFoundDevice(foundService: inout FoundServiceInfo, endpointInfo: EndpointInfo, endpointID: String) -> RemoteDeviceInfo { + let deviceInfo = RemoteDeviceInfo(info: endpointInfo, id: endpointID) foundService.device = deviceInfo foundServices[endpointID] = foundService + for delegate in shareExtensionDelegates { delegate.addDevice(device: deviceInfo) } + + return deviceInfo } @@ -256,6 +297,35 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb delegate.removeDevice(id: endpointID) } } + + + public func generateQrCodeKey() -> String{ + let domain = Domain.instance(curve: .EC256r1) + let (pubKey, privKey) = domain.makeKeyPair() + qrCodePublicKey = pubKey + qrCodePrivateKey = privKey + var keyData = Data() + keyData.append(contentsOf: [0, 0, 2]) + let keyBytes = Data(pubKey.w.x.asSignedBytes()) + // Sometimes, for some keys, there will be a leading zero byte. Strip that, Android really hates it (it breaks the endpoint info) + keyData.append(keyBytes.suffixOfAtMost(numBytes: 32)) + + let ikm = SymmetricKey(data: keyData) + qrCodeAdvertisingToken = NearbyConnection.hkdf(inputKeyMaterial: ikm, salt: Data(), info: "advertisingContext".data(using: .utf8)!, outputByteCount: 16).data() + qrCodeNameEncryptionKey = NearbyConnection.hkdf(inputKeyMaterial: ikm, salt: Data(), info: "encryptionKey".data(using: .utf8)!, outputByteCount: 16) + qrCodeData = keyData + + return keyData.urlSafeBase64EncodedString() + } + + + public func clearQrCodeKey(){ + qrCodePublicKey = nil + qrCodePrivateKey = nil + qrCodeAdvertisingToken = nil + qrCodeNameEncryptionKey = nil + qrCodeData = nil + } public func startOutgoingTransfer(deviceID: String, delegate: ShareExtensionDelegate, urls: [URL], textToSend: String?) { @@ -265,6 +335,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb 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 + conn.qrCodePrivateKey = qrCodePrivateKey let transfer = OutgoingTransferInfo(service: info.service, device: info.device!, connection: conn, delegate: delegate) outgoingTransfers[deviceID] = transfer conn.start() diff --git a/NearbyShare/Connection/OutboundNearbyConnection.swift b/NearbyShare/Connection/OutboundNearbyConnection.swift index d6c1dd8..039f752 100644 --- a/NearbyShare/Connection/OutboundNearbyConnection.swift +++ b/NearbyShare/Connection/OutboundNearbyConnection.swift @@ -26,6 +26,7 @@ class OutboundNearbyConnection: NearbyConnection { private var totalBytesSent: Int64 = 0 private var textPayloadID: Int64 = 0 + public var qrCodePrivateKey: ECPrivateKey? public var delegate: OutboundNearbyConnectionDelegate? enum State { @@ -174,7 +175,7 @@ class OutboundNearbyConnection: NearbyConnection { 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 = endpointInfo.name + frame.v1.connectionRequest.endpointName = endpointInfo.name ?? "QuickDrop" frame.v1.connectionRequest.endpointInfo = endpointInfo.serialize() frame.v1.connectionRequest.mediums = [.wifiLan] try sendFrameAsync(frame.serializedData()) @@ -277,6 +278,14 @@ class OutboundNearbyConnection: NearbyConnection { pairedEncryption.v1.pairedKeyEncryption = Sharing_Nearby_PairedKeyEncryptionFrame() pairedEncryption.v1.pairedKeyEncryption.secretIDHash = Data.randomData(length: 6) pairedEncryption.v1.pairedKeyEncryption.signedData = Data.randomData(length: 72) + + if let qrKey = qrCodePrivateKey, let authKey = authKey { + let signature = qrKey.sign(msg: authKey.data()) + var serializedSignature = Data(signature.r) + serializedSignature.append(Data(signature.s)) + pairedEncryption.v1.pairedKeyEncryption.qrCodeHandshakeData = serializedSignature + } + try sendTransferSetupFrame(pairedEncryption) currentState = .sentPairedKeyEncryption diff --git a/NearbyShare/Model/DataModel.swift b/NearbyShare/Model/DataModel.swift index 4586d21..ec84d14 100644 --- a/NearbyShare/Model/DataModel.swift +++ b/NearbyShare/Model/DataModel.swift @@ -7,6 +7,8 @@ import Foundation import Network +import CryptoKit +import SwiftECC protocol InboundNearbyConnectionDelegate { @@ -36,6 +38,7 @@ public protocol MainAppDelegate { public protocol ShareExtensionDelegate: AnyObject { func addDevice(device: RemoteDeviceInfo) func removeDevice(id: String) + func startTransferWithQrCode(device: RemoteDeviceInfo) func connectionWasEstablished(pinCode: String) func connectionFailed(with error: Error) func transferAccepted() @@ -45,19 +48,23 @@ public protocol ShareExtensionDelegate: AnyObject { public struct RemoteDeviceInfo: Identifiable, Equatable { - public let name: String + public let name: String? public let type: DeviceType + public let qrCodeData:Data? public var id: String? init(name: String, type: DeviceType, id: String? = nil) { self.name = name self.type = type self.id = id + self.qrCodeData = nil } - init(info: EndpointInfo) { - name = info.name - type = info.deviceType + init(info: EndpointInfo, id: String? = nil) { + self.name = info.name + self.type = info.deviceType + self.qrCodeData = info.qrCodeData + self.id = id } @@ -116,24 +123,63 @@ public struct FileMetadata { struct EndpointInfo { - let name: String + var name: String? let deviceType: RemoteDeviceInfo.DeviceType + let qrCodeData: Data? init(name: String, deviceType: RemoteDeviceInfo.DeviceType) { self.name = name self.deviceType = deviceType + self.qrCodeData = nil } 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) + + let hasName = (data[0] & 0x10) == 0 + let deviceNameLength: Int + let deviceName: String? + + if hasName { + deviceNameLength = Int(data[17]) + guard data.count >= deviceNameLength + 18 else { return nil } + guard let newDeviceName = String(data: data[18..<(18 + deviceNameLength)], encoding: .utf8) else { return nil } + deviceName = newDeviceName + } + else { + deviceNameLength = 0 + deviceName = nil + } + + let rawDeviceType: Int = Int(data[0] & 7) >> 1 + self.name = deviceName + self.deviceType = RemoteDeviceInfo.DeviceType.fromRawValue(value: rawDeviceType) + var offset = 1 + 16 + + if hasName { + offset = offset + 1 + deviceNameLength + } + + var qrCodeData: Data? = nil + + // read TLV records, if any + while data.count - offset > 2 { + let type = data[offset] + let length = Int(data[offset+1]) + offset = offset + 2 + if data.count - offset >= length{ + // QR code data + if type == 1 { + qrCodeData = data.subdata(in: offset.. 255 { nameChars = [UInt8](nameChars[0 ..< 255]) } diff --git a/NearbyShare/Util/Helpers.swift b/NearbyShare/Util/Helpers.swift index 4ac8d26..8d115e8 100644 --- a/NearbyShare/Util/Helpers.swift +++ b/NearbyShare/Util/Helpers.swift @@ -6,6 +6,7 @@ // import Foundation +import CryptoKit extension Data { func urlSafeBase64EncodedString() -> String { @@ -20,7 +21,7 @@ extension Data { }) } - + static func randomData(length: Int) -> Data { var data = Data(count: length) data.withUnsafeMutableBytes { @@ -29,7 +30,16 @@ extension Data { return data } - + + func suffixOfAtMost(numBytes: Int) -> Data{ + if count <= numBytes { + return self + } + + return subdata(in: count-numBytes.. Data? { var regularB64 = String(str.map { if $0 == "_" { @@ -53,6 +63,13 @@ extension Data { } +extension SymmetricKey{ + func data() -> Data{ + return withUnsafeBytes({return Data(bytes: $0.baseAddress!, count: $0.count)}) + } +} + + extension String { var dataFromHex: Data { var data = Data(capacity: count / 2) diff --git a/QuickDrop.xcodeproj/project.pbxproj b/QuickDrop.xcodeproj/project.pbxproj index fabd30c..533431a 100644 --- a/QuickDrop.xcodeproj/project.pbxproj +++ b/QuickDrop.xcodeproj/project.pbxproj @@ -95,6 +95,9 @@ EB9925802E45D53E00357055 /* sharing_enums.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB99257F2E45D53E00357055 /* sharing_enums.pb.swift */; }; EB9925812E45D53E00357055 /* sharing_enums.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB99257F2E45D53E00357055 /* sharing_enums.pb.swift */; }; EB9925822E45D53E00357055 /* sharing_enums.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB99257F2E45D53E00357055 /* sharing_enums.pb.swift */; }; + EB9925852E45EF0900357055 /* QRCode in Frameworks */ = {isa = PBXBuildFile; productRef = EB9925842E45EF0900357055 /* QRCode */; }; + EB9925872E45F12F00357055 /* QRCode in Frameworks */ = {isa = PBXBuildFile; productRef = EB9925862E45F12F00357055 /* QRCode */; }; + EB9925892E45F13500357055 /* QRCode in Frameworks */ = {isa = PBXBuildFile; productRef = EB9925882E45F13500357055 /* QRCode */; }; 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 */; }; @@ -243,6 +246,7 @@ files = ( EB5C37D82E377ACF0052EFD7 /* SwiftECC in Frameworks */, EB5C37D62E377ACF0052EFD7 /* SwiftProtobuf in Frameworks */, + EB9925852E45EF0900357055 /* QRCode in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -250,6 +254,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + EB9925872E45F12F00357055 /* QRCode in Frameworks */, EB2F9BFE2DA05F48004010A2 /* BezelNotification in Frameworks */, EB5C37DC2E377AD60052EFD7 /* SwiftECC in Frameworks */, EB5C37DA2E377AD60052EFD7 /* SwiftProtobuf in Frameworks */, @@ -262,6 +267,7 @@ files = ( EB5C37F02E377C3F0052EFD7 /* SwiftECC in Frameworks */, EB5C37EE2E377C3F0052EFD7 /* SwiftProtobuf in Frameworks */, + EB9925892E45F13500357055 /* QRCode in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -509,6 +515,7 @@ EB2F9BFD2DA05F48004010A2 /* BezelNotification */, EB5C37D92E377AD60052EFD7 /* SwiftProtobuf */, EB5C37DB2E377AD60052EFD7 /* SwiftECC */, + EB9925862E45F12F00357055 /* QRCode */, ); productName = NearDrop; productReference = 69DA9A0E29E0BF5100A442DA /* QuickDrop.app */; @@ -533,6 +540,7 @@ packageProductDependencies = ( EB5C37ED2E377C3F0052EFD7 /* SwiftProtobuf */, EB5C37EF2E377C3F0052EFD7 /* SwiftECC */, + EB9925882E45F13500357055 /* QRCode */, ); productName = QuickDropiOS; productReference = EB5C37E22E377C2E0052EFD7 /* QuickDropiOS.app */; @@ -589,6 +597,7 @@ 69DA9A2429E189EF00A442DA /* XCRemoteSwiftPackageReference "swift-protobuf" */, 69DA9A3429E1994C00A442DA /* XCRemoteSwiftPackageReference "SwiftECC" */, EB2F9BFC2DA05F48004010A2 /* XCRemoteSwiftPackageReference "BezelNotification" */, + EB9925832E45EF0900357055 /* XCRemoteSwiftPackageReference "qrcode" */, ); productRefGroup = 69DA9A0F29E0BF5100A442DA /* Products */; projectDirPath = ""; @@ -1183,6 +1192,14 @@ minimumVersion = 2.1.0; }; }; + EB9925832E45EF0900357055 /* XCRemoteSwiftPackageReference "qrcode" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/dagronf/qrcode.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 26.10.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -1221,6 +1238,21 @@ package = 69DA9A3429E1994C00A442DA /* XCRemoteSwiftPackageReference "SwiftECC" */; productName = SwiftECC; }; + EB9925842E45EF0900357055 /* QRCode */ = { + isa = XCSwiftPackageProductDependency; + package = EB9925832E45EF0900357055 /* XCRemoteSwiftPackageReference "qrcode" */; + productName = QRCode; + }; + EB9925862E45F12F00357055 /* QRCode */ = { + isa = XCSwiftPackageProductDependency; + package = EB9925832E45EF0900357055 /* XCRemoteSwiftPackageReference "qrcode" */; + productName = QRCode; + }; + EB9925882E45F13500357055 /* QRCode */ = { + isa = XCSwiftPackageProductDependency; + package = EB9925832E45EF0900357055 /* XCRemoteSwiftPackageReference "qrcode" */; + productName = QRCode; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 69DA9A0629E0BF5100A442DA /* Project object */; diff --git a/QuickDrop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/QuickDrop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 364f54f..da47079 100644 --- a/QuickDrop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/QuickDrop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "c998f2b1c34ecbb24a88c5711f17c1a351f48f3505ccd4cab4552e5760bd39a3", + "originHash" : "10744462d42c114b38e0002c6a341b37abcba576ba3203c8d951905fbbdf1f58", "pins" : [ { "identity" : "asn1", @@ -28,6 +28,15 @@ "version" : "1.21.0" } }, + { + "identity" : "qrcode", + "kind" : "remoteSourceControl", + "location" : "https://github.com/dagronf/qrcode.git", + "state" : { + "revision" : "177d4091f7fcce1efc22de2a5fcdb671bd103101", + "version" : "26.10.0" + } + }, { "identity" : "swift-cross-kit-types", "kind" : "remoteSourceControl", @@ -55,6 +64,15 @@ "version" : "1.30.0" } }, + { + "identity" : "swift-qrcode-generator", + "kind" : "remoteSourceControl", + "location" : "https://github.com/dagronf/swift-qrcode-generator", + "state" : { + "revision" : "2b1980b825f08a81ccc762b0c4d17fcde9d5e953", + "version" : "2.0.2" + } + }, { "identity" : "swiftecc", "kind" : "remoteSourceControl", @@ -63,6 +81,15 @@ "revision" : "2b672501e3a9361316898aa2d5b0e684c992e01e", "version" : "3.9.0" } + }, + { + "identity" : "swiftimagereadwrite", + "kind" : "remoteSourceControl", + "location" : "https://github.com/dagronf/SwiftImageReadWrite", + "state" : { + "revision" : "42ace2412279f18bc264bc306e96b51c36e12a33", + "version" : "1.9.2" + } } ], "version" : 3 diff --git a/QuickDrop/AppDelegate.swift b/QuickDrop/AppDelegate.swift index 1ad51a1..debe4e6 100644 --- a/QuickDrop/AppDelegate.swift +++ b/QuickDrop/AppDelegate.swift @@ -355,16 +355,17 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele let mainMessage: String + let name = device.name ?? "Android" switch transfer.type { case .file: - mainMessage = String(format: (acceptAutomatically ? "DeviceCurrentlySendingFiles" : "DeviceSendingFiles").localized(), arguments: [device.name, fileStr]) + mainMessage = String(format: (acceptAutomatically ? "DeviceCurrentlySendingFiles" : "DeviceSendingFiles").localized(), arguments: [name, fileStr]) case .text: - mainMessage = String(format: (acceptAutomatically ? "DeviceCurrentlySendingText" : "DeviceSendingText").localized(), arguments: [device.name, fileStr]) + mainMessage = String(format: (acceptAutomatically ? "DeviceCurrentlySendingText" : "DeviceSendingText").localized(), arguments: [name, fileStr]) case .url: - mainMessage = String(format: (acceptAutomatically ? "DeviceCurrentlySendingUrl" : "DeviceSendingUrl").localized(), arguments: [device.name, fileStr]) + mainMessage = String(format: (acceptAutomatically ? "DeviceCurrentlySendingUrl" : "DeviceSendingUrl").localized(), arguments: [name, fileStr]) } diff --git a/QuickDrop/Assets.xcassets/QRBackground.imageset/Contents.json b/QuickDrop/Assets.xcassets/QRBackground.imageset/Contents.json new file mode 100644 index 0000000..46177de --- /dev/null +++ b/QuickDrop/Assets.xcassets/QRBackground.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "QRBackground.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/QuickDrop/Assets.xcassets/QRBackground.imageset/QRBackground.png b/QuickDrop/Assets.xcassets/QRBackground.imageset/QRBackground.png new file mode 100644 index 0000000..055818f Binary files /dev/null and b/QuickDrop/Assets.xcassets/QRBackground.imageset/QRBackground.png differ diff --git a/ShareExtension/ShareViewController.swift b/ShareExtension/ShareViewController.swift index 5553a68..4172962 100644 --- a/ShareExtension/ShareViewController.swift +++ b/ShareExtension/ShareViewController.swift @@ -8,8 +8,10 @@ import Cocoa import Foundation import SwiftUI +import QRCode class ShareViewController: NSViewController, ShareExtensionDelegate { + private var urls: [URL] = [] private var textToSend: String? = nil private var foundDevices: [RemoteDeviceInfo] = [] @@ -154,7 +156,27 @@ class ShareViewController: NSViewController, ShareExtensionDelegate { private func openQrCodeView() { if qrCodeSheetView == nil { - let contentView = SmallSheetView(type: .sendToDeviceQrCode) { + + var image: Image? = nil + + do { + let qrKey = NearbyConnectionManager.shared.generateQrCodeKey() + let qrCodeImage = try QRCode.build + .text("https://quickshare.google/qrcode#key=\(qrKey)") + .backgroundColor(CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 0)) + .quietZonePixelCount(3) + .onPixels.shape(.circle()) + .eye.shape(.squircle()) + .errorCorrection(.low) + .generate.image(dimension: 1000) + + image = Image(nsImage: NSImage(cgImage: qrCodeImage, size: qrCodeImage.size)) + } + catch { + log("Error generating QR code: \(error)") + } + + let contentView = SmallSheetView(type: .sendToDeviceQrCode, dynamicQrCode: image) { self.closeQrCodeView() } @@ -279,6 +301,12 @@ class ShareViewController: NSViewController, ShareExtensionDelegate { } + func startTransferWithQrCode(device: RemoteDeviceInfo) { + closeQrCodeView() + selectDevice(device: device) + } + + func connectionWasEstablished(pinCode: String) { connectionEstablished = true @@ -447,9 +475,10 @@ extension ShareViewController: NSCollectionViewDataSource { func getDeviceName(device: RemoteDeviceInfo) -> String { - if device.name.count <= 1 { - return "Android" + if let name = device.name, name.count > 1 { + return name } - return device.name + + return "Android" } } diff --git a/ShareExtension/View/SmallSheetView.swift b/ShareExtension/View/SmallSheetView.swift index 87bf426..212646e 100644 --- a/ShareExtension/View/SmallSheetView.swift +++ b/ShareExtension/View/SmallSheetView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import QRCode let smallSheetViewSize = CGSize(width: 530.0, height: 270.0) @@ -13,6 +14,7 @@ struct SmallSheetView: View { @State private var qrCode: String = "" let type: SheetViewType + var dynamicQrCode: Image? = nil let closeView: () -> Void @Environment(\.colorScheme) var colorScheme @@ -85,10 +87,18 @@ struct SmallSheetView: View { HStack { Spacer() - getImage() - .resizable() - .aspectRatio(1, contentMode: .fit) - .frame(height: 160) + ZStack { + if type == .sendToDeviceQrCode && dynamicQrCode != nil { + Image(.qrBackground) + .resizable() + .aspectRatio(1, contentMode: .fit) + } + + getImage() + .resizable() + .aspectRatio(1, contentMode: .fit) + } + .frame(height: 160) Spacer() @@ -138,7 +148,7 @@ struct SmallSheetView: View { func getImage() -> Image { switch type { case .sendToDeviceQrCode: - return connectWithApp ? Image(.qrApp) : Image(.QR) + return connectWithApp ? Image(.qrApp) : (dynamicQrCode ?? Image(.QR)) case .downloadAndroidApp: return Image(.qrApp) case .downloadCableConnectionApp: @@ -156,5 +166,25 @@ enum SheetViewType { #Preview { - SmallSheetView(type: .downloadCableConnectionApp, closeView: {}) + Preview() +} + + +private struct Preview: View { + var body: some View { + SmallSheetView(type: .sendToDeviceQrCode, dynamicQrCode: getImage(), closeView: {}) + } + + func getImage() -> Image { + let qrCodeImage = try! QRCode.build + .text("https://quickshare.google/qrcode#key=AAA") + .backgroundColor(CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 0)) + .quietZonePixelCount(2) + .onPixels.shape(.circle()) + .eye.shape(.squircle()) + .errorCorrection(.low) + .generate.image(dimension: 1000) + + return Image(nsImage: NSImage(cgImage: qrCodeImage, size: qrCodeImage.size)) + } }