Add support for sending using a QR code

closes #196
This commit is contained in:
Grishka
2025-08-07 04:59:01 +03:00
parent f75f5fb1f0
commit ad1c47012f
11 changed files with 426 additions and 27 deletions

View File

@@ -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 = "<group>"; };
6978D5392BFE97E100A6100C /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fr; path = fr.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
6978D53A2BFE97E900A6100C /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
697DDEEB2E43986400B4749C /* SymmetricKey+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SymmetricKey+Extensions.swift"; sourceTree = "<group>"; };
697DDF0E2E4422B300B4749C /* QrCodeBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QrCodeBackgroundView.swift; sourceTree = "<group>"; };
698DFAE529E2F91A0064F247 /* NearbyConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearbyConnection.swift; sourceTree = "<group>"; };
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 = "<group>";
@@ -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" */;

View File

@@ -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
}

View File

@@ -21,6 +21,13 @@ extension Data{
})
}
func suffixOfAtMost(numBytes:Int) -> Data{
if count<=numBytes{
return self;
}
return subdata(in: count-numBytes..<count)
}
static func randomData(length: Int) -> Data{
var data=Data(count: length)
data.withUnsafeMutableBytes {

View File

@@ -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<SHA256>.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,

View File

@@ -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..<offset+length)
}
offset=offset+length
}
}
self.qrCodeData=qrCodeData
}
func serialize()->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()

View File

@@ -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

View File

