diff --git a/Cartfile b/Cartfile index 76725caba..8070c9681 100644 --- a/Cartfile +++ b/Cartfile @@ -1,7 +1,7 @@ github "Quick/Quick" github "Quick/Nimble" github "ReactiveX/RxSwift" -github "RxSwiftCommunity/RxDataSources" "026857261128bf552369a3b69b3ab56c3f85704d" +github "RxSwiftCommunity/RxDataSources" "81b709c28b8ceb5cddc8c2262d009ea8735f0893" github "RxSwiftCommunity/RxOptional" ~> 3.1.3 github "mozilla-mobile/telemetry-ios" github "jrendel/SwiftKeychainWrapper" ~> 3.0 diff --git a/Cartfile.resolved b/Cartfile.resolved index 2d36f2dc4..f242350d3 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -3,7 +3,7 @@ github "DaveWoodCom/XCGLogger" "Version_4.0.0" github "Quick/Nimble" "v7.3.0" github "Quick/Quick" "v1.3.1" github "ReactiveX/RxSwift" "4.2.0" -github "RxSwiftCommunity/RxDataSources" "026857261128bf552369a3b69b3ab56c3f85704d" +github "RxSwiftCommunity/RxDataSources" "81b709c28b8ceb5cddc8c2262d009ea8735f0893" github "RxSwiftCommunity/RxOptional" "3.5.0" github "SwiftyJSON/SwiftyJSON" "3.1.4" github "adjust/ios_sdk" "v4.15.0" diff --git a/CredentialProvider/Info.plist b/CredentialProvider/Info.plist index a5e08053e..752866b59 100644 --- a/CredentialProvider/Info.plist +++ b/CredentialProvider/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.3 + 1.3.1 CFBundleVersion 1 MozDevelopmentTeam diff --git a/Lockbox.xcodeproj/xcshareddata/xcschemes/uispecs.xcscheme b/Lockbox.xcodeproj/xcshareddata/xcschemes/uispecs.xcscheme index 54a0af7af..4cb58824c 100644 --- a/Lockbox.xcodeproj/xcshareddata/xcschemes/uispecs.xcscheme +++ b/Lockbox.xcodeproj/xcshareddata/xcschemes/uispecs.xcscheme @@ -5,6 +5,16 @@ + + + + + + MMScr app.switches["sendUsageData.switch"].tap() } + screenState.gesture(forAction: Action.OpenDeviceSettings) { userState in + app.cells["autoFillSettingsOption"].tap() + + } + screenState.gesture(forAction: Action.LockNow) { userState in app.buttons["lockNow.button"].tap() } @@ -172,6 +178,19 @@ func createScreenGraph(for test: XCTestCase, with app: XCUIApplication) -> MMScr extension BaseTestCase { + func disconnectAndConnectAccount() { + navigator.performAction(Action.DisconnectFirefoxLockbox) + // And, connect it again + waitforExistence(app.buttons["getStarted.button"]) + app.buttons["getStarted.button"].tap() + userState.fxaUsername = emailTestAccountLogins + userState.fxaPassword = passwordTestAccountLogins + waitforExistence(app.webViews.textFields["Email"], timeout: 10) + navigator.nowAt(Screen.FxASigninScreenEmail) + navigator.performAction(Action.FxATypeEmail) + navigator.performAction(Action.FxATypePassword) + } + func checkIfAccountIsVerified() { if (app.webViews.staticTexts["Confirm this sign-in"].exists) { let group = DispatchGroup() diff --git a/LockboxXCUITests/LockboxXCUITests.swift b/LockboxXCUITests/LockboxXCUITests.swift index b4445ae17..758f84e8f 100644 --- a/LockboxXCUITests/LockboxXCUITests.swift +++ b/LockboxXCUITests/LockboxXCUITests.swift @@ -41,8 +41,9 @@ class LockboxXCUITests: BaseTestCase { navigator.performAction(Action.FxATypeEmail) waitforExistence(app.webViews.secureTextFields["Password"]) navigator.performAction(Action.FxATypePassword) - // Lets try to remove this sleep(10) and see if the tests consistently pass - // Check if the account is verified and if not, verify it + // When the account is unverified, it is necessary to wait here while finding a different workaround + sleep(5) + // Check if the account is verified and if not, verify it checkIfAccountIsVerified() if #available(iOS 12.0, *) { @@ -134,17 +135,7 @@ class LockboxXCUITests: BaseTestCase { waitforExistence(app.buttons["disconnectFirefoxLockbox.button"]) // Now disconnect the account - navigator.performAction(Action.DisconnectFirefoxLockbox) - - // And, connect it again - waitforExistence(app.buttons["getStarted.button"]) - app.buttons["getStarted.button"].tap() - userState.fxaUsername = emailTestAccountLogins - userState.fxaPassword = passwordTestAccountLogins - waitforExistence(app.webViews.textFields["Email"], timeout: 10) - navigator.nowAt(Screen.FxASigninScreenEmail) - navigator.performAction(Action.FxATypeEmail) - navigator.performAction(Action.FxATypePassword) + disconnectAndConnectAccount() if #available(iOS 12.0, *) { waitforExistence(app.buttons["setupAutofill.button"]) @@ -258,4 +249,62 @@ class LockboxXCUITests: BaseTestCase { XCTAssertTrue(app.cells["appVersionSettingOption"].exists) XCTAssertNotEqual(app.cells.staticTexts.element(boundBy: 2).label, "") } + + // Verify SetAutofillNow + func testSetAutofillNow() { + // Disconnect account + disconnectAndConnectAccount() + if #available(iOS 12.0, *) { + waitforExistence(app.buttons["setupAutofill.button"]) + app.buttons["setupAutofill.button"].tap() + } + let settingsApp = XCUIApplication(bundleIdentifier: "com.apple.Preferences") + waitforExistence(settingsApp.cells.staticTexts["Passwords & Accounts"]) + } + + // Once app is open + func testSetAutofillSettings() { + // Open Setting to add Lockbox + navigator.goto(Screen.SettingsMenu) + navigator.performAction(Action.OpenDeviceSettings) + // Wait until settings app is open + let settingsApp = XCUIApplication(bundleIdentifier: "com.apple.Preferences") + waitforExistence(settingsApp.cells.staticTexts["Passwords & Accounts"]) + // Configure Passwords & Accounts settings + settingsApp.cells.staticTexts["Passwords & Accounts"].tap() + waitforExistence(settingsApp.cells.staticTexts["AutoFill Passwords"]) + settingsApp.cells.staticTexts["AutoFill Passwords"].tap() + waitforExistence(settingsApp.switches["AutoFill Passwords"]) + settingsApp.switches["AutoFill Passwords"].tap() + waitforExistence(settingsApp.cells.staticTexts["Lockbox"]) + settingsApp.cells.staticTexts["Lockbox"].tap() + // Wait until the app is updated + sleep(5) + settingsApp.terminate() + + // Open Safari + let safari = XCUIApplication(bundleIdentifier: "com.apple.mobilesafari") + safari.launch() + waitforExistence(safari.buttons["URL"], timeout: 5) + safari.buttons["ReloadButton"].tap() + waitforExistence(safari.textFields["Email or phone"]) + safari.textFields["Email or phone"].tap() + + // Need to confirm what is shown here, different elements have appeared during the tests + if (safari.buttons["Other passwords"].exists) { + safari.buttons["Other passwords"].tap() + // Workaround for the test, the first time the password does not appear + safari.buttons["Cancel"].tap() + safari.buttons["Other passwords"].tap() + waitforExistence(safari.otherElements.staticTexts["Choose a saved password to use"]) + print(safari.debugDescription) + XCTAssertTrue(safari.buttons.otherElements["iosmztest@gmail.com, for this website — Lockbox"].exists) + } else if (safari.otherElements["Password Auto-fill"].exists) { + safari.otherElements["Password Auto-fill"].tap() + XCTAssertTrue(safari.buttons["iosmztest@gmail.com, for this website — Lockbox"].exists) + } else { + XCTAssertTrue(safari.buttons["Use “iosmztest@gmail.com”"].exists) + } + safari.terminate() + } } diff --git a/LockboxXCUITests/SnapshotHelper.swift b/LockboxXCUITests/SnapshotHelper.swift index 5b7ead01a..ad575130b 100644 --- a/LockboxXCUITests/SnapshotHelper.swift +++ b/LockboxXCUITests/SnapshotHelper.swift @@ -42,6 +42,7 @@ enum SnapshotError: Error, CustomDebugStringConvertible { case cannotFindHomeDirectory case cannotFindSimulatorHomeDirectory case cannotAccessSimulatorHomeDirectory(String) + case cannotRunOnPhysicalDevice var debugDescription: String { switch self { @@ -53,22 +54,27 @@ enum SnapshotError: Error, CustomDebugStringConvertible { return "Couldn't find simulator home location. Please, check SIMULATOR_HOST_HOME env variable." case .cannotAccessSimulatorHomeDirectory(let simulatorHostHome): return "Can't prepare environment. Simulator home location is inaccessible. Does \(simulatorHostHome) exist?" + case .cannotRunOnPhysicalDevice: + return "Can't use Snapshot on a physical device." } } } +@objcMembers open class Snapshot: NSObject { - static var app: XCUIApplication! - static var cacheDirectory: URL! + static var app: XCUIApplication? + static var cacheDirectory: URL? static var screenshotsDirectory: URL? { - return cacheDirectory.appendingPathComponent("screenshots", isDirectory: true) + return cacheDirectory?.appendingPathComponent("screenshots", isDirectory: true) } open class func setupSnapshot(_ app: XCUIApplication) { + + Snapshot.app = app + do { let cacheDir = try pathPrefix() Snapshot.cacheDirectory = cacheDir - Snapshot.app = app setLanguage(app) setLocale(app) setLaunchArguments(app) @@ -78,6 +84,11 @@ open class Snapshot: NSObject { } class func setLanguage(_ app: XCUIApplication) { + guard let cacheDirectory = self.cacheDirectory else { + print("CacheDirectory is not set - probably running on a physical device?") + return + } + let path = cacheDirectory.appendingPathComponent("language.txt") do { @@ -90,6 +101,11 @@ open class Snapshot: NSObject { } class func setLocale(_ app: XCUIApplication) { + guard let cacheDirectory = self.cacheDirectory else { + print("CacheDirectory is not set - probably running on a physical device?") + return + } + let path = cacheDirectory.appendingPathComponent("locale.txt") do { @@ -105,6 +121,11 @@ open class Snapshot: NSObject { } class func setLaunchArguments(_ app: XCUIApplication) { + guard let cacheDirectory = self.cacheDirectory else { + print("CacheDirectory is not set - probably running on a physical device?") + return + } + let path = cacheDirectory.appendingPathComponent("snapshot-launch_arguments.txt") app.launchArguments += ["-FASTLANE_SNAPSHOT", "YES", "-ui_testing"] @@ -126,14 +147,21 @@ open class Snapshot: NSObject { waitForLoadingIndicatorToDisappear(within: timeout) } - print("snapshot: \(name)") // more information about this, check out https://github.com/fastlane/fastlane/tree/master/snapshot#how-does-it-work + print("snapshot: \(name)") // more information about this, check out https://docs.fastlane.tools/actions/snapshot/#how-does-it-work sleep(1) // Waiting for the animation to be finished (kind of) #if os(OSX) XCUIApplication().typeKey(XCUIKeyboardKeySecondaryFn, modifierFlags: []) #else - let screenshot = app.windows.firstMatch.screenshot() + + guard let app = self.app else { + print("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") + return + } + + let window = app.windows.firstMatch + let screenshot = window.screenshot() guard let simulator = ProcessInfo().environment["SIMULATOR_DEVICE_NAME"], let screenshotsDir = screenshotsDirectory else { return } let path = screenshotsDir.appendingPathComponent("\(simulator)-\(name).png") do { @@ -164,19 +192,23 @@ open class Snapshot: NSObject { throw SnapshotError.cannotDetectUser } - guard let usersDir = FileManager.default.urls(for: .userDirectory, in: .localDomainMask).first else { + guard let usersDir = FileManager.default.urls(for: .userDirectory, in: .localDomainMask).first else { throw SnapshotError.cannotFindHomeDirectory } homeDir = usersDir.appendingPathComponent(user) #else - guard let simulatorHostHome = ProcessInfo().environment["SIMULATOR_HOST_HOME"] else { - throw SnapshotError.cannotFindSimulatorHomeDirectory - } - guard let homeDirUrl = URL(string: simulatorHostHome) else { - throw SnapshotError.cannotAccessSimulatorHomeDirectory(simulatorHostHome) - } - homeDir = URL(fileURLWithPath: homeDirUrl.path) + #if arch(i386) || arch(x86_64) + guard let simulatorHostHome = ProcessInfo().environment["SIMULATOR_HOST_HOME"] else { + throw SnapshotError.cannotFindSimulatorHomeDirectory + } + guard let homeDirUrl = URL(string: simulatorHostHome) else { + throw SnapshotError.cannotAccessSimulatorHomeDirectory(simulatorHostHome) + } + homeDir = URL(fileURLWithPath: homeDirUrl.path) + #else + throw SnapshotError.cannotRunOnPhysicalDevice + #endif #endif return homeDir.appendingPathComponent("Library/Caches/tools.fastlane") } @@ -221,7 +253,7 @@ private extension XCUIElementQuery { } var deviceStatusBars: XCUIElementQuery { - let deviceWidth = XCUIApplication().frame.width + let deviceWidth = XCUIApplication().windows.firstMatch.frame.width let isStatusBar = NSPredicate { (evaluatedObject, _) in guard let element = evaluatedObject as? XCUIElementAttributes else { return false } @@ -241,4 +273,4 @@ private extension CGFloat { // Please don't remove the lines below // They are used to detect outdated configuration files -// SnapshotHelperVersion [1.7] +// SnapshotHelperVersion [1.12] diff --git a/README.md b/README.md index 9d3002493..3235b69e0 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,9 @@ See the [guidelines][contributing-link] for contributing to this project. This project is governed by a [Code Of Conduct][coc-link]. +To disclose potential a security vulnerability please see our +[security][security-link] documentation. + ## [License][license-link] This module is licensed under the [Mozilla Public License, @@ -37,4 +40,5 @@ Note that some test fixtures use source code from third-party services, and are [org-website]: https://lockbox.firefox.com/ [contributing-link]: docs/contributing.md [coc-link]: docs/code_of_conduct.md +[security-link]: docs/SECURITY.md [license-link]: /LICENSE diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 000000000..19bea32e7 --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,5 @@ +# Mozilla Security # + +- Mozilla cares about privacy and security. For more information please see: https://www.mozilla.org/security/ + +- If you believe that you've found a security vulnerability, please report it by sending email to the addresses: security@mozilla.org and lockbox-dev@mozilla.com diff --git a/docs/contributing.md b/docs/contributing.md index 996fff0d0..cfa4c75f4 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -18,6 +18,8 @@ Please open [a new issue in the GitHub repository](https://github.com/mozilla-lo Be sure to include as much information including screenshots, text output, and both your expected and actual results. +If you believe that you've found a security vulnerability, please report it by sending email to the addresses: security@mozilla.org and lockbox-dev@mozilla.com + ## How to Request Enhancements First, please refer to the applicable [GitHub repository](https://github.com/orgs/mozilla-lockbox/) and search [the repository's GitHub issues](https://github.com/mozilla-lockbox/lockbox-ios/issues) to make sure your idea has not been (or is not still) considered. diff --git a/docs/release-notes.md b/docs/release-notes.md index ad4407b2f..1cc77ea08 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,5 +1,22 @@ # Release Notes +## 1.3.1 (Build 2428) + +We fixed some bugs related to "Disconnecting" (signing out) and the new AutoFill feature. + +Here's the full list of changes: + +- If you "Disconnect" then sign-in, you won't get stuck seeing the “No matching entries” error message +- We also made it so if you sign-in with a different account, you don't accidentally see the items from the first account + +## 1.3 (Build 2362) + +_Date: 2018-09-15_ + +With iOS 12 you can automatically fill your usernames and passwords from Firefox Lockbox into apps and websites. + +Be sure to enable AutoFill after updating to iOS 12 from within Settings under the "Passwords & Accounts" section. + ## 1.3 (Build 2349) _Date: 2018-09-14_ diff --git a/lockbox-ios/Common/Resources/Info.plist b/lockbox-ios/Common/Resources/Info.plist index 6c38e24d6..67675590c 100644 --- a/lockbox-ios/Common/Resources/Info.plist +++ b/lockbox-ios/Common/Resources/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.3 + 1.3.1 CFBundleVersion 1 LSApplicationQueriesSchemes diff --git a/lockbox-ios/Presenter/RootPresenter.swift b/lockbox-ios/Presenter/RootPresenter.swift index 06195a839..bf71570f2 100644 --- a/lockbox-ios/Presenter/RootPresenter.swift +++ b/lockbox-ios/Presenter/RootPresenter.swift @@ -90,6 +90,8 @@ class RootPresenter { } else if latest.oauthInfo == nil && latest.profile == nil { self.dispatcher.dispatch(action: LoginRouteAction.welcome) self.dispatcher.dispatch(action: DataStoreAction.reset) + self.dispatcher.dispatch(action: CredentialProviderAction.clear) + self.dispatcher.dispatch(action: AccountAction.clear) } } .disposed(by: self.disposeBag) diff --git a/lockbox-ios/Store/AccountStore.swift b/lockbox-ios/Store/AccountStore.swift index 57cfd94d2..0820347aa 100644 --- a/lockbox-ios/Store/AccountStore.swift +++ b/lockbox-ios/Store/AccountStore.swift @@ -95,6 +95,12 @@ class AccountStore: BaseAccountStore { }) .disposed(by: self.disposeBag) + initFxa() + } +} + +extension AccountStore { + private func initFxa() { if let accountJSON = self.storedAccountJSON { self.fxa = try? FirefoxAccount.fromJSON(state: accountJSON) self.generateLoginURL() @@ -102,10 +108,10 @@ class AccountStore: BaseAccountStore { } else { FxAConfig.release { (config: FxAConfig?, _) in if let config = config { - self.fxa = try? FirefoxAccount( - config: config, - clientId: Constant.fxa.clientID, - redirectUri: Constant.fxa.redirectURI) + self.fxa = try? FirefoxAccount( + config: config, + clientId: Constant.fxa.clientID, + redirectUri: Constant.fxa.redirectURI) self.generateLoginURL() } @@ -115,9 +121,7 @@ class AccountStore: BaseAccountStore { } } } -} -extension AccountStore { private func generateLoginURL() { self.fxa?.beginOAuthFlow(scopes: Constant.fxa.scopes, wantsKeys: true) { url, _ in if let url = url { @@ -139,6 +143,8 @@ extension AccountStore { self._profile.onNext(nil) self._oauthInfo.onNext(nil) + + self.initFxa() } private func clearOldKeychainValues() { diff --git a/lockbox-iosTests/RootPresenterSpec.swift b/lockbox-iosTests/RootPresenterSpec.swift index 18571af86..0002e3bb9 100644 --- a/lockbox-iosTests/RootPresenterSpec.swift +++ b/lockbox-iosTests/RootPresenterSpec.swift @@ -228,11 +228,18 @@ class RootPresenterSpec: QuickSpec { self.accountStore.profileInfoStub.onNext(nil) } - it("routes to the welcome view and resets the datastore") { + it("clears the account, credential provider, routes to the welcome view and resets the datastore") { + let arg = self.dispatcher.dispatchActionArgument.popLast() as! AccountAction + expect(arg).to(equal(AccountAction.clear)) + + let credentialProviderAction = self.dispatcher.dispatchActionArgument.popLast() as! CredentialProviderAction + expect(credentialProviderAction).to(equal(CredentialProviderAction.clear)) + let dataStoreAction = self.dispatcher.dispatchActionArgument.popLast() as! DataStoreAction expect(dataStoreAction).to(equal(DataStoreAction.reset)) - let arg = self.dispatcher.dispatchActionArgument.popLast() as! LoginRouteAction - expect(arg).to(equal(LoginRouteAction.welcome)) + + let loginRouteAction = self.dispatcher.dispatchActionArgument.popLast() as! LoginRouteAction + expect(loginRouteAction).to(equal(LoginRouteAction.welcome)) } }