376 lines
16 KiB
Swift
376 lines
16 KiB
Swift
//
|
|
// Copyright 2020 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import CryptoKit
|
|
import Foundation
|
|
import MultipeerConnectivity
|
|
import SignalServiceKit
|
|
|
|
extension DeviceTransferService: MCNearbyServiceBrowserDelegate {
|
|
func browser(_ browser: MCNearbyServiceBrowser, foundPeer newDevicePeerID: MCPeerID, withDiscoveryInfo info: [String: String]?) {
|
|
Logger.info("Notifying of discovered new device \(newDevicePeerID)")
|
|
notifyObservers { $0.deviceTransferServiceDiscoveredNewDevice(peerId: newDevicePeerID, discoveryInfo: info) }
|
|
}
|
|
|
|
func browser(_ browser: MCNearbyServiceBrowser, didNotStartBrowsingForPeers error: Swift.Error) {
|
|
Logger.warn("Failed to start browsing for peers \(error)")
|
|
}
|
|
|
|
func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerId: MCPeerID) {}
|
|
}
|
|
|
|
extension DeviceTransferService: MCNearbyServiceAdvertiserDelegate {
|
|
func advertiser(
|
|
_ advertiser: MCNearbyServiceAdvertiser,
|
|
didReceiveInvitationFromPeer peerId: MCPeerID,
|
|
withContext context: Data?,
|
|
invitationHandler: @escaping (Bool, MCSession?) -> Void
|
|
) {
|
|
Logger.info("Accepting invitation from old device \(peerId)")
|
|
invitationHandler(true, session)
|
|
}
|
|
|
|
func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didNotStartAdvertisingPeer error: Swift.Error) {
|
|
Logger.warn("Failed to start advertising for peers \(error)")
|
|
}
|
|
}
|
|
|
|
extension DeviceTransferService: MCSessionDelegate {
|
|
func session(_ session: MCSession, peer peerId: MCPeerID, didChange state: MCSessionState) {
|
|
// dispatch to main ASAP to free up the session's private thread to receive more bytes.
|
|
DispatchQueue.main.async {
|
|
Logger.debug("Connection to \(peerId) did change: \(state.rawValue)")
|
|
|
|
switch self.transferState {
|
|
case .outgoing(let newDevicePeerId, _, _, let transferredFiles, let progress):
|
|
// We only care about state changes for the device we're sending to.
|
|
guard peerId == newDevicePeerId else { return }
|
|
|
|
Logger.info("Connection to new device did change: \(state.rawValue)")
|
|
|
|
switch state {
|
|
case .connected:
|
|
self.notifyObservers { $0.deviceTransferServiceDidStartTransfer(progress: progress) }
|
|
|
|
// Only send the files if we haven't yet sent the manifest.
|
|
guard !transferredFiles.contains(DeviceTransferService.manifestIdentifier) else { return }
|
|
|
|
do {
|
|
try self.sendManifest().done {
|
|
try self.sendAllFiles()
|
|
}.catch { error in
|
|
self.failTransfer(.assertion, "Failed to send manifest to new device \(error)")
|
|
}
|
|
} catch {
|
|
self.failTransfer(.assertion, "Failed to send manifest to new device \(error)")
|
|
}
|
|
case .connecting:
|
|
break
|
|
case .notConnected:
|
|
self.failTransfer(.assertion, "Lost connection to new device")
|
|
@unknown default:
|
|
self.failTransfer(.assertion, "Unexpected connection state: \(state.rawValue)")
|
|
}
|
|
case .incoming(let oldDevicePeerId, _, _, _, _):
|
|
// We only care about state changes for the device we're receiving from.
|
|
guard peerId == oldDevicePeerId else { return }
|
|
|
|
if state == .notConnected { self.failTransfer(.assertion, "Lost connection to old device") }
|
|
case .idle:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func session(_ session: MCSession, didReceive data: Data, fromPeer peerId: MCPeerID) {
|
|
switch transferState {
|
|
case .idle:
|
|
break
|
|
case .outgoing(let newDevicePeerId, _, _, _, _):
|
|
guard peerId == newDevicePeerId else {
|
|
return owsFailDebug("Ignoring data from unexpected peer \(peerId)")
|
|
}
|
|
|
|
switch data {
|
|
case DeviceTransferService.backgroundAppMessage:
|
|
return failTransfer(.backgroundedDevice, "Received terminate message")
|
|
case DeviceTransferService.doneMessage:
|
|
break
|
|
default:
|
|
return failTransfer(.assertion, "Received unexpected data")
|
|
}
|
|
|
|
// Notify the UI that the transfer completed successfully.
|
|
notifyObservers { $0.deviceTransferServiceDidEndTransfer(error: nil) }
|
|
|
|
stopTransfer()
|
|
|
|
// When the old device receives the done message from the new device,
|
|
// it can be confident that the transfer has completed successfully and
|
|
// clear out all data from this device. This will crash the app.
|
|
Task { @MainActor in
|
|
SignalApp.resetAppData(keyFetcher: SSKEnvironment.shared.databaseStorageRef.keyFetcher)
|
|
SignalApp.showTransferCompleteAndExit()
|
|
}
|
|
|
|
case .incoming(let oldDevicePeerId, _, let receivedFileIds, let skippedFileIds, _):
|
|
guard peerId == oldDevicePeerId else {
|
|
return owsFailDebug("Ignoring data from unexpected peer \(peerId)")
|
|
}
|
|
|
|
switch data {
|
|
case DeviceTransferService.backgroundAppMessage:
|
|
return failTransfer(.backgroundedDevice, "Received backgrounded message")
|
|
case DeviceTransferService.doneMessage:
|
|
break
|
|
default:
|
|
return failTransfer(.assertion, "Received unexpected data")
|
|
}
|
|
|
|
stopThroughputCalculation()
|
|
|
|
// When the new device receives the done message from the old device,
|
|
// it indicates that the old device thinks we should have received
|
|
// everything at this point.
|
|
|
|
guard verifyTransferCompletedSuccessfully(
|
|
receivedFileIds: receivedFileIds,
|
|
skippedFileIds: skippedFileIds
|
|
) else {
|
|
return failTransfer(.assertion, "transfer is missing data")
|
|
}
|
|
|
|
// Record that we have a pending restore, so even if the app exits
|
|
// we can still know to restore the data that was transferred.
|
|
let startPhase = RestorationPhase.start
|
|
Logger.info("Setting restoration phase to: \(startPhase)")
|
|
rawRestorationPhase = startPhase.rawValue
|
|
|
|
// Try and notify the old device that we agree, everything is done.
|
|
// At this point, we consider the transfer complete regardless of
|
|
// whether or not this message is received by the old device. If the
|
|
// old device misses this message (because the app crashes, etc.) it
|
|
// will continue acting as if it is "unregistered", but it won't delete
|
|
// all data because it doesn't know for sure if the data was safely
|
|
// received by the new device.
|
|
do {
|
|
try sendDoneMessage(to: oldDevicePeerId)
|
|
} catch {
|
|
owsFailDebug("Failed to send done message to old device \(error)")
|
|
}
|
|
|
|
// Notify the UI that the transfer completed successfully.
|
|
notifyObservers { $0.deviceTransferServiceDidEndTransfer(error: nil) }
|
|
|
|
// Try and restore the received data. If for some reason the app exits
|
|
// or crashes at this point, we will retry the restore when the app next
|
|
// launches.
|
|
do {
|
|
try restoreTransferredData()
|
|
} catch {
|
|
owsFail("Restore failed. Will try again on next launch. Error: \(error)")
|
|
}
|
|
|
|
stopTransfer(notifyRegState: false)
|
|
|
|
Logger.info("Transfer complete")
|
|
|
|
DispatchQueue.main.async {
|
|
self.notifyObservers { $0.deviceTransferServiceDidRequestAppRelaunch() }
|
|
}
|
|
}
|
|
}
|
|
|
|
func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerId: MCPeerID) {}
|
|
|
|
func session(
|
|
_ session: MCSession,
|
|
didStartReceivingResourceWithName resourceName: String,
|
|
fromPeer peerId: MCPeerID,
|
|
with fileProgress: Progress
|
|
) {
|
|
switch transferState {
|
|
case .idle:
|
|
guard resourceName == DeviceTransferService.manifestIdentifier else {
|
|
return Logger.info("Ignoring unexpected incoming file \(resourceName)")
|
|
}
|
|
case .outgoing:
|
|
owsFailDebug("Unexpectedly received a file on old device \(resourceName)")
|
|
case .incoming(let oldDevicePeerId, let manifest, let receivedFileIds, let skippedFileIds, let progress):
|
|
guard peerId == oldDevicePeerId else {
|
|
return owsFailDebug("Ignoring file from unexpected peer \(peerId)")
|
|
}
|
|
|
|
let nameComponents = resourceName.components(separatedBy: " ")
|
|
|
|
guard let fileIdentifier = nameComponents.first, nameComponents.count == 2 else {
|
|
return owsFailDebug("Received incorrectly formatted resourceName: \(resourceName)")
|
|
}
|
|
|
|
guard !receivedFileIds.contains(fileIdentifier) else {
|
|
return Logger.info("Ignoring duplicate file: \(fileIdentifier)")
|
|
}
|
|
|
|
guard !skippedFileIds.contains(fileIdentifier) else {
|
|
return Logger.info("Ignoring previously skipped file: \(fileIdentifier)")
|
|
}
|
|
|
|
guard let file: DeviceTransferProtoFile = {
|
|
switch fileIdentifier {
|
|
case DeviceTransferService.databaseIdentifier:
|
|
return manifest.database?.database
|
|
case DeviceTransferService.databaseWALIdentifier:
|
|
return manifest.database?.wal
|
|
default:
|
|
return manifest.files.first(where: { $0.identifier == fileIdentifier })
|
|
}
|
|
}() else {
|
|
return owsFailDebug("Received unexpected file on new device: \(fileIdentifier)")
|
|
}
|
|
|
|
Logger.info("Receiving file: \(file.identifier), estimatedSize: \(file.estimatedSize)")
|
|
progress.addChild(fileProgress, withPendingUnitCount: Int64(file.estimatedSize))
|
|
}
|
|
}
|
|
|
|
func session(
|
|
_ session: MCSession,
|
|
didFinishReceivingResourceWithName resourceName: String,
|
|
fromPeer peerId: MCPeerID,
|
|
at localURL: URL?,
|
|
withError error: Swift.Error?
|
|
) {
|
|
switch transferState {
|
|
case .idle:
|
|
guard resourceName == DeviceTransferService.manifestIdentifier else {
|
|
return Logger.info("Ignoring unexpected incoming file \(resourceName)")
|
|
}
|
|
|
|
if let error = error {
|
|
owsFailDebug("Failed to receive manifest \(error)")
|
|
} else if let localURL = localURL {
|
|
handleReceivedManifest(at: localURL, fromPeer: peerId)
|
|
} else {
|
|
owsFailDebug("Unexpectedly completed transfer of resource with no URL or error")
|
|
}
|
|
case .outgoing:
|
|
owsFailDebug("Unexpectedly received a file on old device \(resourceName)")
|
|
case .incoming(let oldDevicePeerId, let manifest, let receivedFileIds, let skippedFileIds, _):
|
|
guard peerId == oldDevicePeerId else {
|
|
return owsFailDebug("Ignoring file from unexpected peer \(peerId)")
|
|
}
|
|
|
|
let nameComponents = resourceName.components(separatedBy: " ")
|
|
|
|
guard let fileIdentifier = nameComponents.first, let fileHash = nameComponents.last, nameComponents.count == 2 else {
|
|
return owsFailDebug("Received incorrectly formatted resourceName: \(resourceName)")
|
|
}
|
|
|
|
guard !receivedFileIds.contains(fileIdentifier) else {
|
|
return Logger.info("Ignoring duplicate file: \(fileIdentifier)")
|
|
}
|
|
|
|
guard !skippedFileIds.contains(fileIdentifier) else {
|
|
return Logger.info("Ignoring previously skipped file: \(fileIdentifier)")
|
|
}
|
|
|
|
guard let file: DeviceTransferProtoFile = {
|
|
switch fileIdentifier {
|
|
case DeviceTransferService.databaseIdentifier:
|
|
return manifest.database?.database
|
|
case DeviceTransferService.databaseWALIdentifier:
|
|
return manifest.database?.wal
|
|
default:
|
|
return manifest.files.first(where: { $0.identifier == fileIdentifier })
|
|
}
|
|
}() else {
|
|
return owsFailDebug("Received unexpected file on new device: \(fileIdentifier)")
|
|
}
|
|
|
|
if let error = error {
|
|
failTransfer(.assertion, "Failed to receive file \(file.identifier) \(error)")
|
|
} else if let localURL = localURL {
|
|
OWSFileSystem.ensureDirectoryExists(DeviceTransferService.pendingTransferFilesDirectory.path)
|
|
|
|
guard let computedHash = try? Cryptography.computeSHA256DigestOfFile(at: localURL) else {
|
|
return failTransfer(.assertion, "Failed to compute hash for \(file.identifier)")
|
|
}
|
|
|
|
guard computedHash.hexadecimalString == fileHash else {
|
|
return failTransfer(.assertion, "Received file with incorrect hash \(file.identifier)")
|
|
}
|
|
|
|
guard computedHash != DeviceTransferService.missingFileHash else {
|
|
Logger.warn("Received notification of missing file: \(file.identifier), skipping.")
|
|
transferState = transferState.appendingSkippedFileId(file.identifier)
|
|
return
|
|
}
|
|
|
|
do {
|
|
try OWSFileSystem.moveFilePath(
|
|
localURL.path,
|
|
toFilePath: URL(
|
|
fileURLWithPath: file.identifier,
|
|
relativeTo: DeviceTransferService.pendingTransferFilesDirectory
|
|
).path
|
|
)
|
|
} catch {
|
|
Logger.warn("Couldn't move file: \(error.shortDescription)")
|
|
return failTransfer(.assertion, "Failed to move file into place \(file.identifier)")
|
|
}
|
|
|
|
Logger.info("Received file: \(file.identifier)")
|
|
transferState = transferState.appendingFileId(file.identifier)
|
|
} else {
|
|
owsFailDebug("Unexpectedly completed transfer of resource with no URL or error")
|
|
}
|
|
}
|
|
}
|
|
|
|
func session(
|
|
_ session: MCSession,
|
|
didReceiveCertificate certificates: [Any]?,
|
|
fromPeer peerId: MCPeerID,
|
|
certificateHandler: @escaping (Bool) -> Void
|
|
) {
|
|
var certificateIsTrusted = false
|
|
|
|
defer {
|
|
certificateHandler(certificateIsTrusted)
|
|
if !certificateIsTrusted {
|
|
self.failTransfer(.certificateMismatch, "the received certificate did not match the expected certificate")
|
|
}
|
|
}
|
|
|
|
guard case .outgoing(let newDevicePeerId, let expectedCertificateHash, _, _, _) = transferState else {
|
|
// Accept all connections if we're not doing an outgoing transfer AND we aren't yet registered.
|
|
// Registered devices can only ever perform outgoing transfers.
|
|
certificateIsTrusted = !DependenciesBridge.shared.tsAccountManager.registrationStateWithMaybeSneakyTransaction.isRegistered
|
|
return
|
|
}
|
|
|
|
// Reject any connections from unexpected devices.
|
|
guard peerId == newDevicePeerId else { return }
|
|
|
|
// Verify the received certificate matches the expected certificate.
|
|
guard let certificate = certificates?.first else {
|
|
return owsFailDebug("new connection did not provide any certificate")
|
|
}
|
|
|
|
let certificateData = SecCertificateCopyData(certificate as! SecCertificate) as Data
|
|
|
|
// Reject any connections where we can't compute the certificate hash
|
|
let certificateHash = Data(SHA256.hash(data: certificateData))
|
|
|
|
// Reject any connections where the certificate doesn't match the expected certificate
|
|
guard expectedCertificateHash.ows_constantTimeIsEqual(to: certificateHash) else {
|
|
return owsFailDebug("connection from known peer \(peerId) using unexpected certificate")
|
|
}
|
|
|
|
Logger.info("Successfully verified new device certificate \(peerId)")
|
|
|
|
certificateIsTrusted = true
|
|
}
|
|
}
|