From a5f067936c6fb05d8f22584c0d977d56c25cd060 Mon Sep 17 00:00:00 2001
From: Michael Kirk <michael.code@endoftheworl.de>
Date: Mon, 24 Jul 2017 10:14:58 -0400
Subject: [PATCH] migration to fix any half-registered users

// FREEBIE
---
 Podfile.lock                                  |   2 +-
 Signal.xcodeproj/project.pbxproj              |   4 +
 Signal/src/ProfileFetcherJob.swift            |  93 +++++++++------
 Signal/src/Signal-Bridging-Header.h           |   4 +-
 .../Migrations/OWSDatabaseMigrationRunner.m   |   4 +-
 .../OWS106EnsureProfileComplete.swift         | 110 ++++++++++++++++++
 Signal/src/util/WeakTimer.swift               |   4 +-
 .../src/Storage/TSYapDatabaseObject.m         |   9 ++
 8 files changed, 188 insertions(+), 42 deletions(-)
 create mode 100644 Signal/src/environment/OWS106EnsureProfileComplete.swift

diff --git a/Podfile.lock b/Podfile.lock
index 153b82414..af8dda1fe 100644
--- a/Podfile.lock
+++ b/Podfile.lock
@@ -125,7 +125,7 @@ EXTERNAL SOURCES:
     :branch: signal-master
     :git: https://github.com/WhisperSystems/JSQMessagesViewController.git
   SignalServiceKit:
-    :path: .
+    :path: "."
   SocketRocket:
     :git: https://github.com/facebook/SocketRocket.git
 
diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj
index 5dc3e4d92..42d0adefd 100644
--- a/Signal.xcodeproj/project.pbxproj
+++ b/Signal.xcodeproj/project.pbxproj
@@ -134,6 +134,7 @@
 		4542F0961EBB9E9A00C7EE92 /* Promise+retainUntilComplete.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4542F0951EBB9E9A00C7EE92 /* Promise+retainUntilComplete.swift */; };
 		4542F0971EBB9E9A00C7EE92 /* Promise+retainUntilComplete.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4542F0951EBB9E9A00C7EE92 /* Promise+retainUntilComplete.swift */; };
 		45464DBC1DFA041F001D3FD6 /* DataChannelMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45464DBB1DFA041F001D3FD6 /* DataChannelMessage.swift */; };
+		4563ADF11F22BD7100DEB8C7 /* OWS106EnsureProfileComplete.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4563ADF01F22BD7100DEB8C7 /* OWS106EnsureProfileComplete.swift */; };
 		45666EC61D99483D008FE134 /* OWSAvatarBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 45666EC51D99483D008FE134 /* OWSAvatarBuilder.m */; };
 		45666EC91D994C0D008FE134 /* OWSGroupAvatarBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 45666EC81D994C0D008FE134 /* OWSGroupAvatarBuilder.m */; };
 		45666F561D9B2827008FE134 /* OWSScrubbingLogFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 45666F551D9B2827008FE134 /* OWSScrubbingLogFormatter.m */; };
@@ -566,6 +567,7 @@
 		4542F0951EBB9E9A00C7EE92 /* Promise+retainUntilComplete.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Promise+retainUntilComplete.swift"; sourceTree = "<group>"; };
 		45464DBB1DFA041F001D3FD6 /* DataChannelMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataChannelMessage.swift; sourceTree = "<group>"; };
 		454B35071D08EED80026D658 /* mk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mk; path = translations/mk.lproj/Localizable.strings; sourceTree = "<group>"; };
+		4563ADF01F22BD7100DEB8C7 /* OWS106EnsureProfileComplete.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWS106EnsureProfileComplete.swift; sourceTree = "<group>"; };
 		45666EC41D99483D008FE134 /* OWSAvatarBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSAvatarBuilder.h; sourceTree = "<group>"; };
 		45666EC51D99483D008FE134 /* OWSAvatarBuilder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSAvatarBuilder.m; sourceTree = "<group>"; };
 		45666EC71D994C0D008FE134 /* OWSGroupAvatarBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSGroupAvatarBuilder.h; sourceTree = "<group>"; };
@@ -1130,6 +1132,7 @@
 				45666F7A1D9C0533008FE134 /* OWSDatabaseMigration.m */,
 				45666F7C1D9C0814008FE134 /* OWSDatabaseMigrationRunner.h */,
 				45666F7D1D9C0814008FE134 /* OWSDatabaseMigrationRunner.m */,
