From d74b34d1616968494d24cbbf12099bb2ba21665d Mon Sep 17 00:00:00 2001 From: Dmitry Bespalov Date: Wed, 12 Jun 2024 11:19:24 +0200 Subject: [PATCH 1/3] GH-3406 update libs --- Multisig.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Multisig.xcodeproj/project.pbxproj b/Multisig.xcodeproj/project.pbxproj index 858e0620b..2b1a57b52 100644 --- a/Multisig.xcodeproj/project.pbxproj +++ b/Multisig.xcodeproj/project.pbxproj @@ -7506,7 +7506,7 @@ repositoryURL = "https://github.com/KeystoneHQ/ur-registry-ios"; requirement = { kind = exactVersion; - version = 1.0.0; + version = 1.1.0; }; }; 0A259AB3287D83F9006770E7 /* XCRemoteSwiftPackageReference "resolution-swift" */ = { diff --git a/Multisig.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Multisig.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index fdbc70d1f..7dd22878f 100644 --- a/Multisig.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Multisig.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -384,8 +384,8 @@ "repositoryURL": "https://github.com/KeystoneHQ/ur-registry-ios", "state": { "branch": null, - "revision": "10511be95146e8dfa2063e3ea21cceb9e6536fa1", - "version": "1.0.0" + "revision": "cafed3d8d3f73417c148d4a0b00046a45ef1ed30", + "version": "1.1.0" } }, { From 618a77846e74edb7852b7fffc2734940ca2f3c05 Mon Sep 17 00:00:00 2001 From: Dmitry Bespalov Date: Thu, 13 Jun 2024 12:01:58 +0200 Subject: [PATCH 2/3] GH-3406 GH-3427 adjust export data flow * Added password strength requirements and meter * Changed texts in instructions and success screens --- .../CreateExportPasswordViewController.swift | 44 ++++++++++++++++++- .../CreateExportPasswordViewController.xib | 8 +++- .../Features/Data Export/ExportDataFlow.swift | 14 ++++-- .../Features/Data Export/ImportDataFlow.swift | 2 +- 4 files changed, 60 insertions(+), 8 deletions(-) diff --git a/Multisig/Features/Data Export/CreateExportPasswordViewController.swift b/Multisig/Features/Data Export/CreateExportPasswordViewController.swift index ab493f031..9f8c0bebb 100644 --- a/Multisig/Features/Data Export/CreateExportPasswordViewController.swift +++ b/Multisig/Features/Data Export/CreateExportPasswordViewController.swift @@ -14,6 +14,7 @@ class CreateExportPasswordViewController: UIViewController { var prompt: String = "" var placeholder: String = "" var plainTextPassword: String? + var passwordMeterEnabled: Bool = false var completion: (String) -> Void = { _ in } var validateValue: (String) -> Error? = { _ in nil } @@ -22,7 +23,8 @@ class CreateExportPasswordViewController: UIViewController { @IBOutlet weak var continueButton: UIButton! @IBOutlet weak var scrollView: UIScrollView! @IBOutlet weak var buttonBottomConstraint: NSLayoutConstraint! - + @IBOutlet weak var passwordMeter: UIProgressView! + private var debounceTimer: Timer! private let debounceDuration: TimeInterval = 0.250 private var isValid: Bool = false @@ -50,6 +52,8 @@ class CreateExportPasswordViewController: UIViewController { descriptionLabel.setStyle(.body) continueButton.setText("Continue", .filled) + + passwordMeter.isHidden = !passwordMeterEnabled validateText() @@ -106,16 +110,47 @@ class CreateExportPasswordViewController: UIViewController { self?.view?.layoutIfNeeded() } } + + /* + Password Strength Function + + Factors: + Length: 1 L = 10/14 = 8 + Numbers 1 N = 3 + Symbols 1 S = 3 + Capitals 1 C = 3 + + P = 8 * L * (1 + 0,1 N + 0,1 S + 0,1 C) + P > 100 ? P = 100 + + Req: P >= P(L=8) = 64 + */ + + func passwordScore(_ text: String) -> Double { + let L = Double(text.count) * 8 + let N = text.rangeOfCharacter(from: .decimalDigits) == nil ? 0 : 0.1 + let S = text.rangeOfCharacter(from: .symbols) == nil ? 0 : 0.1 + let C = text.rangeOfCharacter(from: .capitalizedLetters) == nil ? 0 : 0.1 + var P = L < 64 ? L : L * (1 + N + S + C) + P = (P > 100) ? 100 : P + return P + } - private func validateText() { + fileprivate func resetText() { isValid = false continueButton.isEnabled = false textField.setError(nil) + passwordMeter.progress = 0 + } + + private func validateText() { + resetText() guard let text = textField.textField.text?.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty else { self.plainTextPassword = nil return } + passwordMeter.progress = Float(passwordScore(text) / 100) if let error = validateValue(text) { textField.setError(error) return @@ -135,6 +170,11 @@ extension CreateExportPasswordViewController: UITextFieldDelegate { func textFieldDidBeginEditing(_ textField: UITextField) { keyboardBehavior.activeTextField = textField } + + func textFieldShouldClear(_ textField: UITextField) -> Bool { + self.resetText() + return true + } func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { debounceTimer?.invalidate() diff --git a/Multisig/Features/Data Export/CreateExportPasswordViewController.xib b/Multisig/Features/Data Export/CreateExportPasswordViewController.xib index 3c60955fe..7a1025b86 100644 --- a/Multisig/Features/Data Export/CreateExportPasswordViewController.xib +++ b/Multisig/Features/Data Export/CreateExportPasswordViewController.xib @@ -15,6 +15,7 @@ + @@ -32,10 +33,10 @@ - + - + diff --git a/Multisig/Features/Data Export/ExportDataFlow.swift b/Multisig/Features/Data Export/ExportDataFlow.swift index b6b75663a..377bee443 100644 --- a/Multisig/Features/Data Export/ExportDataFlow.swift +++ b/Multisig/Features/Data Export/ExportDataFlow.swift @@ -24,7 +24,7 @@ class ExportDataFlow: UIFlow { vc.steps = [ .header, .step(number: "1", title: "Create a file password", description: "Enter a strong password for locking the export file."), - .step(number: "2", title: "Export the data", description: "Data includes the owner keys, safes and address book in an encrypted file format."), + .step(number: "2", title: "Export the data", description: "Data includes private keys, safes and address book in an encrypted file format."), .step(number: "3", title: "Save the data file", description: "Store the export file in Files on your device or a secure location of your choice."), .finalStep(title: "Export of data completed!") ] @@ -43,8 +43,16 @@ class ExportDataFlow: UIFlow { func createPassword() { let vc = CreateExportPasswordViewController(nibName: nil, bundle: nil) vc.title = "Create password" - vc.prompt = "Choose a password for protecting the exported data." + vc.prompt = "Your password protects the data, including private keys.\n• At least 8 characters long.\n• Use numbers, symbols, and capital letters.\n• Use a password manager like 1Password or iCloud Keychain." vc.placeholder = "Enter password" + vc.passwordMeterEnabled = true + vc.validateValue = { [unowned vc] value in + let score = vc.passwordScore(value) + if score < 64 { + return "Password must be at least 8 characters long." + } + return nil + } vc.completion = { [unowned self] plainTextPassword in repeatPassword(plainTextPassword) } @@ -81,7 +89,7 @@ class ExportDataFlow: UIFlow { if let url = tempFileURL { let vc = SuccessViewController( titleText: "Export completed", - bodyText: "Exported data is encrypted and includes owner keys, list of safes and address book", + bodyText: "Exported data is encrypted and includes private keys, list of safes and address book.\n\nSave it to Files - On My iPhone.", primaryAction: "Save", secondaryAction: "Done" ) diff --git a/Multisig/Features/Data Export/ImportDataFlow.swift b/Multisig/Features/Data Export/ImportDataFlow.swift index 9463c6141..f92dfc9c4 100644 --- a/Multisig/Features/Data Export/ImportDataFlow.swift +++ b/Multisig/Features/Data Export/ImportDataFlow.swift @@ -27,7 +27,7 @@ class ImportDataFlow: UIFlow { .step(number: "2", title: "Enter file password", description: "Enter the password to access the data from the file"), .step(number: "3", title: "Import the data", - description: "The imported data includes owner keys, safes and address book. Duplicates will be skipped."), + description: "The imported data includes private keys, safes and address book. Duplicates will be skipped."), .finalStep(title: "Import of data completed!") ] From 00c24a4541690475ed083521bd60a232796522a9 Mon Sep 17 00:00:00 2001 From: Dmitry Bespalov Date: Thu, 13 Jun 2024 12:16:10 +0200 Subject: [PATCH 3/3] GH-3406 GH-3427 add comment on password score --- .../CreateExportPasswordViewController.swift | 37 ++++++++----------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/Multisig/Features/Data Export/CreateExportPasswordViewController.swift b/Multisig/Features/Data Export/CreateExportPasswordViewController.swift index 9f8c0bebb..b01240a7e 100644 --- a/Multisig/Features/Data Export/CreateExportPasswordViewController.swift +++ b/Multisig/Features/Data Export/CreateExportPasswordViewController.swift @@ -110,29 +110,22 @@ class CreateExportPasswordViewController: UIViewController { self?.view?.layoutIfNeeded() } } - - /* - Password Strength Function - - Factors: - Length: 1 L = 10/14 = 8 - Numbers 1 N = 3 - Symbols 1 S = 3 - Capitals 1 C = 3 - - P = 8 * L * (1 + 0,1 N + 0,1 S + 0,1 C) - P > 100 ? P = 100 - - Req: P >= P(L=8) = 64 - */ - + func passwordScore(_ text: String) -> Double { - let L = Double(text.count) * 8 - let N = text.rangeOfCharacter(from: .decimalDigits) == nil ? 0 : 0.1 - let S = text.rangeOfCharacter(from: .symbols) == nil ? 0 : 0.1 - let C = text.rangeOfCharacter(from: .capitalizedLetters) == nil ? 0 : 0.1 - var P = L < 64 ? L : L * (1 + N + S + C) - P = (P > 100) ? 100 : P + // We define score P as: + // P[ L >= 8 ] = 8 * L * (1 + 0.1 * N + 0.1 * S + 0.1 * C) + // P[ L < 8 ] = 8 * L + // where L = length of the password text string + // N = 1 if password contains numbers, 0 otherwise + // S = 1 if password contains symbols, 0 otherwise + // C = 1 if password contains capital letters, 0 otherwise + // Maximum P value equals 100, i.e.: + // P = min(P, 100) + let L = Double(text.count) + let N = text.rangeOfCharacter(from: .decimalDigits) == nil ? 0 : 1.0 + let S = text.rangeOfCharacter(from: .symbols) == nil ? 0 : 1.0 + let C = text.rangeOfCharacter(from: .capitalizedLetters) == nil ? 0 : 1.0 + let P = min(100, (L < 8) ? (8 * L) : (8 * L * (1 + 0.1 * N + 0.1 * S + 0.1 * C)) ) return P }