From 2cb1ddbdadc9795599bb76e84ef6cfbef4d4d707 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Wed, 1 May 2019 13:16:43 +1000 Subject: [PATCH] Implement mnemonic based key pair restoration --- Signal/src/Signal-Bridging-Header.h | 1 + .../OnboardingKeyPairViewController.swift | 162 +++++++++++++----- .../translations/en.lproj/Localizable.strings | 3 + SignalServiceKit/src/Util/ECKeyPair.h | 7 + SignalServiceKit/src/Util/ECKeyPair.m | 27 +++ 5 files changed, 161 insertions(+), 39 deletions(-) create mode 100644 SignalServiceKit/src/Util/ECKeyPair.h create mode 100644 SignalServiceKit/src/Util/ECKeyPair.m diff --git a/Signal/src/Signal-Bridging-Header.h b/Signal/src/Signal-Bridging-Header.h index 2c47e9850..3f7d3287f 100644 --- a/Signal/src/Signal-Bridging-Header.h +++ b/Signal/src/Signal-Bridging-Header.h @@ -77,6 +77,7 @@ #import #import #import +#import #import #import #import diff --git a/Signal/src/ViewControllers/Registration/OnboardingKeyPairViewController.swift b/Signal/src/ViewControllers/Registration/OnboardingKeyPairViewController.swift index f199f8970..11f06259b 100644 --- a/Signal/src/ViewControllers/Registration/OnboardingKeyPairViewController.swift +++ b/Signal/src/ViewControllers/Registration/OnboardingKeyPairViewController.swift @@ -1,9 +1,24 @@ final class OnboardingKeyPairViewController : OnboardingBaseViewController { + private var mode: Mode = .register { didSet { if mode != oldValue { handleModeChanged() } } } private var keyPair: ECKeyPair! { didSet { updateMnemonic() } } - private var mnemonic: String! { didSet { mnemonicLabel.text = mnemonic } } + private var mnemonic: String! { didSet { handleMnemonicChanged() } } private var userName: String? + // MARK: Components + private lazy var registerStackView: UIStackView = { + let result = UIStackView(arrangedSubviews: [ explanationLabel, UIView.spacer(withHeight: 32), mnemonicLabel, UIView.spacer(withHeight: 24), copyButton, UIView.spacer(withHeight: 8), restoreButton ]) + result.accessibilityIdentifier = "onboarding.keyPairStep.registerStackView" + result.axis = .vertical + return result + }() + + private lazy var explanationLabel: UILabel = { + let result = createExplanationLabel(text: NSLocalizedString("Please save the seed below in a safe location. It can be used to restore your account if you lose access, or to migrate to a new device.", comment: "")) + result.accessibilityIdentifier = "onboarding.keyPairStep.explanationLabel" + return result + }() + private lazy var mnemonicLabel: UILabel = { let result = createExplanationLabel(text: "") result.accessibilityIdentifier = "onboarding.keyPairStep.mnemonicLabel" @@ -20,6 +35,43 @@ final class OnboardingKeyPairViewController : OnboardingBaseViewController { return result }() + private lazy var restoreButton: OWSFlatButton = { + let result = createLinkButton(title: NSLocalizedString("Restore Using Mnemonic", comment: ""), selector: #selector(switchMode)) + result.accessibilityIdentifier = "onboarding.keyPairStep.restoreButton" + return result + }() + + private lazy var restoreStackView: UIStackView = { + let result = UIStackView(arrangedSubviews: [ mnemonicTextField, UIView.spacer(withHeight: 24), registerButton ]) + result.accessibilityIdentifier = "onboarding.keyPairStep.restoreStackView" + result.axis = .vertical + return result + }() + + private lazy var mnemonicTextField: UITextField = { + let result = UITextField(frame: CGRect.zero) + result.accessibilityIdentifier = "onboarding.keyPairStep.mnemonicTextField" + result.textAlignment = .center + result.placeholder = NSLocalizedString("Enter Your Mnemonic", comment: "") + return result + }() + + private lazy var registerButton: OWSFlatButton = { + let result = createLinkButton(title: NSLocalizedString("Register a New Account", comment: ""), selector: #selector(switchMode)) + result.accessibilityIdentifier = "onboarding.keyPairStep.registerButton" + return result + }() + + private lazy var registerOrRestoreButton: OWSFlatButton = { + let result = createButton(title: "", selector: #selector(registerOrRestore)) + result.accessibilityIdentifier = "onboarding.keyPairStep.registerOrRestoreButton" + return result + }() + + // MARK: Types + enum Mode { case register, restore } + + // MARK: Lifecycle init(onboardingController: OnboardingController, userName: String?) { super.init(onboardingController: onboardingController) self.userName = userName @@ -28,6 +80,7 @@ final class OnboardingKeyPairViewController : OnboardingBaseViewController { override public func viewDidLoad() { super.loadView() setUpViewHierarchy() + handleModeChanged() // Perform initial update updateKeyPair() // Test // ================ @@ -43,36 +96,50 @@ final class OnboardingKeyPairViewController : OnboardingBaseViewController { } private func setUpViewHierarchy() { + // Prepare view.backgroundColor = Theme.backgroundColor view.layoutMargins = .zero + // Set up view hierarchy let titleLabel = createTitleLabel(text: NSLocalizedString("Create Your Loki Messenger Account", comment: "")) titleLabel.accessibilityIdentifier = "onboarding.keyPairStep.titleLabel" - let topSpacer = UIView.vStretchingSpacer() - let explanationLabel = createExplanationLabel(text: NSLocalizedString("Please save the seed below in a safe location. It can be used to restore your account if you lose access, or to migrate to a new device.", comment: "")) - explanationLabel.accessibilityIdentifier = "onboarding.keyPairStep.explanationLabel" - let bottomSpacer = UIView.vStretchingSpacer() - let registerButton = createButton(title: NSLocalizedString("Register", comment: ""), selector: #selector(register)) - registerButton.accessibilityIdentifier = "onboarding.keyPairStep.registerButton" - let stackView = UIStackView(arrangedSubviews: [ - titleLabel, - topSpacer, - explanationLabel, - UIView.spacer(withHeight: 32), - mnemonicLabel, - UIView.spacer(withHeight: 24), - copyButton, - bottomSpacer, - registerButton - ]) - stackView.axis = .vertical - stackView.alignment = .fill - stackView.layoutMargins = UIEdgeInsets(top: 32, left: 32, bottom: 32, right: 32) - stackView.isLayoutMarginsRelativeArrangement = true - view.addSubview(stackView) - stackView.autoPinWidthToSuperview() - stackView.autoPin(toTopLayoutGuideOf: self, withInset: 0) - autoPinView(toBottomOfViewControllerOrKeyboard: stackView, avoidNotch: true) - topSpacer.autoMatch(.height, to: .height, of: bottomSpacer) + titleLabel.setContentHuggingPriority(.required, for: NSLayoutConstraint.Axis.vertical) + let mainView = UIView(frame: CGRect.zero) + mainView.addSubview(restoreStackView) + mainView.addSubview(registerStackView) + let mainStackView = UIStackView(arrangedSubviews: [ titleLabel, mainView, registerOrRestoreButton ]) + mainStackView.axis = .vertical + mainStackView.layoutMargins = UIEdgeInsets(top: 32, left: 32, bottom: 32, right: 32) + mainStackView.isLayoutMarginsRelativeArrangement = true + view.addSubview(mainStackView) + // Set up constraints + mainStackView.autoPinWidthToSuperview() + mainStackView.autoPin(toTopLayoutGuideOf: self, withInset: 0) + autoPinView(toBottomOfViewControllerOrKeyboard: mainStackView, avoidNotch: true) + registerStackView.autoPinWidthToSuperview() + registerStackView.autoVCenterInSuperview() + restoreStackView.autoPinWidthToSuperview() + restoreStackView.autoVCenterInSuperview() + } + + // MARK: General + @objc private func enableCopyButton() { + copyButton.isUserInteractionEnabled = true + copyButton.setTitle(NSLocalizedString("Copy", comment: "")) + } + + // MARK: Updating + private func handleModeChanged() { + let registerOrRestoreButtonTitle: String = { + switch mode { + case .register: return NSLocalizedString("Register", comment: "") + case .restore: return NSLocalizedString("Restore", comment: "") + } + }() + UIView.animate(withDuration: 0.25) { + self.registerStackView.alpha = (self.mode == .register ? 1 : 0) + self.restoreStackView.alpha = (self.mode == .restore ? 1 : 0) + } + self.registerOrRestoreButton.setTitle(registerOrRestoreButtonTitle) } private func updateKeyPair() { @@ -84,33 +151,50 @@ final class OnboardingKeyPairViewController : OnboardingBaseViewController { private func updateMnemonic() { mnemonic = Mnemonic.encode(hexEncodedString: keyPair.hexEncodedPrivateKey) } + + private func handleMnemonicChanged() { + mnemonicLabel.text = mnemonic + } + // MARK: Interaction @objc private func copyMnemonic() { UIPasteboard.general.string = mnemonic copyButton.isUserInteractionEnabled = false copyButton.setTitle(NSLocalizedString("Copied ✓", comment: "")) Timer.scheduledTimer(timeInterval: 4, target: self, selector: #selector(enableCopyButton), userInfo: nil, repeats: false) } - - @objc private func enableCopyButton() { - copyButton.isUserInteractionEnabled = true - copyButton.setTitle(NSLocalizedString("Copy", comment: "")) + + @objc private func switchMode() { + switch mode { + case .register: mode = .restore + case .restore: mode = .register + } } - @objc private func register() { + @objc private func registerOrRestore() { + let hexEncodedPublicKey: String + switch mode { + case .register: hexEncodedPublicKey = keyPair.hexEncodedPublicKey + case .restore: + let mnemonic = mnemonicTextField.text! + do { + let hexEncodedPrivateKey = try Mnemonic.decode(mnemonic: mnemonic) + let keyPair = ECKeyPair.generate(withHexEncodedPrivateKey: hexEncodedPrivateKey) + // TODO: Store key pair + hexEncodedPublicKey = keyPair.hexEncodedPublicKey + } catch let error { + fatalError(error.localizedDescription) // TODO: Handle + } + } let accountManager = TSAccountManager.sharedInstance() - accountManager.phoneNumberAwaitingVerification = keyPair.hexEncodedPublicKey + accountManager.phoneNumberAwaitingVerification = hexEncodedPublicKey accountManager.didRegister() - let onSuccess: () -> Void = { [weak self] in + let onSuccess = { [weak self] in guard let strongSelf = self else { return } strongSelf.onboardingController.verificationDidComplete(fromView: strongSelf) } if let userName = userName { - // Try to save the user name - OWSProfileManager.shared().updateLocalProfileName(userName, avatarImage: nil, success: onSuccess, failure: { - Logger.warn("Failed to set user name") - onSuccess() - }) + OWSProfileManager.shared().updateLocalProfileName(userName, avatarImage: nil, success: onSuccess, failure: onSuccess) // Try to save the user name but ignore the result } else { onSuccess() } diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 8933f0be5..05697716b 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -2556,3 +2556,6 @@ "Copy" = "Copy"; "Copied ✓" = "Copied ✓"; "Register" = "Register"; +"Restore" = "Restore"; +"Enter Your Mnemonic" = "Enter Your Mnemonic"; +"Restore Using Mnemonic" = "Restore Using Mnemonic"; diff --git a/SignalServiceKit/src/Util/ECKeyPair.h b/SignalServiceKit/src/Util/ECKeyPair.h new file mode 100644 index 000000000..bdf4f53e8 --- /dev/null +++ b/SignalServiceKit/src/Util/ECKeyPair.h @@ -0,0 +1,7 @@ +@import Curve25519Kit; + +@interface ECKeyPair (ECKeyPairExtension) + ++ (nonnull ECKeyPair *)generateKeyPairWithHexEncodedPrivateKey:(nonnull NSString *)hexEncodedPrivateKey; + +@end diff --git a/SignalServiceKit/src/Util/ECKeyPair.m b/SignalServiceKit/src/Util/ECKeyPair.m new file mode 100644 index 000000000..f894a5e60 --- /dev/null +++ b/SignalServiceKit/src/Util/ECKeyPair.m @@ -0,0 +1,27 @@ +#import "ECKeyPair.h" + +extern void curve25519_donna(unsigned char *output, const unsigned char *a, const unsigned char *b); + +@implementation ECKeyPair (ECKeyPairExtension) + ++ (nonnull ECKeyPair *)generateKeyPairWithHexEncodedPrivateKey:(nonnull NSString *)hexEncodedPrivateKey { + NSMutableData *privateKey = [NSMutableData new]; + for (int i = 0; i < hexEncodedPrivateKey.length; i += 2) { + char buffer[3]; + buffer[0] = [hexEncodedPrivateKey characterAtIndex:i]; + buffer[1] = [hexEncodedPrivateKey characterAtIndex:i + 1]; + buffer[2] = '\0'; + unsigned char byte = strtol(buffer, NULL, 16); + [privateKey appendBytes:&byte length:1]; + } + static const uint8_t basepoint[ECCKeyLength] = { 9 }; + NSMutableData *publicKey = [NSMutableData dataWithLength:ECCKeyLength]; + if (!publicKey) { OWSFail(@"Could not allocate buffer"); } + curve25519_donna(publicKey.mutableBytes, privateKey.mutableBytes, basepoint); + ECKeyPair *result = [ECKeyPair new]; + [result setValue:[privateKey copy] forKey:@"privateKey"]; + [result setValue:[publicKey copy] forKey:@"publicKey"]; + return result; +} + +@end