+				4563ADF01F22BD7100DEB8C7 /* OWS106EnsureProfileComplete.swift */,
 			);
 			name = Migrations;
 			sourceTree = "<group>";
@@ -2144,6 +2147,7 @@
 				34E3EF101EFC2684007F6822 /* DebugUIPage.m in Sources */,
 				76EB058A18170B33006006FC /* Release.m in Sources */,
 				45D231771DC7E8F10034FA89 /* SessionResetJob.swift in Sources */,
+				4563ADF11F22BD7100DEB8C7 /* OWS106EnsureProfileComplete.swift in Sources */,
 				450873C71D9D867B006B54F2 /* OWSIncomingMessageCollectionViewCell.m in Sources */,
 				76EB057A18170B33006006FC /* OWSContactsManager.m in Sources */,
 				76EB064218170B33006006FC /* StringUtil.m in Sources */,
diff --git a/Signal/src/ProfileFetcherJob.swift b/Signal/src/ProfileFetcherJob.swift
index c53a39701..aebf6702f 100644
--- a/Signal/src/ProfileFetcherJob.swift
+++ b/Signal/src/ProfileFetcherJob.swift
@@ -3,6 +3,7 @@
 //
 
 import Foundation
+import PromiseKit
 
 @objc
 class ProfileFetcherJob: NSObject {
@@ -12,34 +13,54 @@ class ProfileFetcherJob: NSObject {
     let networkManager: TSNetworkManager
     let storageManager: TSStorageManager
 
-    let thread: TSThread
-
     // This property is only accessed on the main queue.
     static var fetchDateMap = [String: Date]()
 
     public class func run(thread: TSThread, networkManager: TSNetworkManager) {
-        ProfileFetcherJob(thread: thread, networkManager: networkManager).run()
+        ProfileFetcherJob(networkManager: networkManager).run(thread: thread)
     }
 
-    init(thread: TSThread, networkManager: TSNetworkManager) {
+    init(networkManager: TSNetworkManager) {
         self.networkManager = networkManager
         self.storageManager = TSStorageManager.shared()
-
-        self.thread = thread
     }
 
-    public func run() {
+    public func run(thread: TSThread) {
         AssertIsOnMainThread()
 
         DispatchQueue.main.async {
-            for recipientId in self.thread.recipientIdentifiers {
-                self.getProfile(recipientId: recipientId)
+            for recipientId in thread.recipientIdentifiers {
+                self.updateProfile(recipientId: recipientId)
             }
         }
     }
 
-    public func getProfile(recipientId: String, remainingRetries: Int = 3) {
+    enum ProfileFetcherJobError: Error {
+        case throttled(lastTimeInterval: TimeInterval),
+             unknownNetworkError
+    }
+
+    public func updateProfile(recipientId: String, remainingRetries: Int = 3) {
+        self.getProfile(recipientId: recipientId).then { profile in
+            self.updateProfile(signalServiceProfile: profile)
+        }.catch { error in
+            switch error {
+            case ProfileFetcherJobError.throttled(let lastTimeInterval):
+                Logger.info("\(self.TAG) skipping updateProfile: \(recipientId), lastTimeInterval: \(lastTimeInterval)")
+            case let error as SignalServiceProfile.ValidationError:
+                Logger.warn("\(self.TAG) skipping updateProfile retry. Invalid profile for: \(recipientId) error: \(error)")
+            default:
+                if remainingRetries > 0 {
+                    self.updateProfile(recipientId: recipientId, remainingRetries: remainingRetries - 1)
+                } else {
+                    owsFail("\(self.TAG) in \(#function) failed to get profile with error: \(error)")
+                }
+            }
+        }.retainUntilComplete()
+    }
 
+    public func getProfile(recipientId: String) -> Promise<SignalServiceProfile> {
+        AssertIsOnMainThread()
         if let lastDate = ProfileFetcherJob.fetchDateMap[recipientId] {
             let lastTimeInterval = fabs(lastDate.timeIntervalSinceNow)
             // Don't check a profile more often than every N minutes.
@@ -48,8 +69,7 @@ class ProfileFetcherJob: NSObject {
             // facilitate debugging.
             let kGetProfileMaxFrequencySeconds = _isDebugAssertConfiguration() ? 0 : 60.0 * 5.0
             guard lastTimeInterval > kGetProfileMaxFrequencySeconds else {
-                Logger.info("\(self.TAG) skipping getProfile: \(recipientId), lastTimeInterval: \(lastTimeInterval)")
-                return
+                return Promise(error: ProfileFetcherJobError.throttled(lastTimeInterval: lastTimeInterval))
             }
         }
         ProfileFetcherJob.fetchDateMap[recipientId] = Date()
@@ -58,33 +78,31 @@ class ProfileFetcherJob: NSObject {
 
         let request = OWSGetProfileRequest(recipientId: recipientId)
 
+        let (promise, fulfill, reject) = Promise<SignalServiceProfile>.pending()
+
         self.networkManager.makeRequest(
             request,
             success: { (_: URLSessionDataTask?, responseObject: Any?) -> Void in
-                guard let profileResponse = SignalServiceProfile(recipientId: recipientId, rawResponse: responseObject) else {
-                    owsFail("\(self.TAG) response object had unexpected content")
-                    return
+                do {
+                    let profile = try SignalServiceProfile(recipientId: recipientId, rawResponse: responseObject)
+                    fulfill(profile)
+                } catch {
+                    reject(error)
                 }
-
-                self.processResponse(signalServiceProfile: profileResponse)
         },
             failure: { (_: URLSessionDataTask?, error: Error?) in
-                guard let error = error else {
-                    owsFail("\(self.TAG) error in \(#function) was surpringly nil. sheesh rough day.")
-                    return
-                }
-
-                Logger.error("\(self.TAG) failed to fetch profile for recipient: \(recipientId) with error: \(error)")
 
-                if remainingRetries > 1 {
-                    DispatchQueue.global().async {
-                        self.getProfile(recipientId: recipientId, remainingRetries:remainingRetries - 1)
-                    }
+                if let error = error {
+                    reject(error)
                 }
+
+                reject(ProfileFetcherJobError.unknownNetworkError)
         })
+
+        return promise
     }
 
-    private func processResponse(signalServiceProfile: SignalServiceProfile) {
+    private func updateProfile(signalServiceProfile: SignalServiceProfile) {
         verifyIdentityUpToDateAsync(recipientId: signalServiceProfile.recipientId, latestIdentityKey: signalServiceProfile.identityKey)
 
         // Eventually we'll want to do more things with new SignalServiceProfile fields here.
@@ -105,31 +123,32 @@ class ProfileFetcherJob: NSObject {
 struct SignalServiceProfile {
     let TAG = "[SignalServiceProfile]"
 
+    enum ValidationError: Error {
+        case invalid(description: String)
+        case invalidIdentityKey(description: String)
+    }
+
     public let recipientId: String
     public let identityKey: Data
 
-    init?(recipientId: String, rawResponse: Any?) {
+    init(recipientId: String, rawResponse: Any?) throws {
         self.recipientId = recipientId
 
         guard let responseDict = rawResponse as? [String: Any?] else {
-            Logger.error("\(TAG) unexpected type: \(String(describing: rawResponse))")
-            return nil
+            throw ValidationError.invalid(description: "\(TAG) unexpected type: \(String(describing: rawResponse))")
         }
 
         guard let identityKeyString = responseDict["identityKey"] as? String else {
-            Logger.error("\(TAG) missing identity key: \(String(describing: rawResponse))")
-            return nil
+            throw ValidationError.invalidIdentityKey(description: "\(TAG) missing identity key: \(String(describing: rawResponse))")
         }
 
         guard let identityKeyWithType = Data(base64Encoded: identityKeyString) else {
-            Logger.error("\(TAG) unable to parse identity key: \(identityKeyString)")
-            return nil
+            throw ValidationError.invalidIdentityKey(description: "\(TAG) unable to parse identity key: \(identityKeyString)")
         }
 
         let kIdentityKeyLength = 33
         guard identityKeyWithType.count == kIdentityKeyLength else {
-            Logger.error("\(TAG) malformed key \(identityKeyString) with decoded length: \(identityKeyWithType.count)")
-            return nil
+            throw ValidationError.invalidIdentityKey(description: "\(TAG) malformed key \(identityKeyString) with decoded length: \(identityKeyWithType.count)")
         }
 
         // `removeKeyType` is an objc category method only on NSData, so temporarily cast.
diff --git a/Signal/src/Signal-Bridging-Header.h b/Signal/src/Signal-Bridging-Header.h
index b59c9cac9..8b8509f74 100644
--- a/Signal/src/Signal-Bridging-Header.h
+++ b/Signal/src/Signal-Bridging-Header.h
@@ -4,7 +4,6 @@
 
 #import <Foundation/Foundation.h>
 
-#import "TSMessageAdapter.h"
 #import "AttachmentSharing.h"
 #import "Environment.h"
 #import "FLAnimatedImage.h"
@@ -15,6 +14,7 @@
 #import "OWSCallNotificationsAdaptee.h"
 #import "OWSContactAvatarBuilder.h"
 #import "OWSContactsManager.h"
+#import "OWSDatabaseMigration.h"
 #import "OWSLogger.h"
 #import "OWSMessageEditing.h"
 #import "OWSProgressView.h"
@@ -24,6 +24,7 @@
 #import "PushManager.h"
 #import "SettingsTableViewController.h"
 #import "SignalsViewController.h"
+#import "TSMessageAdapter.h"
 #import "UIColor+OWS.h"
 #import "UIFont+OWS.h"
 #import "UIUtil.h"
@@ -77,6 +78,7 @@
 #import <SignalServiceKit/TSInfoMessage.h>
 #import <SignalServiceKit/TSMessagesManager.h>
 #import <SignalServiceKit/TSNetworkManager.h>
+#import <SignalServiceKit/TSPreKeyManager.h>
 #import <SignalServiceKit/TSSocketManager.h>
 #import <SignalServiceKit/TSStorageManager+Calling.h>
 #import <SignalServiceKit/TSStorageManager+SessionStore.h>
diff --git a/Signal/src/environment/Migrations/OWSDatabaseMigrationRunner.m b/Signal/src/environment/Migrations/OWSDatabaseMigrationRunner.m
index f3815bba1..527faee6b 100644
--- a/Signal/src/environment/Migrations/OWSDatabaseMigrationRunner.m
+++ b/Signal/src/environment/Migrations/OWSDatabaseMigrationRunner.m
@@ -8,6 +8,7 @@
 #import "OWS103EnableVideoCalling.h"
 #import "OWS104CreateRecipientIdentities.h"
 #import "OWS105AttachmentFilePaths.h"
+#import "Signal-Swift.h"
 
 NS_ASSUME_NONNULL_BEGIN
 
@@ -32,7 +33,8 @@ NS_ASSUME_NONNULL_BEGIN
         [[OWS102MoveLoggingPreferenceToUserDefaults alloc] initWithStorageManager:self.storageManager],
         [[OWS103EnableVideoCalling alloc] initWithStorageManager:self.storageManager],
         // OWS104CreateRecipientIdentities is run separately. See runSafeBlockingMigrations.
-        [[OWS105AttachmentFilePaths alloc] initWithStorageManager:self.storageManager]
+        [[OWS105AttachmentFilePaths alloc] initWithStorageManager:self.storageManager],
+        [[OWS106EnsureProfileComplete alloc] initWithStorageManager:self.storageManager]
     ];
 }
 
diff --git a/Signal/src/environment/OWS106EnsureProfileComplete.swift b/Signal/src/environment/OWS106EnsureProfileComplete.swift
new file mode 100644
index 000000000..8c930c205
--- /dev/null
+++ b/Signal/src/environment/OWS106EnsureProfileComplete.swift
@@ -0,0 +1,110 @@
+//
+//  Copyright (c) 2017 Open Whisper Systems. All rights reserved.
+//
+
+import Foundation
+import PromiseKit
+
+class OWS106EnsureProfileComplete: OWSDatabaseMigration {
+
+    let TAG = "[OWS106EnsureProfileComplete]"
+
+    // increment a similar constant for each migration.
+    class func migrationId() -> String {
+        return "106"
+    }
+
+    // Overriding runUp since we have some specific completion criteria which
+    // is more likely to fail since it involves network requests.
+    override func runUp() {
+        CompleteRegistrationFixerJob(completionHandler: {
+            Logger.info("\(self.TAG) Completed. Saving.")
+            self.save()
+        }).start()
+    }
+
+    /**
+     * A previous client bug made it possible for re-registering users to register their new account
+     * but never upload new pre-keys. The symptom is that there will be accounts with no uploaded
+     * identity key. We detect that here and fix the situation
+     */
+    private class CompleteRegistrationFixerJob {
+
+        let TAG = "[CompleteRegistrationFixerJob]"
+
+        // Duration between retries if update fails.
+        static let kRetryInterval: TimeInterval = 5 * 60
+
+        var timer: Timer?
+        let completionHandler: () -> Void
+
+        init (completionHandler: @escaping () -> Void) {
+            self.completionHandler = completionHandler
+        }
+
+        func start() {
+            assert(self.timer == nil)
+
+            let timer = WeakTimer.scheduledTimer(timeInterval: CompleteRegistrationFixerJob.kRetryInterval, target: self, userInfo: nil, repeats: true) { [weak self] timer in
+                guard let strongSelf = self else {
+                    return
+                }
+
+                var isCompleted = false
+                strongSelf.ensureProfileComplete().then { _ -> Void in
+                    guard isCompleted == false else {
+                        Logger.info("Already saved. Skipping redundant call.")
+                        return
+                    }
+                    Logger.info("\(strongSelf.TAG) complete. Canceling timer and saving.")
+                    isCompleted = true
+                    timer.invalidate()
+                    strongSelf.completionHandler()
+                }.catch { error in
+                    Logger.error("\(strongSelf.TAG) failed with \(error). We'll try again in \(CompleteRegistrationFixerJob.kRetryInterval) seconds.")
+                }.retainUntilComplete()
+            }
+            self.timer = timer
+
+            timer.fire()
+        }
+
+        func ensureProfileComplete() -> Promise<Void> {
+            guard let localRecipientId = TSAccountManager.localNumber() else {
+                // local app doesn't think we're registered, so nothing to worry about.
+                return Promise(value: ())
+            }
+
+            let (promise, fulfill, reject) = Promise<Void>.pending()
+
+            guard let networkManager = Environment.getCurrent().networkManager else {
+                owsFail("\(TAG) network manager was unexpectedly not set")
+                return Promise(error: OWSErrorMakeAssertionError())
+            }
+
+            ProfileFetcherJob(networkManager: networkManager).getProfile(recipientId: localRecipientId).then { _ -> Void in
+                Logger.info("\(self.TAG) verified recipient profile is in good shape: \(localRecipientId)")
+
+                fulfill()
+            }.catch { error in
+                switch error {
+                case SignalServiceProfile.ValidationError.invalidIdentityKey(let description):
+                    Logger.warn("\(self.TAG) detected incomplete profile for \(localRecipientId) error: \(description)")
+                    // This is the error condition we're looking for. Update prekeys to properly set the identity key, completing registration.
+                    TSPreKeyManager.registerPreKeys(with: .signedAndOneTime,
+                                                    success: {
+                                                        Logger.info("\(self.TAG) successfully uploaded pre-keys. Profile should be fixed.")
+                                                        fulfill()
+                    },
+                                                    failure: { _ in
+                                                        reject(OWSErrorWithCodeDescription(.signalServiceFailure, "\(self.TAG) Unknown error in \(#function)"))
+                    })
+                default:
+                    reject(error)
+                }
+            }.retainUntilComplete()
+
+            return promise
+        }
+    }
+}
diff --git a/Signal/src/util/WeakTimer.swift b/Signal/src/util/WeakTimer.swift
index 1eea5e153..094d56d2c 100644
--- a/Signal/src/util/WeakTimer.swift
+++ b/Signal/src/util/WeakTimer.swift
@@ -1,5 +1,5 @@
 //
-//  Copyright © 2017 Open Whisper Systems. All rights reserved.
+//  Copyright (c) 2017 Open Whisper Systems. All rights reserved.
 //
 
 /**
@@ -33,7 +33,7 @@ final class WeakTimer {
                          action: action).timer!
     }
 
-    @objc fileprivate func fire(timer: Timer) {
+    @objc public func fire(timer: Timer) {
         if target != nil {
             action(timer)
         } else {
diff --git a/SignalServiceKit/src/Storage/TSYapDatabaseObject.m b/SignalServiceKit/src/Storage/TSYapDatabaseObject.m
index 71b71c0f5..032d55b48 100644
--- a/SignalServiceKit/src/Storage/TSYapDatabaseObject.m
+++ b/SignalServiceKit/src/Storage/TSYapDatabaseObject.m
@@ -78,6 +78,15 @@
 
 #pragma mark Class Methods
 
++ (MTLPropertyStorage)storageBehaviorForPropertyWithKey:(NSString *)propertyKey
+{
+    if ([propertyKey isEqualToString:@"TAG"]) {
+        return MTLPropertyStorageNone;
+    } else {
+        return [super storageBehaviorForPropertyWithKey:propertyKey];
+    }
+}
+
 + (YapDatabaseConnection *)dbReadConnection
 {
     // We use TSYapDatabaseObject's dbReadWriteConnection (not TSStorageManager's