@@ -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)})
}
}

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="22505" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23504" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22505"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23504"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@@ -22,6 +22,10 @@
<outlet property="progressProgressBar" destination="J5x-hu-Kn5" id="vYH-DN-03b"/>
<outlet property="progressState" destination="y8I-D3-scQ" id="Tj3-xa-8VG"/>
<outlet property="progressView" destination="Q9K-dc-THx" id="LlE-4d-mI5"/>
<outlet property="qrCodeButton" destination="ZsR-MN-QVA" id="KaY-NO-F5z"/>
<outlet property="qrCodeSheetView" destination="cIc-n7-hIV" id="Ol8-aW-fH0"/>
<outlet property="qrCodeView" destination="9Hk-Pa-eYI" id="Weo-3l-5pN"/>
<outlet property="qrCodeWrapView" destination="dq7-IV-6Ef" id="V35-5n-nyq"/>
<outlet property="view" destination="1" id="2"/>
</connections>
</customObject>
@@ -94,12 +98,28 @@ Gw
<customView translatesAutoresizingMaskIntoConstraints="NO" id="ACO-U2-AZj">
<rect key="frame" x="0.0" y="40" width="404" height="146"/>
</customView>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZsR-MN-QVA">
<rect key="frame" x="3" y="3" width="125" height="32"/>
<buttonCell key="cell" type="push" title="Use QR code..." bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="Ll5-lP-3bU">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="100" id="7o5-tK-dlg"/>
</constraints>
<connections>
<action selector="useQrCode:" target="-2" id="BS0-WG-B7q"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="ZsR-MN-QVA" secondAttribute="bottom" constant="10" id="76Q-6H-r3I"/>
<constraint firstItem="OEz-QK-nem" firstAttribute="top" secondItem="1xF-vr-5sH" secondAttribute="bottom" constant="10" id="852-oh-vM0"/>
<constraint firstItem="ZsR-MN-QVA" firstAttribute="leading" secondItem="1" secondAttribute="leading" constant="10" id="CHP-wF-HhN"/>
<constraint firstItem="OEz-QK-nem" firstAttribute="leading" secondItem="afM-Om-nLQ" secondAttribute="trailing" constant="5" id="CIj-Dk-Md0"/>
<constraint firstAttribute="trailing" secondItem="ACO-U2-AZj" secondAttribute="trailing" id="HIg-sX-IK0"/>
<constraint firstItem="ACO-U2-AZj" firstAttribute="leading" secondItem="1" secondAttribute="leading" id="IGL-2g-wpe"/>
<constraint firstItem="ZsR-MN-QVA" firstAttribute="top" secondItem="ACO-U2-AZj" secondAttribute="bottom" constant="10" id="K9U-z1-gie"/>
<constraint firstItem="afM-Om-nLQ" firstAttribute="leading" secondItem="1" secondAttribute="leading" constant="10" id="LK2-cg-qcY"/>
<constraint firstItem="NVE-vN-dkz" firstAttribute="top" secondItem="ACO-U2-AZj" secondAttribute="bottom" constant="10" id="RSD-PX-W0H"/>
<constraint firstAttribute="bottom" secondItem="NVE-vN-dkz" secondAttribute="bottom" constant="10" id="USG-Gg-of3"/>
@@ -256,6 +276,82 @@ Gw
</scroller>
<point key="canvasLocation" x="-397.5" y="30"/>
</scrollView>
<customView id="cIc-n7-hIV">
<rect key="frame" x="0.0" y="0.0" width="380" height="400"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="oJM-qC-yyW">
<rect key="frame" x="8" y="358" width="364" height="32"/>
<textFieldCell key="cell" alignment="center" title="Scan this QR code with an Android device. The transfer will begin automatically." id="d3J-ot-h5d">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="pLr-Yq-OMn">
<rect key="frame" x="301" y="3" width="76" height="32"/>
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="9EW-Db-fGL">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
Gw
</string>
<connections>
<action selector="dismissQrCodeSheet:" target="-2" id="OX6-j3-Dag"/>
</connections>
</buttonCell>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="60" id="eRJ-dH-AQv"/>
</constraints>
</button>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="mP8-S4-SUv">
<rect key="frame" x="8" y="40" width="364" height="42"/>
<textFieldCell key="cell" alignment="center" id="uuF-4K-rMR">
<font key="font" metaFont="smallSystem"/>
<string key="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.</string>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<customView translatesAutoresizingMaskIntoConstraints="NO" id="dq7-IV-6Ef">
<rect key="frame" x="80" y="113" width="220" height="220"/>
<subviews>
<customView translatesAutoresizingMaskIntoConstraints="NO" id="lnf-Ci-bVP" customClass="QrCodeBackgroundView" customModule="ShareExtension" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="220" height="220"/>
</customView>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="9Hk-Pa-eYI">
<rect key="frame" x="0.0" y="0.0" width="220" height="220"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="J6N-uf-WsV"/>
</imageView>
</subviews>
<constraints>
<constraint firstItem="lnf-Ci-bVP" firstAttribute="leading" secondItem="dq7-IV-6Ef" secondAttribute="leading" id="16L-0N-tMO"/>
<constraint firstItem="9Hk-Pa-eYI" firstAttribute="top" secondItem="dq7-IV-6Ef" secondAttribute="top" id="22t-Se-FtM"/>
<constraint firstAttribute="bottom" secondItem="lnf-Ci-bVP" secondAttribute="bottom" id="9XU-id-5Lj"/>
<constraint firstItem="9Hk-Pa-eYI" firstAttribute="leading" secondItem="dq7-IV-6Ef" secondAttribute="leading" id="KWg-eg-4sO"/>
<constraint firstAttribute="width" constant="220" id="drn-Gr-Tx6"/>
<constraint firstAttribute="trailing" secondItem="9Hk-Pa-eYI" secondAttribute="trailing" id="fif-Sg-QCc"/>
<constraint firstAttribute="bottom" secondItem="9Hk-Pa-eYI" secondAttribute="bottom" id="iVK-yf-1Ev"/>
<constraint firstAttribute="height" constant="220" id="l69-dh-xwJ"/>
<constraint firstAttribute="trailing" secondItem="lnf-Ci-bVP" secondAttribute="trailing" id="pva-qe-Ibk"/>
<constraint firstItem="lnf-Ci-bVP" firstAttribute="top" secondItem="dq7-IV-6Ef" secondAttribute="top" id="yOu-Bz-qVZ"/>
</constraints>
</customView>
</subviews>
<constraints>
<constraint firstItem="oJM-qC-yyW" firstAttribute="leading" secondItem="cIc-n7-hIV" secondAttribute="leading" constant="10" id="HaS-Sl-afe"/>
<constraint firstItem="dq7-IV-6Ef" firstAttribute="centerX" secondItem="cIc-n7-hIV" secondAttribute="centerX" id="Iip-bT-bl3"/>
<constraint firstItem="dq7-IV-6Ef" firstAttribute="top" secondItem="oJM-qC-yyW" secondAttribute="bottom" constant="25" id="YMN-xe-7cI"/>
<constraint firstAttribute="trailing" secondItem="pLr-Yq-OMn" secondAttribute="trailing" constant="10" id="cAr-rb-sXX"/>
<constraint firstItem="oJM-qC-yyW" firstAttribute="top" secondItem="cIc-n7-hIV" secondAttribute="top" constant="10" id="gts-uR-3Ej"/>
<constraint firstAttribute="trailing" secondItem="oJM-qC-yyW" secondAttribute="trailing" constant="10" id="hwy-vE-lHk"/>
<constraint firstAttribute="bottom" secondItem="pLr-Yq-OMn" secondAttribute="bottom" constant="10" id="jpn-he-6Ju"/>
<constraint firstItem="pLr-Yq-OMn" firstAttribute="top" secondItem="mP8-S4-SUv" secondAttribute="bottom" constant="10" id="sbP-BO-Te7"/>
<constraint firstItem="mP8-S4-SUv" firstAttribute="leading" secondItem="cIc-n7-hIV" secondAttribute="leading" constant="10" id="vUz-rq-dbN"/>
<constraint firstAttribute="trailing" secondItem="mP8-S4-SUv" secondAttribute="trailing" constant="10" id="way-Az-buy"/>
</constraints>
<point key="canvasLocation" x="-13" y="400"/>
</customView>
</objects>
<resources>
<image name="NSCaution" width="32" height="32"/>

View File

@@ -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()
}
}

View File

@@ -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)

View File

@@ -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\" и нажмите \"Получить\" на вкладке Обмен.";