Skip to content
This repository has been archived by the owner on Dec 14, 2021. It is now read-only.

Update application services to 0.48.0 - Xcode 11.3 #1178

Merged
merged 11 commits into from
Feb 6, 2020
Merged
6 changes: 3 additions & 3 deletions Cartfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ github "adjust/ios_sdk" ~> 4.14.1
github "apple/swift-protobuf" "1.5.0"
github "ashleymills/Reachability.swift" "v4.3.0"
github "getsentry/sentry-cocoa" "4.3.4"
github "jrendel/SwiftKeychainWrapper" "3.3.0"
github "jrendel/SwiftKeychainWrapper" "3.4.0"
github "mozilla-mobile/MappaMundi" "02b6f0b404d0a7178c47c073550936016f42981e"
github "mozilla-mobile/telemetry-ios" "1.1.1"
github "mozilla/application-services" "v0.42.3"
github "mozilla/glean" "v21.3.0"
github "mozilla/application-services" "v0.48.3"
github "mozilla/glean" "v23.0.1"
8 changes: 4 additions & 4 deletions Cartfile.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ github "Quick/Quick" "v2.1.0"
github "ReactiveX/RxSwift" "4b42d62e34e278f677e9788c1e4c2db2a65f88ce"
github "RxSwiftCommunity/RxDataSources" "8d4ef9abd7fd093b9225aa3e833b46171c00cc38"
github "RxSwiftCommunity/RxOptional" "3.7.0"
github "adjust/ios_sdk" "v4.18.3"
github "adjust/ios_sdk" "v4.20.0"
github "apple/swift-protobuf" "1.5.0"
github "ashleymills/Reachability.swift" "v4.3.0"
github "getsentry/sentry-cocoa" "4.3.4"
github "jrendel/SwiftKeychainWrapper" "3.3.0"
github "jrendel/SwiftKeychainWrapper" "3.4.0"
github "mozilla-mobile/MappaMundi" "02b6f0b404d0a7178c47c073550936016f42981e"
github "mozilla-mobile/telemetry-ios" "1.1.1"
github "mozilla/application-services" "v0.42.3"
github "mozilla/glean" "v21.3.0"
github "mozilla/application-services" "v0.48.3"
github "mozilla/glean" "v23.0.1"
44 changes: 44 additions & 0 deletions Shared/Common/Extensions/KeychainWrapper+.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,47 @@ public extension KeychainWrapper {
return KeychainWrapper(serviceName: baseBundleIdentifier, accessGroup: accessGroupIdentifier)
}
}

