Add Trusted Devices Authentication

This commit is contained in:
Leon Böttger committed 2025-08-23 11:52:14 +02:00
1 parent 78df7fe227
commit 48164b8095
14 files changed
+621 -70

No files matched your search

@@ -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)
@@ -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)
}
@@ -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()
+5 -3
View File
@@ -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 {
+21
View File
@@ -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)
}
}
+119
View File
@@ -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)
}
}
+2 -9
View File
@@ -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)
}
+33
View File
@@ -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)
}
}
+30
View File
@@ -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 = "<group>"; };
69DA9A2C29E18CB500A442DA /* securegcm.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = securegcm.pb.swift; sourceTree = "<group>"; };
69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = securemessage.pb.swift; sourceTree = "<group>"; };
EB16B6112E58B4B9007D9792 /* IdentityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityManager.swift; sourceTree = "<group>"; };
EB16B6162E58B4C8007D9792 /* TrustStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrustStore.swift; sourceTree = "<group>"; };
EB16B7762E59BA9F007D9792 /* AppGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroup.swift; sourceTree = "<group>"; };
EB2B41E32DC63AC80070FC39 /* SleepManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SleepManager.swift; sourceTree = "<group>"; };
EB2F9BFA2DA04B67004010A2 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = "<group>"; };
EB36435F2D73A29F009CDFF8 /* AudioManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioManager.swift; sourceTree = "<group>"; };
@@ -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 */,
+214
View File
@@ -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" : {
+1 -1
View File
@@ -14,7 +14,7 @@ let configuration = LUIConfiguration(
copyrightInformation: licenseText,
loggingUrl: getAppGroupDirectory(),
loggingUrl: AppGroup.appGroupDirectory,
introductionViewInformation: "QuickDropWelcomeDescription",
+2 -1
View File
@@ -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)
+25 -13
View File
@@ -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)
}
}
}
+35 -14
View File
@@ -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
}
}