diff --git a/NearDrop.xcodeproj/project.pbxproj b/NearDrop.xcodeproj/project.pbxproj index d585859..50981c9 100644 --- a/NearDrop.xcodeproj/project.pbxproj +++ b/NearDrop.xcodeproj/project.pbxproj @@ -15,6 +15,9 @@ 691F53C72AC2594E0089FD92 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 691F53C52AC2594E0089FD92 /* Localizable.strings */; }; 691F53CB2AC2599B0089FD92 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 691F53C92AC2599B0089FD92 /* Localizable.stringsdict */; }; 693D8DC92E41325300E8E7F4 /* sharing_enums.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693D8DC82E41325300E8E7F4 /* sharing_enums.pb.swift */; }; + 693D8DCC2E43771D00E8E7F4 /* QRCode in Frameworks */ = {isa = PBXBuildFile; productRef = 693D8DCB2E43771D00E8E7F4 /* QRCode */; }; + 697DDEEC2E43986400B4749C /* SymmetricKey+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 697DDEEB2E43986400B4749C /* SymmetricKey+Extensions.swift */; }; + 697DDF0F2E4422B300B4749C /* QrCodeBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 697DDF0E2E4422B300B4749C /* QrCodeBackgroundView.swift */; }; 698DFB0329E362140064F247 /* NDNotificationCenterHackery.m in Sources */ = {isa = PBXBuildFile; fileRef = 698DFB0229E362140064F247 /* NDNotificationCenterHackery.m */; }; 699B03452AB5FBA300E0D718 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 699B03442AB5FBA300E0D718 /* Assets.xcassets */; }; 699DEBA62AB0573200115D22 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 699DEBA52AB0573200115D22 /* ShareViewController.swift */; }; @@ -148,6 +151,8 @@ 693D8DC82E41325300E8E7F4 /* sharing_enums.pb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = sharing_enums.pb.swift; sourceTree = ""; }; 6978D5392BFE97E100A6100C /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fr; path = fr.lproj/Localizable.stringsdict; sourceTree = ""; }; 6978D53A2BFE97E900A6100C /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; + 697DDEEB2E43986400B4749C /* SymmetricKey+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SymmetricKey+Extensions.swift"; sourceTree = ""; }; + 697DDF0E2E4422B300B4749C /* QrCodeBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QrCodeBackgroundView.swift; sourceTree = ""; }; 698DFAE529E2F91A0064F247 /* NearbyConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearbyConnection.swift; sourceTree = ""; }; 698DFAED29E353220064F247 /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; }; 698DFAEF29E353220064F247 /* UserNotificationsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotificationsUI.framework; path = System/Library/Frameworks/UserNotificationsUI.framework; sourceTree = SDKROOT; }; @@ -217,6 +222,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 693D8DCC2E43771D00E8E7F4 /* QRCode in Frameworks */, 69DCF49D2AB8C9A500CBE2CC /* libNearbyShare.dylib in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -254,6 +260,7 @@ isa = PBXGroup; children = ( 699DEBA52AB0573200115D22 /* ShareViewController.swift */, + 697DDF0E2E4422B300B4749C /* QrCodeBackgroundView.swift */, 691F53B92ABB70840089FD92 /* DeviceListCell.swift */, 691F53BA2ABB70840089FD92 /* DeviceListCell.xib */, 699DEBA72AB0573200115D22 /* ShareViewController.xib */, @@ -328,6 +335,7 @@ 691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */, 698DFAE529E2F91A0064F247 /* NearbyConnection.swift */, 69DA9A2029E0CC4E00A442DA /* Data+Extensions.swift */, + 697DDEEB2E43986400B4749C /* SymmetricKey+Extensions.swift */, ); path = NearbyShare; sourceTree = ""; @@ -459,6 +467,7 @@ packageReferences = ( 69DA9A2429E189EF00A442DA /* XCRemoteSwiftPackageReference "swift-protobuf" */, 69DA9A3429E1994C00A442DA /* XCRemoteSwiftPackageReference "SwiftECC" */, + 693D8DCA2E43771D00E8E7F4 /* XCRemoteSwiftPackageReference "qrcode" */, ); productRefGroup = 69DA9A0F29E0BF5100A442DA /* Products */; projectDirPath = ""; @@ -505,6 +514,7 @@ buildActionMask = 2147483647; files = ( 699DEBA62AB0573200115D22 /* ShareViewController.swift in Sources */, + 697DDF0F2E4422B300B4749C /* QrCodeBackgroundView.swift in Sources */, 691F53BB2ABB70840089FD92 /* DeviceListCell.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -533,6 +543,7 @@ 69DCF4902AB70E9700CBE2CC /* InboundNearbyConnection.swift in Sources */, 69DCF48B2AB70E8C00CBE2CC /* ukey.pb.swift in Sources */, 69DCF48A2AB70E8C00CBE2CC /* wire_format.pb.swift in Sources */, + 697DDEEC2E43986400B4749C /* SymmetricKey+Extensions.swift in Sources */, 69DCF4932AB70E9700CBE2CC /* Data+Extensions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -683,7 +694,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 7; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = NearDrop; @@ -694,7 +705,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 2.0.2; + MARKETING_VERSION = 2.2.0; PRODUCT_BUNDLE_IDENTIFIER = me.grishka.NearDrop.ShareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -709,7 +720,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 7; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = NearDrop; @@ -720,7 +731,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 2.0.2; + MARKETING_VERSION = 2.2.0; PRODUCT_BUNDLE_IDENTIFIER = me.grishka.NearDrop.ShareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -852,9 +863,12 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = NearDrop/NearDrop.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; + DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSUIElement = YES; @@ -866,9 +880,10 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 2.1.0; + MARKETING_VERSION = 2.2.0; PRODUCT_BUNDLE_IDENTIFIER = me.grishka.NearDrop; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "NearDrop/NearDrop-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -884,9 +899,12 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = NearDrop/NearDrop.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; + DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSUIElement = YES; @@ -898,9 +916,10 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.15; - MARKETING_VERSION = 2.1.0; + MARKETING_VERSION = 2.2.0; PRODUCT_BUNDLE_IDENTIFIER = me.grishka.NearDrop; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "NearDrop/NearDrop-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -981,6 +1000,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 693D8DCA2E43771D00E8E7F4 /* XCRemoteSwiftPackageReference "qrcode" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/dagronf/qrcode.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 26.10.0; + }; + }; 69DA9A2429E189EF00A442DA /* XCRemoteSwiftPackageReference "swift-protobuf" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/apple/swift-protobuf.git"; @@ -1000,6 +1027,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 693D8DCB2E43771D00E8E7F4 /* QRCode */ = { + isa = XCSwiftPackageProductDependency; + package = 693D8DCA2E43771D00E8E7F4 /* XCRemoteSwiftPackageReference "qrcode" */; + productName = QRCode; + }; 69DCF4992AB8C58500CBE2CC /* SwiftProtobuf */ = { isa = XCSwiftPackageProductDependency; package = 69DA9A2429E189EF00A442DA /* XCRemoteSwiftPackageReference "swift-protobuf" */; diff --git a/NearDrop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NearDrop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 12467f6..bb605bc 100644 --- a/NearDrop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NearDrop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "f2a7ac82cfff4c1d8403f173eab86d0110b35f7ef9b3ae22165a6e3b814b176b", "pins" : [ { "identity" : "asn1", @@ -18,6 +19,15 @@ "version" : "1.9.0" } }, + { + "identity" : "qrcode", + "kind" : "remoteSourceControl", + "location" : "https://github.com/dagronf/qrcode.git", + "state" : { + "revision" : "177d4091f7fcce1efc22de2a5fcdb671bd103101", + "version" : "26.10.0" + } + }, { "identity" : "swift-protobuf", "kind" : "remoteSourceControl", @@ -27,6 +37,15 @@ "version" : "1.21.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", @@ -35,7 +54,16 @@ "revision" : "55493f0609f07b24bf6cd52b90898ed1cefb268e", "version" : "3.5.3" } + }, + { + "identity" : "swiftimagereadwrite", + "kind" : "remoteSourceControl", + "location" : "https://github.com/dagronf/SwiftImageReadWrite", + "state" : { + "revision" : "42ace2412279f18bc264bc306e96b51c36e12a33", + "version" : "1.9.2" + } } ], - "version" : 2 + "version" : 3 } diff --git a/NearbyShare/Data+Extensions.swift b/NearbyShare/Data+Extensions.swift index efa8472..5bf1a07 100644 --- a/NearbyShare/Data+Extensions.swift +++ b/NearbyShare/Data+Extensions.swift @@ -21,6 +21,13 @@ extension Data{ }) } + func suffixOfAtMost(numBytes:Int) -> Data{ + if count<=numBytes{ + return self; + } + return subdata(in: count-numBytes.. Data{ var data=Data(count: length) data.withUnsafeMutableBytes { diff --git a/NearbyShare/NearbyConnection.swift b/NearbyShare/NearbyConnection.swift index 2303aa3..2bb6903 100644 --- a/NearbyShare/NearbyConnection.swift +++ b/NearbyShare/NearbyConnection.swift @@ -44,6 +44,7 @@ class NearbyConnection{ private var clientSeq:Int32=0 private(set) var pinCode:String? + private(set) var authKey:SymmetricKey? init(connection:NWConnection, id:String) { self.connection=connection @@ -340,7 +341,7 @@ class NearbyConnection{ if #available(macOS 11.0, *){ return HKDF.deriveKey(inputKeyMaterial: inputKeyMaterial, salt: salt, info: info, outputByteCount: outputByteCount) }else{ - return SymmetricKey(data: hkdfExpand(prk: hkdfExtract(salt: salt, ikm: inputKeyMaterial.withUnsafeBytes({return Data(bytes: $0.baseAddress!, count: $0.count)})), info: info, length: outputByteCount)) + return SymmetricKey(data: hkdfExpand(prk: hkdfExtract(salt: salt, ikm: inputKeyMaterial.data()), info: info, length: outputByteCount)) } } @@ -369,6 +370,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=Data([0x82, 0xAA, 0x55, 0xA0, 0xD3, 0x97, 0xF8, 0x83, 0x46, 0xCA, 0x1C, diff --git a/NearbyShare/NearbyConnectionManager.swift b/NearbyShare/NearbyConnectionManager.swift index 0fbb222..9f4658f 100644 --- a/NearbyShare/NearbyConnectionManager.swift +++ b/NearbyShare/NearbyConnectionManager.swift @@ -8,21 +8,27 @@ import Foundation import Network import System +import CryptoKit +import SwiftECC public struct RemoteDeviceInfo{ 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){ - self.name=info.name + init(info:EndpointInfo, id: String? = nil){ + self.name=info.name! self.type=info.deviceType + self.qrCodeData=info.qrCodeData + self.id=id } public enum DeviceType:Int32{ @@ -94,22 +100,50 @@ struct OutgoingTransferInfo{ } 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 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 _deviceName=String(data: data[18..<(18+deviceNameLength)], encoding: .utf8) else {return nil} + deviceName=_deviceName + }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 + while data.count-offset>2{ // read TLV records, if any + let type=data[offset] + let length=Int(data[offset+1]) + offset=offset+2 + if data.count-offset>=length{ + if type==1{ // QR code data + qrCodeData=data.subdata(in: offset..Data{ @@ -121,7 +155,7 @@ struct EndpointInfo{ endpointInfo.append(UInt8.random(in: 0...255)) } // Device name in UTF-8 prefixed with 1-byte length - var nameChars=[UInt8](name.utf8) + var nameChars=[UInt8](name!.utf8) if nameChars.count>255{ nameChars=[UInt8](nameChars[0..<255]) } @@ -141,6 +175,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() @@ -162,6 +197,12 @@ public class NearbyConnectionManager : NSObject, NetServiceDelegate, InboundNear private var browser:NWBrowser? + private var qrCodePublicKey:ECPublicKey? + private var qrCodePrivateKey:ECPrivateKey? + private var qrCodeAdvertisingToken:Data? + private var qrCodeNameEncryptionKey:SymmetricKey? + private var qrCodeData:Data? + public static let shared=NearbyConnectionManager() override init() { @@ -314,19 +355,51 @@ public class NearbyConnectionManager : NSObject, NetServiceDelegate, InboundNear 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} + guard let endpointInfoSerialized=Data.dataFromUrlSafeBase64(endpointInfoEncoded) else {return} + guard var endpointInfo=EndpointInfo(data: endpointInfoSerialized) else {return} - let deviceInfo=RemoteDeviceInfo(name: deviceName, type: deviceType, id: endpointID) + var deviceInfo:RemoteDeviceInfo? + if let _=endpointInfo.name{ + deviceInfo=addFoundDevice(foundService: &foundService, endpointInfo: endpointInfo, endpointID: endpointID) + } + + if let qrData=endpointInfo.qrCodeData, let _=qrCodeAdvertisingToken{ +#if DEBUG + print("Device has QR data: \(qrData.base64EncodedString()), our advertising token is \(qrCodeAdvertisingToken!.base64EncodedString())") +#endif + 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{ +#if DEBUG + print("Error decrypting QR code data of an invisible device: \(error)") +#endif + } + } + } + } + + 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 } private func maybeRemoveFoundDevice(service:NWBrowser.Result){ @@ -337,6 +410,33 @@ public class NearbyConnectionManager : NSObject, NetServiceDelegate, InboundNear } } + 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]){ guard let info=foundServices[deviceID] else {return} let tcp=NWProtocolTCP.Options.init() @@ -344,6 +444,7 @@ public class NearbyConnectionManager : NSObject, NetServiceDelegate, InboundNear let nwconn=NWConnection(to: info.service.endpoint, using: NWParameters(tls: .none, tcp: tcp)) let conn=OutboundNearbyConnection(connection: nwconn, id: deviceID, urlsToSend: urls) conn.delegate=self + 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/OutboundNearbyConnection.swift b/NearbyShare/OutboundNearbyConnection.swift index b9a2223..90fafb8 100644 --- a/NearbyShare/OutboundNearbyConnection.swift +++ b/NearbyShare/OutboundNearbyConnection.swift @@ -27,6 +27,8 @@ class OutboundNearbyConnection:NearbyConnection{ private var cancelled:Bool=false private var textPayloadID:Int64=0 + public var qrCodePrivateKey:ECPrivateKey? + enum State{ case initial, sentUkeyClientInit, sentUkeyClientFinish, sentPairedKeyEncryption, sentPairedKeyResult, sentIntroduction, sendingFiles } @@ -237,6 +239,12 @@ 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 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/SymmetricKey+Extensions.swift b/NearbyShare/SymmetricKey+Extensions.swift new file mode 100644 index 0000000..372a78b --- /dev/null +++ b/NearbyShare/SymmetricKey+Extensions.swift @@ -0,0 +1,15 @@ +// +// SymmetricKey+Extensions.swift +// NearbyShare +// +// Created by Grishka on 06.08.2025. +// + +import Foundation +import CryptoKit + +extension SymmetricKey{ + func data() -> Data{ + return withUnsafeBytes({return Data(bytes: $0.baseAddress!, count: $0.count)}) + } +} diff --git a/ShareExtension/Base.lproj/ShareViewController.xib b/ShareExtension/Base.lproj/ShareViewController.xib index 558acb4..1d57983 100644 --- a/ShareExtension/Base.lproj/ShareViewController.xib +++ b/ShareExtension/Base.lproj/ShareViewController.xib @@ -1,8 +1,8 @@ - + - + @@ -22,6 +22,10 @@ + + + + @@ -94,12 +98,28 @@ Gw + + + + @@ -256,6 +276,82 @@ Gw + + + + + + + + + + + + + + + + + + If this doesn't work, make sure that the device and your Mac are on the same network, and that the router isn't blocking LAN communication. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ShareExtension/QrCodeBackgroundView.swift b/ShareExtension/QrCodeBackgroundView.swift new file mode 100644 index 0000000..c7defe6 --- /dev/null +++ b/ShareExtension/QrCodeBackgroundView.swift @@ -0,0 +1,46 @@ +// +// QrCodeBackgroundView.swift +// ShareExtension +// +// Created by Grishka on 07.08.2025. +// + +import Foundation +import Metal +import MetalKit + +class QrCodeBackgroundView:MTKView{ + private var commandQueue:MTLCommandQueue? + private var commandBuffer:MTLCommandBuffer? + + override func awakeFromNib() { + super.awakeFromNib() + isPaused=true + enableSetNeedsDisplay=false + device=MTLCreateSystemDefaultDevice() + let brightness=1.5 + clearColor=MTLClearColor(red: brightness, green: brightness, blue: brightness, alpha: 1) + + let mtlLayer:CAMetalLayer=(layer as? CAMetalLayer)! + mtlLayer.wantsExtendedDynamicRangeContent=true + mtlLayer.cornerRadius=20 + mtlLayer.masksToBounds=true + colorspace=CGColorSpace(name: CGColorSpace.extendedSRGB) + colorPixelFormat = .rgba16Float + + commandQueue=device!.makeCommandQueue() + commandBuffer=commandQueue!.makeCommandBuffer() + draw() + } + + override func draw(_ dirtyRect: NSRect) { + guard let commandBuffer=commandBuffer else {return} + if let descriptor=currentRenderPassDescriptor, let encoder=commandBuffer.makeRenderCommandEncoder(descriptor: descriptor){ + encoder.endEncoding() + if let currentDrawable=currentDrawable{ + commandBuffer.present(currentDrawable) + } + } + commandBuffer.commit() + } +} diff --git a/ShareExtension/ShareViewController.swift b/ShareExtension/ShareViewController.swift index 92a3c27..9731414 100644 --- a/ShareExtension/ShareViewController.swift +++ b/ShareExtension/ShareViewController.swift @@ -8,6 +8,7 @@ import Foundation import Cocoa import NearbyShare +import QRCode class ShareViewController: NSViewController, ShareExtensionDelegate{ @@ -15,6 +16,7 @@ class ShareViewController: NSViewController, ShareExtensionDelegate{ private var foundDevices:[RemoteDeviceInfo]=[] private var chosenDevice:RemoteDeviceInfo? private var lastError:Error? + private var sheetWindow:NSWindow? @IBOutlet var filesIcon:NSImageView? @IBOutlet var filesLabel:NSTextField? @@ -30,6 +32,11 @@ class ShareViewController: NSViewController, ShareExtensionDelegate{ @IBOutlet var progressState:NSTextField? @IBOutlet var progressDeviceIconWrap:NSView? @IBOutlet var progressDeviceSecondaryIcon:NSImageView? + @IBOutlet var qrCodeButton:NSButton? + + @IBOutlet var qrCodeSheetView:NSView? + @IBOutlet var qrCodeView:NSImageView? + @IBOutlet var qrCodeWrapView:NSView? override var nibName: NSNib.Name? { return NSNib.Name("ShareViewController") @@ -38,7 +45,6 @@ class ShareViewController: NSViewController, ShareExtensionDelegate{ override func loadView() { super.loadView() - // Insert code here to customize the view let item = self.extensionContext!.inputItems[0] as! NSExtensionItem if let attachments = item.attachments { for attachment in attachments as NSArray{ @@ -99,6 +105,13 @@ class ShareViewController: NSViewController, ShareExtensionDelegate{ progressDeviceIconWrap!.wantsLayer=true progressDeviceIconWrap!.layer!.masksToBounds=false + + qrCodeWrapView!.wantsLayer=true + qrCodeWrapView!.layer!.masksToBounds=false + qrCodeWrapView!.layer!.shadowColor = .black + qrCodeWrapView!.layer!.shadowOpacity=0.3 + qrCodeWrapView!.layer!.shadowRadius=12 + qrCodeWrapView!.layer!.shadowOffset=CGSizeMake(0, -5) } override func viewDidLoad(){ @@ -122,6 +135,39 @@ class ShareViewController: NSViewController, ShareExtensionDelegate{ self.extensionContext!.cancelRequest(withError: cancelError) } + @IBAction func useQrCode(_ sender: AnyObject?) { + let window=contentWrap!.window! + let sheetWindow=NSWindow() + sheetWindow.contentView=qrCodeSheetView! + let size=NSSize(width: 380, height: 400) + sheetWindow.contentMaxSize=size + sheetWindow.contentMinSize=size + sheetWindow.setContentSize(size) + + 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(.roundedPointing()) + .errorCorrection(.low) + .generate.image(dimension: Int(qrCodeView!.frame.width)*2) + qrCodeView!.image=NSImage(cgImage: qrCodeImage, size: qrCodeImage.size) + + self.sheetWindow=sheetWindow + window.beginSheet(sheetWindow) { response in + self.sheetWindow=nil + NearbyConnectionManager.shared.clearQrCodeKey() + } + } + + @IBAction func dismissQrCodeSheet(_ sender: AnyObject?){ + contentWrap!.window!.endSheet(sheetWindow!) + sheetWindow=nil + NearbyConnectionManager.shared.clearQrCodeKey() + } + private func urlsReady(){ for url in urls{ if url.isFileURL{ @@ -172,6 +218,11 @@ class ShareViewController: NSViewController, ShareExtensionDelegate{ } } + func startTransferWithQrCode(device: RemoteDeviceInfo){ + dismissQrCodeSheet(nil) + selectDevice(device: device) + } + func connectionWasEstablished(pinCode: String) { progressState?.stringValue=String(format:NSLocalizedString("PinCode", value: "PIN: %@", comment: ""), arguments: [pinCode]) progressProgressBar?.isIndeterminate=false @@ -224,6 +275,7 @@ class ShareViewController: NSViewController, ShareExtensionDelegate{ NearbyConnectionManager.shared.stopDeviceDiscovery() listViewWrapper?.animator().isHidden=true progressView?.animator().isHidden=false + qrCodeButton?.animator().isHidden=true progressDeviceName?.stringValue=device.name progressDeviceIcon?.image=imageForDeviceType(type: device.type) progressProgressBar?.startAnimation(nil) diff --git a/ShareExtension/ru.lproj/ShareViewController.strings b/ShareExtension/ru.lproj/ShareViewController.strings index bb56687..1093488 100644 --- a/ShareExtension/ru.lproj/ShareViewController.strings +++ b/ShareExtension/ru.lproj/ShareViewController.strings @@ -5,8 +5,20 @@ /* Class = "NSButtonCell"; title = "Cancel"; ObjectID = "6Up-t3-mwm"; */ "6Up-t3-mwm.title" = "Отменить"; +/* Class = "NSButtonCell"; title = "Cancel"; ObjectID = "9EW-Db-fGL"; */ +"9EW-Db-fGL.title" = "Отменить"; + +/* Class = "NSButtonCell"; title = "Use QR code..."; ObjectID = "Ll5-lP-3bU"; */ +"Ll5-lP-3bU.title" = "Использовать QR-код..."; + /* Class = "NSTextFieldCell"; title = "Looking for devices..."; ObjectID = "NaJ-Wx-Pim"; */ "NaJ-Wx-Pim.title" = "Ищу устройства..."; +/* Class = "NSTextFieldCell"; title = "Scan this QR code with an Android device. The transfer will begin automatically."; ObjectID = "d3J-ot-h5d"; */ +"d3J-ot-h5d.title" = "Отсканируйте этот QR-код на Android-устройстве. Передача начнётся автоматически."; + +/* Class = "NSTextFieldCell"; title = "If this doesn't work, make sure that the device and your Mac are on the same network, and that the router isn't blocking LAN communication."; ObjectID = "uuF-4K-rMR"; */ +"uuF-4K-rMR.title" = "Если соединиться не удаётся, убедитесь, что устройство и Mac находятся в одной сети, и что роутер не блокирует передачу данных по локальной сети."; + /* Class = "NSTextFieldCell"; title = "If you don't see your device, open \"Files by Google\" app and tap \"Receive\" on the Nearby Share tab."; ObjectID = "vla-gF-eJo"; */ "vla-gF-eJo.title" = "Если Вы не видите своё устройство, откройте приложение \"Google Files\" и нажмите \"Получить\" на вкладке Обмен.";