From 48164b8095f67bd429d9086fe6d3a7389cdaf2e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20B=C3=B6ttger?= Date: Sat, 23 Aug 2025 11:52:14 +0200 Subject: [PATCH] Add Trusted Devices Authentication --- .../Connection/InboundNearbyConnection.swift | 114 ++++++++-- .../Connection/NearbyConnectionManager.swift | 8 +- .../Connection/OutboundNearbyConnection.swift | 41 +++- NearbyShare/Model/DataModel.swift | 8 +- NearbyShare/Util/AppGroup.swift | 21 ++ NearbyShare/Util/IdentityManager.swift | 119 ++++++++++ NearbyShare/Util/Logger.swift | 11 +- NearbyShare/Util/TrustStore.swift | 33 +++ QuickDrop.xcodeproj/project.pbxproj | 30 +++ QuickDrop/Localizable.xcstrings | 214 ++++++++++++++++++ QuickDropiOS/Configuration.swift | 2 +- QuickDropiOS/ContentView.swift | 3 +- QuickDropiOS/ReceiveModel.swift | 38 ++-- QuickDropiOS/UIAlert.swift | 49 ++-- 14 files changed, 621 insertions(+), 70 deletions(-) create mode 100644 NearbyShare/Util/AppGroup.swift create mode 100644 NearbyShare/Util/IdentityManager.swift create mode 100644 NearbyShare/Util/TrustStore.swift diff --git a/NearbyShare/Connection/InboundNearbyConnection.swift b/NearbyShare/Connection/InboundNearbyConnection.swift index 2f2fa04..c2f945b 100644 --- a/NearbyShare/Connection/InboundNearbyConnection.swift +++ b/NearbyShare/Connection/InboundNearbyConnection.swift @@ -26,6 +26,8 @@ class InboundNearbyConnection: NearbyConnection { private var cipherCommitment: Data? private var textPayloadID: Int64 = 0 private var bytesToBeTransferred: Int64 = 0 + private var isAuthenticated = false + private var peerCertificate: Sharing_Nearby_PublicCertificate? = nil var isPlainTextTransfer = false var wasUserRejected = false @@ -371,9 +373,11 @@ class InboundNearbyConnection: NearbyConnection { pairedEncryption.v1 = Sharing_Nearby_V1Frame() pairedEncryption.v1.type = .pairedKeyEncryption pairedEncryption.v1.pairedKeyEncryption = Sharing_Nearby_PairedKeyEncryptionFrame() - // Presumably used for all the phone number stuff that no one needs anyway + + // For inbound connections, we do not use provide secretIDHash and signedData pairedEncryption.v1.pairedKeyEncryption.secretIDHash = Data.randomData(length: 6) pairedEncryption.v1.pairedKeyEncryption.signedData = Data.randomData(length: 72) + try sendTransferSetupFrame(pairedEncryption) currentState = .sentConnectionResponse } else { @@ -381,16 +385,77 @@ class InboundNearbyConnection: NearbyConnection { } } - + private func processPairedKeyEncryptionFrame(_ frame: Sharing_Nearby_Frame) throws { - guard frame.hasV1, frame.v1.hasPairedKeyEncryption else { throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.pairedKeyEncryption") } - var pairedResult = Sharing_Nearby_Frame() - pairedResult.version = .v1 - pairedResult.v1 = Sharing_Nearby_V1Frame() - pairedResult.v1.type = .pairedKeyResult - pairedResult.v1.pairedKeyResult = Sharing_Nearby_PairedKeyResultFrame() - pairedResult.v1.pairedKeyResult.status = .unable - try sendTransferSetupFrame(pairedResult) + let pkeFrame = frame.v1.pairedKeyEncryption + + log("[InboundNearbyConnection \(self.id)] Processing paired key encryption frame with secretIDHash: \(pkeFrame.secretIDHash.hex)") + + log("Signed data: " + pkeFrame.signedData.hex) + + // Check if we can accept the connection automatically + if !pkeFrame.secretIDHash.isEmpty, + let trustedCertData = TrustStore.shared.findTrustedKey(for: pkeFrame.secretIDHash), + let trustedCert = try? Sharing_Nearby_PublicCertificate(serializedBytes: trustedCertData), + let peerGenericKey = try? Securemessage_GenericPublicKey(serializedBytes: trustedCert.publicKey), + let authKeyData = self.authKey?.data() { + + log("[InboundNearbyConnection \(self.id)] Found trusted certificate for secretIDHash: \(pkeFrame.secretIDHash.hex)") + + let domain = Domain.instance(curve: .EC256r1) + let ecKey = peerGenericKey.ecP256PublicKey + let point = Point(BInt(magnitude: [UInt8](ecKey.x)), BInt(magnitude: [UInt8](ecKey.y))) + + guard let peerPublicKey = try? ECPublicKey(domain: domain, w: point) else { + log("[InboundNearbyConnection \(self.id)] Paired key auth failed: Invalid peer public key.") + try automaticAuthFailed() + return + } + + guard pkeFrame.signedData.count == 72 else { + log("[InboundNearbyConnection \(self.id)] Paired key auth failed: Invalid signature length.") + try automaticAuthFailed() + return + } + + // Strip header + let signedData = pkeFrame.signedData.subdata(in: 8..<72) + + let r = Array(signedData.prefix(32)) + let s = Array(signedData.suffix(32)) + + let signature = ECSignature(domain: domain, r: r, s: s) + + if peerPublicKey.verify(signature: signature, msg: authKeyData) { + log("[InboundNearbyConnection \(self.id)] Paired key authentication successful.") + try sendPairedKeyResult(status: .success) + isAuthenticated = true + } else { + log("[InboundNearbyConnection \(self.id)] Paired key auth failed: Signature verification failed.") + try automaticAuthFailed() + } + } else { + try automaticAuthFailed() + } + + func automaticAuthFailed() throws { + // Store certificate of peer to let user device later to trust it + if let peerCertificate = frame.v1.certificateInfo.publicCertificate.first { + log("[InboundNearbyConnection \(self.id)] Storing peer certificate for potential later use") + self.peerCertificate = peerCertificate + } + + try sendPairedKeyResult(status: .unable) + } + } + + + private func sendPairedKeyResult(status: Sharing_Nearby_PairedKeyResultFrame.Status) throws { + var resultFrame = Sharing_Nearby_Frame() + resultFrame.version = .v1 + resultFrame.v1.type = .pairedKeyResult + resultFrame.v1.pairedKeyResult.status = status + try sendTransferSetupFrame(resultFrame) currentState = .sentPairedKeyResult } @@ -436,10 +501,8 @@ class InboundNearbyConnection: NearbyConnection { filesToBeReceived[file.payloadID] = info bytesToBeTransferred += file.size } - let metadata = TransferMetadata(files: filesToBeReceived.map { $0.value.meta }, id: id, pinCode: pinCode) - DispatchQueue.main.async { - self.delegate?.obtainUserConsent(transfer: metadata, device: self.remoteDeviceInfo!, connection: self) - } + let metadata = TransferMetadata(files: filesToBeReceived.map { $0.value.meta }, id: id, pinCode: pinCode, allowsToBeAddedAsTrustedDevice: self.peerCertificate != nil) + checkIfCanProceed(metadata: metadata) } else if let textMetadata = frame.v1.introduction.textMetadata.first { @@ -447,16 +510,14 @@ class InboundNearbyConnection: NearbyConnection { let isClipboardText = textMetadata.type == .text - let metadata = TransferMetadata(files: [], id: id, pinCode: pinCode, textDescription: textMetadata.textTitle, transferType: isClipboardText ? .text : .url) + let metadata = TransferMetadata(files: [], id: id, pinCode: pinCode, textDescription: textMetadata.textTitle, transferType: isClipboardText ? .text : .url, allowsToBeAddedAsTrustedDevice: self.peerCertificate != nil) textPayloadID = textMetadata.payloadID if isClipboardText { isPlainTextTransfer = true } - DispatchQueue.main.async { - self.delegate?.obtainUserConsent(transfer: metadata, device: self.remoteDeviceInfo!, connection: self) - } + checkIfCanProceed(metadata: metadata) } else { rejectDueToUnsupportedFileType(frame) @@ -464,6 +525,16 @@ class InboundNearbyConnection: NearbyConnection { } else { rejectDueToUnsupportedFileType(frame) } + + + func checkIfCanProceed(metadata: TransferMetadata) { + + let acceptAutomatically = isAuthenticated || UserDefaults.standard.bool(forKey: UserDefaultsKeys.automaticallyAcceptFiles.rawValue) + + DispatchQueue.main.async { + self.delegate?.obtainUserConsent(transfer: metadata, device: self.remoteDeviceInfo!, connection: self, acceptAutomatically: acceptAutomatically) + } + } } @@ -476,7 +547,12 @@ class InboundNearbyConnection: NearbyConnection { } - func submitUserConsent(accepted: Bool, storeInTemp: Bool = false) { + func submitUserConsent(accepted: Bool, trustDevice: Bool, storeInTemp: Bool = false) { + + if trustDevice, let peerCertificate = peerCertificate { + TrustStore.shared.addTrusted(certificate: peerCertificate) + } + DispatchQueue.global(qos: .utility).async { if accepted { self.acceptTransfer(storeInTemp: storeInTemp) diff --git a/NearbyShare/Connection/NearbyConnectionManager.swift b/NearbyShare/Connection/NearbyConnectionManager.swift index 077fa15..560e13d 100644 --- a/NearbyShare/Connection/NearbyConnectionManager.swift +++ b/NearbyShare/Connection/NearbyConnectionManager.swift @@ -154,8 +154,8 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb } - func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo, connection _: InboundNearbyConnection) { - mainAppDelegate?.obtainUserConsent(transfer: transfer, device: device) + func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo, connection _: InboundNearbyConnection, acceptAutomatically: Bool) { + mainAppDelegate?.obtainUserConsent(transfer: transfer, device: device, acceptAutomatically: acceptAutomatically) } @@ -173,11 +173,11 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb } - public func submitUserConsent(transferID: String, accept: Bool, storeInTemp: Bool = false) { + public func submitUserConsent(transferID: String, accept: Bool, trustDevice: Bool, storeInTemp: Bool = false) { guard let conn = incomingConnections[transferID] else { return } log("[NearbyConnectionManager] Submitting user consent for transfer \(transferID), accepted: \(accept), store in temp: \(storeInTemp)") - conn.submitUserConsent(accepted: accept, storeInTemp: storeInTemp) + conn.submitUserConsent(accepted: accept, trustDevice: trustDevice, storeInTemp: storeInTemp) } diff --git a/NearbyShare/Connection/OutboundNearbyConnection.swift b/NearbyShare/Connection/OutboundNearbyConnection.swift index 4184792..c0cf157 100644 --- a/NearbyShare/Connection/OutboundNearbyConnection.swift +++ b/NearbyShare/Connection/OutboundNearbyConnection.swift @@ -271,11 +271,40 @@ class OutboundNearbyConnection: NearbyConnection { var pairedEncryption = Sharing_Nearby_Frame() pairedEncryption.version = .v1 - pairedEncryption.v1 = Sharing_Nearby_V1Frame() pairedEncryption.v1.type = .pairedKeyEncryption - pairedEncryption.v1.pairedKeyEncryption = Sharing_Nearby_PairedKeyEncryptionFrame() - pairedEncryption.v1.pairedKeyEncryption.secretIDHash = Data.randomData(length: 6) - pairedEncryption.v1.pairedKeyEncryption.signedData = Data.randomData(length: 72) + + // add public key + var cert = Sharing_Nearby_PublicCertificate() + if let signingPrivateKey = IdentityManager.shared.getPrivateKey(), + let publicKey = IdentityManager.shared.getPublicKey()?.toGenericPublicKey(), + let publicKeyData = IdentityManager.shared.getPublicKey()?.toGenericPublicKeyData(), + let publicKeyId = publicKey.id(), + let authKeyData = self.authKey?.data() { + + log("[OutboundNearbyConnection \(self.id)] Using private key for signing") + + cert.secretID = publicKeyId + cert.publicKey = publicKeyData + + let signatureTuple = signingPrivateKey.sign(msg: authKeyData) + + let rBytes = signatureTuple.r + let sBytes = signatureTuple.s + + // Make sure r and s are 32 bytes each by padding with leading zeros if necessary + let paddedR = Data(repeating: 0, count: 32 - rBytes.count) + rBytes + let paddedS = Data(repeating: 0, count: 32 - sBytes.count) + sBytes + + // Signature: Header (8 bytes) + r (32 bytes) + s (32 bytes) + let signatureData = Data(repeating: 0, count: 8) + paddedR + paddedS + + pairedEncryption.v1.certificateInfo.publicCertificate.append(cert) + pairedEncryption.v1.pairedKeyEncryption.signedData = signatureData + pairedEncryption.v1.pairedKeyEncryption.secretIDHash = cert.secretID + + pairedEncryption.v1.pairedKeyResult.status = .success + } + if let qrKey = qrCodePrivateKey, let authKey = authKey { let signature = qrKey.sign(msg: authKey.data()) @@ -298,6 +327,7 @@ class OutboundNearbyConnection: NearbyConnection { pairedResult.v1.type = .pairedKeyResult pairedResult.v1.pairedKeyResult = Sharing_Nearby_PairedKeyResultFrame() pairedResult.v1.pairedKeyResult.status = .unable + try sendTransferSetupFrame(pairedResult) currentState = .sentPairedKeyResult } @@ -375,8 +405,7 @@ class OutboundNearbyConnection: NearbyConnection { if let textToSend = textToSend { try sendText(text: textToSend) - } - if hasURL() { + } else if hasURL() { try sendURL() } else { try sendNextFileChunk() diff --git a/NearbyShare/Model/DataModel.swift b/NearbyShare/Model/DataModel.swift index c3b7c6b..5ef367b 100644 --- a/NearbyShare/Model/DataModel.swift +++ b/NearbyShare/Model/DataModel.swift @@ -12,7 +12,7 @@ import SwiftECC protocol InboundNearbyConnectionDelegate { - func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo, connection: InboundNearbyConnection) + func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo, connection: InboundNearbyConnection, acceptAutomatically: Bool) func updatedTransferProgress(connection: InboundNearbyConnection, progress: Double) func connectionWasTerminated(connection: InboundNearbyConnection, error: Error?) } @@ -28,7 +28,7 @@ protocol OutboundNearbyConnectionDelegate { public protocol InboundAppDelegate: AnyObject { - func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo) + func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo, acceptAutomatically: Bool) func connectionWasTerminated(from device: RemoteDeviceInfo, wasPlainTextTransfer: Bool, error: Error?) func transferProgress(progress: Double) } @@ -107,13 +107,15 @@ public struct TransferMetadata { public let pinCode: String? public let textDescription: String? public let type: TransferType + public let allowsToBeAddedAsTrustedDevice: Bool - init(files: [FileMetadata], id: String, pinCode: String?, textDescription: String? = nil, transferType: TransferType = .file) { + init(files: [FileMetadata], id: String, pinCode: String?, textDescription: String? = nil, transferType: TransferType = .file, allowsToBeAddedAsTrustedDevice: Bool) { self.files = files self.id = id self.pinCode = pinCode self.textDescription = textDescription self.type = transferType + self.allowsToBeAddedAsTrustedDevice = allowsToBeAddedAsTrustedDevice } public enum TransferType { diff --git a/NearbyShare/Util/AppGroup.swift b/NearbyShare/Util/AppGroup.swift new file mode 100644 index 0000000..c7541a9 --- /dev/null +++ b/NearbyShare/Util/AppGroup.swift @@ -0,0 +1,21 @@ +// +// AppGroup.swift +// QuickDrop +// +// Created by Leon Böttger on 23.08.25. +// + +import Foundation + +/// Contains information about the app group used to share data between main app and widget +struct AppGroup { + + /// Identifier of the app group + static let appGroupName = "group.com.leonboettger.neardrop" + static let appGroupUD = UserDefaults(suiteName: AppGroup.appGroupName)! + + /// Returns the shared App Group directory + static var appGroupDirectory: URL? { + return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName) + } +} diff --git a/NearbyShare/Util/IdentityManager.swift b/NearbyShare/Util/IdentityManager.swift new file mode 100644 index 0000000..3af4e99 --- /dev/null +++ b/NearbyShare/Util/IdentityManager.swift @@ -0,0 +1,119 @@ +// +// IdentityManager.swift +// QuickDrop +// +// Created by Leon Böttger on 22.08.25. +// + + +import Foundation +import SwiftECC +import Security +import CryptoKit + +class IdentityManager { + + static let shared = IdentityManager() + + private let signingKeyTag = "com.leonboettger.quickdrop.identity.signingkey" + private var signingPrivateKey: ECPrivateKey? + private var signingPublicKey: ECPublicKey? + + private init() { + + if let (privateKey, publicKey) = loadKeys() { + + log("[IdentityManager] Loaded existing private key.") + + self.signingPrivateKey = privateKey + self.signingPublicKey = publicKey + } + else { + log("[IdentityManager] No existing private key found, generating a new one.") + let (newPrivate, newPublic) = generateAndStoreKeyPair() + self.signingPrivateKey = newPrivate + self.signingPublicKey = newPublic + } + } + + + func getPrivateKey() -> ECPrivateKey? { + return signingPrivateKey + } + + + func getPublicKey() -> ECPublicKey? { + return signingPublicKey + } + + + private func generateAndStoreKeyPair() -> (ECPrivateKey, ECPublicKey) { + let domain = Domain.instance(curve: .EC256r1) + let (pubKey, privKey) = domain.makeKeyPair() + + AppGroup.appGroupUD.set(Data(privKey.s.asSignedBytes()), forKey: signingKeyTag) + + return (privKey, pubKey) + } + + + private func loadKeys() -> (ECPrivateKey, ECPublicKey)? { + let keyData = AppGroup.appGroupUD.data(forKey: signingKeyTag) + + guard let keyData = keyData else { return nil } + + let domain = Domain.instance(curve: .EC256r1) + guard let privateKey = try? ECPrivateKey(domain: domain, s: .init(signed: Array(keyData))) else { + log("[IdentityManager] Failed to reconstruct private key from stored data.") + return nil + } + + guard let pubPoint = try? domain.multiplyPoint(domain.g, privateKey.s) else { + log("[IdentityManager] Failed to derive public key from private key.") + return nil + } + guard let publicKey = try? ECPublicKey(domain: domain, w: pubPoint) else { + log("[IdentityManager] Failed to reconstruct public key.") + return nil + } + + return (privateKey, publicKey) + } +} + + +extension ECPublicKey { + + func toGenericPublicKey() -> Securemessage_GenericPublicKey { + var genericKey = Securemessage_GenericPublicKey() + genericKey.type = .ecP256 + genericKey.ecP256PublicKey = Securemessage_EcP256PublicKey() + genericKey.ecP256PublicKey.x = Data(self.w.x.asSignedBytes()) + genericKey.ecP256PublicKey.y = Data(self.w.y.asSignedBytes()) + + return genericKey + } + + + func toGenericPublicKeyData() -> Data? { + let genericKey = self.toGenericPublicKey() + return try? genericKey.serializedData() + } +} + + +extension Securemessage_GenericPublicKey { + + func id() -> Data? { + var hasher = SHA256() + + guard let data = try? self.serializedData() else { + return nil + } + + hasher.update(data: data) + let hash = hasher.finalize() + + return Data(hash).prefix(6) + } +} diff --git a/NearbyShare/Util/Logger.swift b/NearbyShare/Util/Logger.swift index 5aef397..b4bd483 100644 --- a/NearbyShare/Util/Logger.swift +++ b/NearbyShare/Util/Logger.swift @@ -25,7 +25,7 @@ final class LogManager { /// Private initializer private init() { // Use the App Group container - let directory = getAppGroupDirectory() + let directory = AppGroup.appGroupDirectory // Create file name from date let formatter = DateFormatter() @@ -89,7 +89,7 @@ final class LogManager { func deleteLogs() { log("[LUI] Deleting all logs...") - guard let directory = getAppGroupDirectory() else { return } + guard let directory = AppGroup.appGroupDirectory else { return } let fileManager = FileManager.default guard let allFiles = try? fileManager.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil) else { @@ -124,10 +124,3 @@ public func log(_ text: String) { } } } - - -/// Returns the shared App Group directory -func getAppGroupDirectory() -> URL? { - let groupIdentifier = "group.com.leonboettger.neardrop" - return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: groupIdentifier) -} diff --git a/NearbyShare/Util/TrustStore.swift b/NearbyShare/Util/TrustStore.swift new file mode 100644 index 0000000..e09f5f8 --- /dev/null +++ b/NearbyShare/Util/TrustStore.swift @@ -0,0 +1,33 @@ +// +// TrustStore.swift +// QuickDrop +// +// Created by Leon Böttger on 22.08.25. +// + + +import Foundation + +class TrustStore { + + static let shared = TrustStore() + private let trustedKeysKey = "com.leonboettger.quickdrop.identity.trustedKeys" + + + private func getTrustedKeys() -> [String: Data] { + return AppGroup.appGroupUD.dictionary(forKey: trustedKeysKey) as? [String: Data] ?? [:] + } + + + func findTrustedKey(for secretIdHash: Data) -> Data? { + return getTrustedKeys()[secretIdHash.hex] + } + + + func addTrusted(certificate: Sharing_Nearby_PublicCertificate) { + guard let publicKeyData = try? certificate.serializedData() else { return } + var keys = getTrustedKeys() + keys[certificate.secretID.hex] = publicKeyData + AppGroup.appGroupUD.set(keys, forKey: trustedKeysKey) + } +} diff --git a/QuickDrop.xcodeproj/project.pbxproj b/QuickDrop.xcodeproj/project.pbxproj index 8dcdc4d..081ec29 100644 --- a/QuickDrop.xcodeproj/project.pbxproj +++ b/QuickDrop.xcodeproj/project.pbxproj @@ -14,6 +14,18 @@ 699DEBAE2AB0573200115D22 /* QuickDropShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 699DEBA12AB0573200115D22 /* QuickDropShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 69DA9A1229E0BF5100A442DA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1129E0BF5100A442DA /* AppDelegate.swift */; }; 69DA9A1429E0BF5200A442DA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 69DA9A1329E0BF5200A442DA /* Assets.xcassets */; }; + EB16B6122E58B4B9007D9792 /* IdentityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB16B6112E58B4B9007D9792 /* IdentityManager.swift */; }; + EB16B6132E58B4B9007D9792 /* IdentityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB16B6112E58B4B9007D9792 /* IdentityManager.swift */; }; + EB16B6142E58B4B9007D9792 /* IdentityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB16B6112E58B4B9007D9792 /* IdentityManager.swift */; }; + EB16B6152E58B4B9007D9792 /* IdentityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB16B6112E58B4B9007D9792 /* IdentityManager.swift */; }; + EB16B6172E58B4C8007D9792 /* TrustStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB16B6162E58B4C8007D9792 /* TrustStore.swift */; }; + EB16B6182E58B4C8007D9792 /* TrustStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB16B6162E58B4C8007D9792 /* TrustStore.swift */; }; + EB16B6192E58B4C8007D9792 /* TrustStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB16B6162E58B4C8007D9792 /* TrustStore.swift */; }; + EB16B61A2E58B4C8007D9792 /* TrustStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB16B6162E58B4C8007D9792 /* TrustStore.swift */; }; + EB16B7772E59BAA1007D9792 /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB16B7762E59BA9F007D9792 /* AppGroup.swift */; }; + EB16B7782E59BAA1007D9792 /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB16B7762E59BA9F007D9792 /* AppGroup.swift */; }; + EB16B7792E59BAA1007D9792 /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB16B7762E59BA9F007D9792 /* AppGroup.swift */; }; + EB16B77A2E59BAA1007D9792 /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB16B7762E59BA9F007D9792 /* AppGroup.swift */; }; EB2F9BFE2DA05F48004010A2 /* BezelNotification in Frameworks */ = {isa = PBXBuildFile; productRef = EB2F9BFD2DA05F48004010A2 /* BezelNotification */; }; EB3643602D73A2A2009CDFF8 /* AudioManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB36435F2D73A29F009CDFF8 /* AudioManager.swift */; }; EB5C36932E34EBBD0052EFD7 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = EB5C36922E34EBBD0052EFD7 /* Localizable.xcstrings */; }; @@ -287,6 +299,9 @@ 69DA9A2B29E18CB500A442DA /* offline_wire_formats.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = offline_wire_formats.pb.swift; sourceTree = ""; }; 69DA9A2C29E18CB500A442DA /* securegcm.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = securegcm.pb.swift; sourceTree = ""; }; 69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = securemessage.pb.swift; sourceTree = ""; }; + EB16B6112E58B4B9007D9792 /* IdentityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityManager.swift; sourceTree = ""; }; + EB16B6162E58B4C8007D9792 /* TrustStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrustStore.swift; sourceTree = ""; }; + EB16B7762E59BA9F007D9792 /* AppGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroup.swift; sourceTree = ""; }; EB2B41E32DC63AC80070FC39 /* SleepManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SleepManager.swift; sourceTree = ""; }; EB2F9BFA2DA04B67004010A2 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; EB36435F2D73A29F009CDFF8 /* AudioManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioManager.swift; sourceTree = ""; }; @@ -496,8 +511,11 @@ isa = PBXGroup; children = ( EB96ED8E2D7F8103004B3FF1 /* Localized.swift */, + EB16B6112E58B4B9007D9792 /* IdentityManager.swift */, EB2B41E32DC63AC80070FC39 /* SleepManager.swift */, + EB16B6162E58B4C8007D9792 /* TrustStore.swift */, EBCC27F82D80C9F70079C478 /* Zip.swift */, + EB16B7762E59BA9F007D9792 /* AppGroup.swift */, EB886E512E4E19390052B7D9 /* HKDF.swift */, EB7CD1442D6FB0C100CB0CB5 /* Logger.swift */, EB5C36E62E3544FB0052EFD7 /* SaveFilesManager.swift */, @@ -862,6 +880,7 @@ files = ( EB704A832E4B10D4008E608F /* DistributionDetector.swift in Sources */, EB96ED902D7F8114004B3FF1 /* Localized.swift in Sources */, + EB16B6172E58B4C8007D9792 /* TrustStore.swift in Sources */, EB886E542E4E19390052B7D9 /* HKDF.swift in Sources */, EBC05C862E33B58300EECC18 /* NetworkFilterIssueView.swift in Sources */, EBC05C872E33B58300EECC18 /* IssueView.swift in Sources */, @@ -877,8 +896,10 @@ EB5C37C82E377A780052EFD7 /* ukey.pb.swift in Sources */, EB5C37C92E377A780052EFD7 /* device_to_device_messages.pb.swift in Sources */, EB950B742E520957002D68FA /* Attachments.swift in Sources */, + EB16B7782E59BAA1007D9792 /* AppGroup.swift in Sources */, EB5C383C2E38C2D90052EFD7 /* AudioManager.swift in Sources */, EB5C37CA2E377A780052EFD7 /* offline_wire_formats.pb.swift in Sources */, + EB16B6122E58B4B9007D9792 /* IdentityManager.swift in Sources */, EBC05C5C2E3394EF00EECC18 /* ErrorAlertHandler.swift in Sources */, EBC05C7B2E33B58100EECC18 /* ActiveNetworkInterface.swift in Sources */, EBC05C7C2E33B58100EECC18 /* ApIsolationTester.swift in Sources */, @@ -907,6 +928,7 @@ EB5C37B82E377A6F0052EFD7 /* SleepManager.swift in Sources */, EB5C37B92E377A6F0052EFD7 /* SaveFilesManager.swift in Sources */, EB5C37BA2E377A6F0052EFD7 /* Helpers.swift in Sources */, + EB16B6182E58B4C8007D9792 /* TrustStore.swift in Sources */, EBC05C5B2E33937200EECC18 /* ErrorAlertHandler.swift in Sources */, EBC05C712E33B57D00EECC18 /* Converter.swift in Sources */, EB886E5D2E4F13EB0052B7D9 /* Zip.swift in Sources */, @@ -917,6 +939,7 @@ EB5C37C12E377A780052EFD7 /* offline_wire_formats.pb.swift in Sources */, EB5C37C22E377A780052EFD7 /* device_to_device_messages.pb.swift in Sources */, EB5C37C32E377A780052EFD7 /* wire_format.pb.swift in Sources */, + EB16B6152E58B4B9007D9792 /* IdentityManager.swift in Sources */, EB5C37C42E377A780052EFD7 /* securegcm.pb.swift in Sources */, EB97A0102E51CD13008B8C5C /* ReceiveModel.swift in Sources */, EBC05C782E33B58100EECC18 /* ActiveNetworkInterface.swift in Sources */, @@ -939,6 +962,7 @@ EB9925812E45D53E00357055 /* sharing_enums.pb.swift in Sources */, EB96ED8F2D7F8105004B3FF1 /* Localized.swift in Sources */, EB7CD1452D6FB0C300CB0CB5 /* Logger.swift in Sources */, + EB16B77A2E59BAA1007D9792 /* AppGroup.swift in Sources */, EBC05C822E33B58300EECC18 /* ApIsolationIssueView.swift in Sources */, EBC05C832E33B58300EECC18 /* FirewallIssueView.swift in Sources */, EBC05C842E33B58300EECC18 /* IssueView.swift in Sources */, @@ -953,6 +977,7 @@ EB5C37F12E377CE70052EFD7 /* ukey.pb.swift in Sources */, EB5C37F22E377CE70052EFD7 /* NearbyConnection.swift in Sources */, EB5C37F32E377CE70052EFD7 /* SaveFilesManager.swift in Sources */, + EB16B6142E58B4B9007D9792 /* IdentityManager.swift in Sources */, EB886E672E4F2F800052B7D9 /* License.swift in Sources */, EB5C37F42E377CE70052EFD7 /* NearbyConnectionManager.swift in Sources */, EB5C37F52E377CE70052EFD7 /* securemessage.pb.swift in Sources */, @@ -979,6 +1004,8 @@ EB979F9A2E50CAA6008B8C5C /* PieProgressView.swift in Sources */, EB979F972E50CA23008B8C5C /* SendModel.swift in Sources */, EB979F982E50CA23008B8C5C /* QuickDropiOSApp.swift in Sources */, + EB16B61A2E58B4C8007D9792 /* TrustStore.swift in Sources */, + EB16B7792E59BAA1007D9792 /* AppGroup.swift in Sources */, EB5C38402E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */, EB5C37FF2E377CE70052EFD7 /* wire_format.pb.swift in Sources */, EB950B792E521717002D68FA /* AttachmentDetails.swift in Sources */, @@ -999,6 +1026,7 @@ EB950B4A2E520436002D68FA /* securegcm.pb.swift in Sources */, EB950B4B2E520436002D68FA /* offline_wire_formats.pb.swift in Sources */, EB950B4C2E520436002D68FA /* InboundNearbyConnection.swift in Sources */, + EB16B6132E58B4B9007D9792 /* IdentityManager.swift in Sources */, EB950B4D2E520436002D68FA /* NearbyConnection.swift in Sources */, EB950B4F2E520436002D68FA /* Logger.swift in Sources */, EB950B502E520436002D68FA /* wire_format.pb.swift in Sources */, @@ -1011,6 +1039,8 @@ EB950B562E520436002D68FA /* DataModel.swift in Sources */, EB950B3E2E52038A002D68FA /* SendModel.swift in Sources */, EB950B772E521712002D68FA /* AttachmentDetails.swift in Sources */, + EB16B6192E58B4C8007D9792 /* TrustStore.swift in Sources */, + EB16B7772E59BAA1007D9792 /* AppGroup.swift in Sources */, EB950B3F2E52038A002D68FA /* FilePicker.swift in Sources */, EB950B402E52038A002D68FA /* Configuration.swift in Sources */, EB950B412E52038A002D68FA /* PieProgressView.swift in Sources */, diff --git a/QuickDrop/Localizable.xcstrings b/QuickDrop/Localizable.xcstrings index 935a24e..8349680 100644 --- a/QuickDrop/Localizable.xcstrings +++ b/QuickDrop/Localizable.xcstrings @@ -214,6 +214,113 @@ } } }, + "AlwaysAccept" : { + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "قبول دائمًا" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Immer annehmen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Always Accept" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aceptar siempre" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Toujours accepter" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mindig elfogad" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accetta sempre" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "常に受け入れる" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "항상 수락" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Altijd Accepteren" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sempre Aceitar" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aceitar Sempre" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acceptă întotdeauna" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Всегда принимать" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "ยอมรับเสมอ" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Завжди приймати" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "始终接受" + } + } + } + }, "AndroidApp" : { "extractionState" : "manual", "localizations" : { @@ -856,6 +963,113 @@ } } }, + "AutoAcceptFromThisDevice" : { + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "قبول تلقائي من هذا الجهاز" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatisch von diesem Gerät annehmen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatically accept from this device" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aceptar automáticamente desde este dispositivo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accepter automatiquement depuis cet appareil" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatikusan fogadás erről az eszközről" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accetta automaticamente da questo dispositivo" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このデバイスから自動的に受け入れる" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 장치로부터 자동으로 수락" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatisch accepteren van dit apparaat" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aceitar automaticamente deste dispositivo" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aceitar automaticamente deste dispositivo" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Acceptă automat de pe acest dispozitiv" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Автоматически принимать файлы с этого устройства" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "ยอมรับอัตโนมัติจากอุปกรณ์นี้" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Автоматично приймати з цього пристрою" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "自动接受来自此设备的文件" + } + } + } + }, "AutomaticallyAcceptFiles" : { "localizations" : { "ar" : { diff --git a/QuickDropiOS/Configuration.swift b/QuickDropiOS/Configuration.swift index 909ee8a..73581fa 100644 --- a/QuickDropiOS/Configuration.swift +++ b/QuickDropiOS/Configuration.swift @@ -14,7 +14,7 @@ let configuration = LUIConfiguration( copyrightInformation: licenseText, - loggingUrl: getAppGroupDirectory(), + loggingUrl: AppGroup.appGroupDirectory, introductionViewInformation: "QuickDropWelcomeDescription", diff --git a/QuickDropiOS/ContentView.swift b/QuickDropiOS/ContentView.swift index 5bbfb11..9578051 100644 --- a/QuickDropiOS/ContentView.swift +++ b/QuickDropiOS/ContentView.swift @@ -133,7 +133,8 @@ struct DeviceListView: View { .font(.system(size: 14)) .foregroundColor(Color.primary.opacity(0.6)) - ProgressView() + LoadingIndicator() + .opacity(0.75) .frame(width: 12, height: 12) } .padding(.vertical, 16) diff --git a/QuickDropiOS/ReceiveModel.swift b/QuickDropiOS/ReceiveModel.swift index 439d138..2cb13b5 100644 --- a/QuickDropiOS/ReceiveModel.swift +++ b/QuickDropiOS/ReceiveModel.swift @@ -26,15 +26,13 @@ class ReceiveModel: ObservableObject, InboundAppDelegate { } - func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo) { + func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo, acceptAutomatically: Bool) { #if os(macOS) NSApp.activate(ignoringOtherApps: true) AudioManager.playIncomingFileSound() #endif - let acceptAutomatically = UserDefaults.standard.bool(forKey: UserDefaultsKeys.automaticallyAcceptFiles.rawValue) - let fileStr: String if let textTitle = transfer.textDescription { @@ -64,8 +62,8 @@ class ReceiveModel: ObservableObject, InboundAppDelegate { let pinCodeMessage = String(format: "PinCode".localized(), arguments: [transfer.pinCode ?? "?"]) let transferID = transfer.id - if acceptAutomatically { - pressAcceptButton(transferID: transfer.id) + if acceptAutomatically && isMac() { + pressAcceptButton(transferID: transfer.id, trustDevice: false) UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, _ in if granted { @@ -89,9 +87,9 @@ class ReceiveModel: ObservableObject, InboundAppDelegate { let title = "QuickDrop - \(pinCodeMessage)" let primaryButtonTitle = "Accept".localized() - let primaryButtonAction = { self.pressAcceptButton(transferID: transferID) } + let primaryButtonAction = { (trustDevice: Bool) in self.pressAcceptButton(transferID: transferID, trustDevice: trustDevice) } let secondaryButtonTitle = "Decline".localized() - let secondaryButtonAction = { NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: false, storeInTemp: false) } + let secondaryButtonAction = { NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: false, trustDevice: false, storeInTemp: false) } #if os(macOS) let alert = NSAlert() @@ -101,18 +99,32 @@ class ReceiveModel: ObservableObject, InboundAppDelegate { alert.informativeText = mainMessage alert.addButton(withTitle: primaryButtonTitle) alert.addButton(withTitle: secondaryButtonTitle) + + if transfer.allowsToBeAddedAsTrustedDevice { + alert.showsSuppressionButton = true + alert.suppressionButton?.title = "AutoAcceptFromThisDevice".localized() + } let result = alert.runModal() if result == .alertFirstButtonReturn { - primaryButtonAction() + primaryButtonAction(alert.suppressionButton?.state == .on) } else if result == .alertSecondButtonReturn { secondaryButtonAction() } #else // iOS - ProgressAlert.shared.askForUserPermission(title: title, message: mainMessage, acceptLabel: primaryButtonTitle, rejectLabel: secondaryButtonTitle) { accepted in - accepted ? primaryButtonAction() : secondaryButtonAction() + let alwaysAcceptLabel = transfer.allowsToBeAddedAsTrustedDevice ? "AlwaysAccept".localized() : nil + ProgressAlert.shared.askForUserPermission(title: title, message: mainMessage, acceptLabel: primaryButtonTitle, acceptAlwaysLabel: alwaysAcceptLabel, rejectLabel: secondaryButtonTitle, acceptAutomatically: acceptAutomatically) { accepted in + + switch accepted { + case .Accept: + primaryButtonAction(false) + case .AcceptAlways: + primaryButtonAction(true) + case .Decline: + secondaryButtonAction() + } } #endif } @@ -181,13 +193,13 @@ class ReceiveModel: ObservableObject, InboundAppDelegate { } - private func pressAcceptButton(transferID: String) { + private func pressAcceptButton(transferID: String, trustDevice: Bool) { if isFileTransferRestricted() { log("[ReceiveModel] Showing plus screen...") - NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: true, storeInTemp: true) + NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: true, trustDevice: trustDevice, storeInTemp: true) controlPlusScreen(true) } else { - NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: true, storeInTemp: false) + NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: true, trustDevice: trustDevice, storeInTemp: false) } } } diff --git a/QuickDropiOS/UIAlert.swift b/QuickDropiOS/UIAlert.swift index 30234c8..dfe33b5 100644 --- a/QuickDropiOS/UIAlert.swift +++ b/QuickDropiOS/UIAlert.swift @@ -18,23 +18,37 @@ final class ProgressAlert { private weak var progressView: UIProgressView? // MARK: - Initial Accept / Decline - func askForUserPermission(title: String, message: String, acceptLabel: String, rejectLabel: String, onAccept: @escaping (Bool) -> Void) { + func askForUserPermission(title: String, message: String, acceptLabel: String, acceptAlwaysLabel: String?, rejectLabel: String, acceptAutomatically: Bool, onAccept: @escaping (AcceptAction) -> Void) { guard let vc = topMostViewController() else { return } - let alert = UIAlertController(title: title, - message: message, - preferredStyle: .alert) - - alert.addAction(UIAlertAction(title: rejectLabel, style: .cancel, handler: { _ in - onAccept(false) - })) - - alert.addAction(UIAlertAction(title: acceptLabel, style: .default, handler: { _ in - onAccept(true) + if acceptAutomatically { + onAccept(.Accept) self.showProgressAlert(on: vc) - })) - - vc.present(alert, animated: true) + } + else { + + let alert = UIAlertController(title: title, + message: message, + preferredStyle: .alert) + + alert.addAction(UIAlertAction(title: rejectLabel, style: .cancel, handler: { _ in + onAccept(.Decline) + })) + + alert.addAction(UIAlertAction(title: acceptLabel, style: .default, handler: { _ in + onAccept(.Accept) + self.showProgressAlert(on: vc) + })) + + if let acceptAlways = acceptAlwaysLabel { + alert.addAction(UIAlertAction(title: acceptAlways, style: .default, handler: { _ in + onAccept(.AcceptAlways) + self.showProgressAlert(on: vc) + })) + } + + vc.present(alert, animated: true) + } } // MARK: - Progress Alert @@ -90,4 +104,11 @@ final class ProgressAlert { if let presented = base?.presentedViewController { return topMostViewController(base: presented) } return base } + + + enum AcceptAction { + case Accept + case AcceptAlways + case Decline + } }