public extension KeychainWrapper {
func ensureStringItemAccessibility(_ accessibility: SwiftKeychainWrapper.KeychainItemAccessibility, forKey key: String) {
if self.hasValue(forKey: key) {
if self.accessibilityOfKey(key) != .afterFirstUnlock {
debugPrint("updating item \(key) with \(accessibility)")

guard let value = self.string(forKey: key) else {
debugPrint("failed to get item \(key)")
return
}

if !self.removeObject(forKey: key) {
debugPrint("failed to remove item \(key)")
}

if !self.set(value, forKey: key, withAccessibility: accessibility) {
debugPrint("failed to update item \(key)")
}
}
}
}

func ensureObjectItemAccessibility(_ accessibility: SwiftKeychainWrapper.KeychainItemAccessibility, forKey key: String) {
if self.hasValue(forKey: key) {
if self.accessibilityOfKey(key) != .afterFirstUnlock {
debugPrint("updating item \(key) with \(accessibility)")

guard let value = self.object(forKey: key) else {
debugPrint("failed to get item \(key)")
return
}

if !self.removeObject(forKey: key) {
debugPrint("failed to remove item \(key)")
}

if !self.set(value, forKey: key, withAccessibility: accessibility) {
debugPrint("failed to update item \(key)")
}
}
}
}
}
4 changes: 2 additions & 2 deletions Shared/Common/Extensions/LoginRecord+.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ extension LoginRecord: Equatable {
extension LoginRecord {
open var passwordCredentialIdentity: ASPasswordCredentialIdentity {
let serviceIdentifier = ASCredentialServiceIdentifier(identifier: self.hostname, type: .URL)
return ASPasswordCredentialIdentity(serviceIdentifier: serviceIdentifier, user: self.username ?? "", recordIdentifier: self.id)
return ASPasswordCredentialIdentity(serviceIdentifier: serviceIdentifier, user: self.username, recordIdentifier: self.id)
}

open var passwordCredential: ASPasswordCredential {
return ASPasswordCredential(user: self.username ?? "", password: self.password)
return ASPasswordCredential(user: self.username, password: self.password)
}
}
2 changes: 1 addition & 1 deletion Shared/Common/Helpers/DataStoreSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import MozillaAppServices
public protocol LoginsStorageProtocol {
func close()
func isLocked() -> Bool
func ensureUnlocked(withEncryptionKey key: String) throws
func ensureUnlockedWithKeyAndSalt(key: String, salt: String) throws
func ensureLocked()
func sync(unlockInfo: MozillaAppServices.SyncUnlockInfo) throws -> String
func wipeLocal() throws
Expand Down
8 changes: 6 additions & 2 deletions Shared/Common/Resources/BaseConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,13 @@ enum UserDefaultKey: String {
enum KeychainKey: String {
// note: these additional keys are holdovers from the previous Lockbox-owned style of
// authentication
case email, displayName, avatarURL, accountJSON
case email
case displayName
case avatarURL
case accountJSON
case salt = "sqlcipher.key.logins.salt"

static let allValues: [KeychainKey] = [.accountJSON, .email, .displayName, .avatarURL]
static let allValues: [KeychainKey] = [.accountJSON, .email, .displayName, .avatarURL, .salt]

static let oldAccountValues: [KeychainKey] = [.email, .displayName, .avatarURL]
}
3 changes: 1 addition & 2 deletions Shared/Presenter/BaseItemListPresenter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,7 @@ extension BaseItemListPresenter {
fileprivate func configurationsFromItems(_ items: [LoginRecord], detailItemId: String) -> [LoginListCellConfiguration] {
let loginCells = items.map { login -> LoginListCellConfiguration in
let titleText = login.hostname.titleFromHostname()
let usernameEmpty = login.username == "" || login.username == nil
let usernameText = usernameEmpty ? Constant.string.usernamePlaceholder : login.username!
let usernameText = login.username.isEmpty ? Constant.string.usernamePlaceholder : login.username

return LoginListCellConfiguration.Item(
title: titleText,
Expand Down
97 changes: 74 additions & 23 deletions Shared/Store/BaseDataStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,31 @@ class BaseDataStore {
public var storageState: Observable<LoginStoreState> {
return self.storageStateSubject.asObservable()
}

internal lazy var loginsDatabasePath: String? = {
let filename = "logins.db"
let profileDirName = "profile.lockbox-profile)"

// Bug 1147262: First option is for device, second is for simulator.
var rootPath: String
let sharedContainerIdentifier = AppInfo.sharedContainerIdentifier
if let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: sharedContainerIdentifier) {
rootPath = url.path
} else {
print("Unable to find the shared container. Defaulting profile location to ~/Documents instead.")
rootPath = (NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0])
}

let files = File(rootPath: URL(fileURLWithPath: rootPath).appendingPathComponent(profileDirName).path)
do {
let filePath = try files.getAndEnsureDirectory()
return URL(fileURLWithPath: filePath).appendingPathComponent(filename).path
} catch {
dispatcher.dispatch(action: SentryAction(title: "BaseDataStoreError accessing loginsDatabasePath", error: error, line: nil))
return nil
}
}()

// From: https://github.com/mozilla-lockwise/lockwise-ios-fxa-sync/blob/120bcb10967ea0f2015fc47bbf8293db57043568/Providers/Profile.swift#L168
internal var loginsKey: String? {
let key = "sqlcipher.key.logins.db"
if self.keychainWrapper.hasValue(forKey: key) {
Expand All @@ -97,6 +120,10 @@ class BaseDataStore {
return secret
}

private var salt: String? {
return setupSalt()
}

var unlockInfo: SyncUnlockInfo?

init(dispatcher: Dispatcher = Dispatcher.shared,
Expand Down Expand Up @@ -359,11 +386,12 @@ extension BaseDataStore {
}

private func unlockInternal() {
guard let loginsStorage = self.loginsStorage,
let loginsKey = self.loginsKey else { return }
guard let loginsStorage = loginsStorage,
let loginsKey = loginsKey,
let salt = salt else { return }

do {
try loginsStorage.ensureUnlocked(withEncryptionKey: loginsKey)
try loginsStorage.ensureUnlockedWithKeyAndSalt(key: loginsKey, salt: salt)
self.storageStateSubject.onNext(.Unlocked)
} catch let error as LoginsStoreError {
pushError(error)
Expand Down Expand Up @@ -413,44 +441,67 @@ extension BaseDataStore {
}

private func reset() {
guard let loginsStorage = self.loginsStorage,
let loginsKey = self.loginsKey else { return }
guard let loginsStorage = self.loginsStorage else { return }

queue.async {
do {
self.storageStateSubject.onNext(.Unprepared)
try loginsStorage.ensureUnlocked(withEncryptionKey: loginsKey)
try loginsStorage.wipeLocal()
} catch let error as LoginsStoreError {
self.pushError(error)
} catch let error {
NSLog("Unknown error wiping database: \(error.localizedDescription)")
print("Unknown error wiping database: \(error.localizedDescription)")
}
}
}

private func shutdown() {
self.loginsStorage?.close()
loginsStorage?.close()
}

private func initializeLoginsStorage() {
let filename = "logins.db"

let profileDirName = "profile.lockbox-profile)"
guard let loginsDatabasePath = loginsDatabasePath else { return }
loginsStorage = dataStoreSupport.createLoginsStorage(databasePath: loginsDatabasePath)
}

private func setupSalt() -> String? {
guard let loginsDatabasePath = loginsDatabasePath,
let loginsKey = loginsKey else { return nil }

let key = KeychainKey.salt.rawValue
if keychainWrapper.hasValue(forKey: key, withAccessibility: .afterFirstUnlock) {
return keychainWrapper.string(forKey: key, withAccessibility: .afterFirstUnlock)
}

// Bug 1147262: First option is for device, second is for simulator.
var rootPath: String
let sharedContainerIdentifier = AppInfo.sharedContainerIdentifier
if let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: sharedContainerIdentifier) {
rootPath = url.path
} else {
print("Unable to find the shared container. Defaulting profile location to ~/Documents instead.")
rootPath = (NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0])
let val = setupPlaintextHeaderAndGetSalt(databasePath: loginsDatabasePath, encryptionKey: loginsKey)
keychainWrapper.set(val, forKey: key, withAccessibility: .afterFirstUnlock)
return val
}

// Migrate and return the salt, or create a new salt
// Also, in the event of an error, returns a new salt.
private func setupPlaintextHeaderAndGetSalt(databasePath: String, encryptionKey: String) -> String {
guard FileManager.default.fileExists(atPath: databasePath) else {
return createRandomSalt()
}
guard let db = loginsStorage as? LoginsStorage else {
return createRandomSalt()
}

let files = File(rootPath: URL(fileURLWithPath: rootPath).appendingPathComponent(profileDirName).path)
let file = URL(fileURLWithPath: (try! files.getAndEnsureDirectory())).appendingPathComponent(filename).path
do {
let salt = try db.getDbSaltForKey(key: encryptionKey)
try db.migrateToPlaintextHeader(key: encryptionKey, salt: salt)
return salt
} catch {
print("setupPlaintextHeaderAndGetSalt failed with error: \(error)")
self.dispatcher.dispatch(action: SentryAction(title: "setupPlaintextHeaderAndGetSalt failed", error: error, line: nil))
// the database exists. but we didn't store the salt?
return createRandomSalt()
}
}

self.loginsStorage = dataStoreSupport.createLoginsStorage(databasePath: file)
private func createRandomSalt() -> String {
return UUID().uuidString.replacingOccurrences(of: "-", with: "")
}

}
2 changes: 1 addition & 1 deletion docs/install.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
1. Install Xcode version 11.2.1 + Swift 5.1.2
1. Install Xcode version 11.3 + Swift 5.1.3

2. Install the latest [Xcode developer tools](https://developer.apple.com/xcode/downloads/) from Apple

Expand Down
44 changes: 35 additions & 9 deletions lockbox-ios/Common/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,22 @@ class AppDelegate: UIResponder, UIApplicationDelegate {

var window: UIWindow?

var isFirstRun: Bool {
get {
return UserDefaults.standard.string(forKey: PostFirstRunKey) == nil
}
set {
UserDefaults.standard.set(newValue ? nil : "NO", forKey: PostFirstRunKey)
}
}

func application(_ application: UIApplication, willFinishLaunchingWithOptions
launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
if isFirstRun {
KeychainWrapper.wipeKeychain()
} else {
checkForKeychainVersionAbnormalities()
}
_ = AccountStore.shared
_ = DataStore.shared
_ = ExternalLinkStore.shared
Expand All @@ -27,22 +41,20 @@ class AppDelegate: UIResponder, UIApplicationDelegate {

func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
self.window = UIWindow(frame: UIScreen.main.bounds)
window = UIWindow(frame: UIScreen.main.bounds)

self.window?.rootViewController = RootView()
self.window?.makeKeyAndVisible()
window?.rootViewController = RootView()
window?.makeKeyAndVisible()

// This key will not be set on the first run of the application, only on subsequent runs.
let firstRun = UserDefaults.standard.string(forKey: PostFirstRunKey) == nil
if firstRun {
if isFirstRun {
Dispatcher.shared.dispatch(action: AccountAction.clear)
Dispatcher.shared.dispatch(action: DataStoreAction.reset)
UserDefaults.standard.set(false, forKey: PostFirstRunKey)
isFirstRun = false
} else {
checkForUpgrades()
}

if !firstRun {
self.checkForUpgrades()
}
UserDefaults.standard.set(Constant.app.appVersionCode, forKey: LocalUserDefaultKey.appVersionCode.rawValue)

AppearanceHelper.shared.setupAppearance()
Expand Down Expand Up @@ -84,6 +96,20 @@ extension AppDelegate {
Dispatcher.shared.dispatch(action: LifecycleAction.upgrade(from: previous, to: current))
}
}

func checkForKeychainVersionAbnormalities() {
let current = Constant.app.appVersionCode
let previous = UserDefaults.standard.integer(forKey: LocalUserDefaultKey.appVersionCode.rawValue)
let keychainWrapper = KeychainWrapper.sharedAppContainerKeychain
if previous == 3 && current == 4 {
//if we already have keychain value for salt and came from a version that should not have this data, delete it
keychainWrapper.removeObject(forKey: KeychainKey.salt.rawValue)
} else if previous > current {
//this would mean a user had an upgraded test version and has now downgraded to a previous version
//we should wipe keychain data to remove any possible abnormalities
KeychainWrapper.wipeKeychain()
}
}
}

extension AppDelegate {
Expand Down
2 changes: 1 addition & 1 deletion lockbox-ios/Common/Resources/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import UIKit

extension Constant.app {
static let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
static let appVersionCode = 3 // this is the version of the app that will drive updates.
static let appVersionCode = 4 // this is the version of the app that will drive updates.
static let sumoURL = "https://support.mozilla.org/en-US/kb/"
static let privacyURL = sumoURL + "firefox-lockwise-and-privacy"
static let provideFeedbackURL = "https://qsurvey.mozilla.com/s3/Lockbox-Input?ver=\(appVersion ?? "1.1")"
Expand Down
9 changes: 4 additions & 5 deletions lockbox-ios/Store/AccountStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,7 @@ extension AccountStore {
}

private func clear() {
for identifier in KeychainKey.allValues {
_ = self.keychainWrapper.removeObject(forKey: identifier.rawValue)
}
KeychainWrapper.wipeKeychain()

self.webData.fetchDataRecords(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes()) { records in
self.webData.removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), for: records) { }
Expand All @@ -154,8 +152,9 @@ extension AccountStore {

_profile.onNext(nil)
_syncCredentials.onNext(nil)

initFxa()
DispatchQueue.global(qos: .background).async {
self.initFxa()
}
}

private func clearOldKeychainValues() {
Expand Down
2 changes: 1 addition & 1 deletion lockbox-iosTests/BaseDataStoreSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class BaseDataStoreSpec: QuickSpec {
return lockedStub
}

func ensureUnlocked(withEncryptionKey key: String) throws {
func ensureUnlockedWithKeyAndSalt(key: String, salt: String) throws {
self.ensureUnlockedArgument = key
}

Expand Down