Commit fe94ed97 authored by Jose Blaya's avatar Jose Blaya
Browse files

Merge branch '68-auth-token-remove-the-storage-of-the-plain-user-password'...

Merge branch '68-auth-token-remove-the-storage-of-the-plain-user-password' into '65-auth-token-store-the-token-to-be-used-in-all-account-requests'

Resolve "AUTH-TOKEN Remove the storage of the plain user password"

See merge request ios/client-library-apple!124
parents 1a3b4881 cf3954db
......@@ -249,6 +249,7 @@
84D5DA702126CE2900F753F8 /* QRCameraScannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D5DA6F2126CE2900F753F8 /* QRCameraScannerViewController.swift */; };
A6680249A33738280B9AD733 /* Pods_PIALibrary_PIALibraryHost_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 566B62D4C21DD5A90EF1BAE6 /* Pods_PIALibrary_PIALibraryHost_iOS.framework */; };
DD58F4BB21AEB99C00D043F7 /* GlossToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD58F4BA21AEB99C00D043F7 /* GlossToken.swift */; };
DD58F4BD21AEF76100D043F7 /* String+Components.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD58F4BC21AEF76100D043F7 /* String+Components.swift */; };
DDC812482176166600CB290C /* SwiftGen+ScenesStoryboards.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC812472176166500CB290C /* SwiftGen+ScenesStoryboards.swift */; };
DDC8124A217617F900CB290C /* SwiftGen+SeguesStoryboards.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC81249217617F900CB290C /* SwiftGen+SeguesStoryboards.swift */; };
/* End PBXBuildFile section */
......@@ -493,6 +494,7 @@
BB47885FCCA9DDFF53237D2C /* Pods-PIALibrary-PIALibraryTests-iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PIALibrary-PIALibraryTests-iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-PIALibrary-PIALibraryTests-iOS/Pods-PIALibrary-PIALibraryTests-iOS.release.xcconfig"; sourceTree = "<group>"; };
D9FDC8980713EDCEDD51B936 /* Pods-PIALibrary-PIALibrary-iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PIALibrary-PIALibrary-iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-PIALibrary-PIALibrary-iOS/Pods-PIALibrary-PIALibrary-iOS.debug.xcconfig"; sourceTree = "<group>"; };
DD58F4BA21AEB99C00D043F7 /* GlossToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlossToken.swift; sourceTree = "<group>"; };
DD58F4BC21AEF76100D043F7 /* String+Components.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Components.swift"; sourceTree = "<group>"; };
DDC812472176166500CB290C /* SwiftGen+ScenesStoryboards.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SwiftGen+ScenesStoryboards.swift"; sourceTree = "<group>"; };
DDC81249217617F900CB290C /* SwiftGen+SeguesStoryboards.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SwiftGen+SeguesStoryboards.swift"; sourceTree = "<group>"; };
E526D8161FFFEE65AC2DF844 /* Pods-PIALibrary-PIALibraryTests-iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PIALibrary-PIALibraryTests-iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-PIALibrary-PIALibraryTests-iOS/Pods-PIALibrary-PIALibraryTests-iOS.debug.xcconfig"; sourceTree = "<group>"; };
......@@ -580,6 +582,7 @@
0E9D628B1FDEAC2D009A90CF /* module.modulemap */,
0E492C591FE5EA06007F23DF /* CMacros.h */,
0E492C5A1FE5EA06007F23DF /* CMacros.m */,
DD58F4BC21AEF76100D043F7 /* String+Components.swift */,
);
path = iOS;
sourceTree = "<group>";
......@@ -1556,6 +1559,7 @@
0EB8C0661F9CD38B005857E4 /* LoginViewController.swift in Sources */,
0EA8072C20A1C7A30033EC1A /* RedeemRequest.swift in Sources */,
0EC7A2A41F9D3D78006DDB91 /* RenewRequest.swift in Sources */,
DD58F4BD21AEF76100D043F7 /* String+Components.swift in Sources */,
0EE771091F9D21020029A77B /* Payment.swift in Sources */,
0E53A8531FE5D73F000C2A18 /* Client+Mock.swift in Sources */,
0E392D9D1FE31D630002160D /* MockVPNProvider.swift in Sources */,
......
......@@ -25,6 +25,9 @@ public protocol AccountProvider: class {
/// The current auth token, or 'nil' if logged out.
var token: String? { get }
/// The public username to be displayed in the views.
var publicUsername: String? { get }
/// The password reference object associated with the currentUser, or `nil` if logged out.
var currentPasswordReference: Data? { get }
......@@ -77,6 +80,11 @@ public protocol AccountProvider: class {
*/
func logout(_ callback: SuccessLibraryCallback?)
/**
Remove all data from the plain and secure internal database
*/
func cleanDatabase()
#if os(iOS)
/**
Lists the available plans with their corresponding product to purchase in order to get them.
......
......@@ -12,7 +12,7 @@ protocol PlainStore: class {
// MARK: Account
var username: String? { get set }
var publicUsername: String? { get set }
var accountInfo: AccountInfo? { get set }
......
......@@ -13,6 +13,10 @@ protocol SecureStore: class {
@discardableResult func setPublicKey(withData data: Data) -> SecKey?
func username() -> String?
func setUsername(_ username: String?)
func password(for username: String) -> String?
func setPassword(_ password: String?, for username: String)
......
......@@ -41,15 +41,22 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
#endif
var isLoggedIn: Bool {
guard let username = accessedDatabase.plain.username else {
guard let username = accessedDatabase.secure.username() else {
return false
}
return (accessedDatabase.secure.password(for: username) != nil)
}
var publicUsername: String? {
guard let username = accessedDatabase.plain.publicUsername else {
return nil
}
return username
}
var currentUser: UserAccount? {
get {
guard let username = accessedDatabase.plain.username else {
guard let username = accessedDatabase.secure.username() else {
return nil
}
guard let password = accessedDatabase.secure.password(for: username) else {
......@@ -62,21 +69,22 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
}
set {
if let user = newValue {
accessedDatabase.plain.username = user.credentials.username
accessedDatabase.plain.publicUsername = user.credentials.username
accessedDatabase.secure.setPassword(user.credentials.password, for: user.credentials.username)
accessedDatabase.plain.accountInfo = user.info
} else {
if let username = accessedDatabase.plain.username {
if let username = accessedDatabase.secure.username() {
accessedDatabase.secure.setPassword(nil, for: username)
accessedDatabase.secure.setUsername(nil)
}
accessedDatabase.plain.username = nil
accessedDatabase.plain.publicUsername = nil
accessedDatabase.plain.accountInfo = nil
}
}
}
var token: String? {
guard let username = accessedDatabase.plain.username else {
guard let username = accessedDatabase.secure.username() else {
return nil
}
return accessedDatabase.secure.token(for: accessedDatabase.secure.tokenKey(for: username))
......@@ -84,7 +92,7 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
var currentPasswordReference: Data? {
guard let username = accessedDatabase.plain.username else {
guard let username = accessedDatabase.secure.username() else {
return nil
}
return accessedDatabase.secure.passwordReference(for: username)
......@@ -99,6 +107,19 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
}
#endif
private func updateDatabaseWith(_ token: String, andUsername username: String) {
let tokenComponents = token.split(by: token.count/2)
if let first = tokenComponents.first,
let last = tokenComponents.last {
self.accessedDatabase.plain.publicUsername = username
self.accessedDatabase.secure.setUsername(first)
self.accessedDatabase.secure.setToken(token,
for: self.accessedDatabase.secure.tokenKey(for: first))
self.accessedDatabase.secure.setPassword(last,
for: first)
}
}
func login(with request: LoginRequest, _ callback: ((UserAccount?, Error?) -> Void)?) {
guard !isLoggedIn else {
preconditionFailure()
......@@ -110,10 +131,9 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
callback?(nil, error)
return
}
self.accessedDatabase.plain.username = request.credentials.username
self.accessedDatabase.secure.setToken(token,
for: self.accessedDatabase.secure.tokenKey(for: request.credentials.username))
self.updateDatabaseWith(token,
andUsername: request.credentials.username)
self.webServices.info(token: token) { (accountInfo, error) in
guard let accountInfo = accountInfo else {
......@@ -122,8 +142,6 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
}
//Save after confirm the login was successful.
self.accessedDatabase.secure.setPassword(request.credentials.password,
for: request.credentials.username)
self.accessedDatabase.plain.accountInfo = accountInfo
let user = UserAccount(credentials: request.credentials, info: accountInfo)
......@@ -138,10 +156,30 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
}
func refreshAccountInfo(_ callback: ((AccountInfo?, Error?) -> Void)?) {
guard let token = self.token else {
preconditionFailure()
guard let user = currentUser else {
preconditionFailure()
}
self.webServices.token(credentials: user.credentials) { (token, error) in
if let token = token {
self.updateDatabaseWith(token,
andUsername: user.credentials.username)
self.accountInfoWith(token, callback)
}
}
return
}
accountInfoWith(token, callback)
}
private func accountInfoWith(_ token: String, _ callback: ((AccountInfo?, Error?) -> Void)?) {
webServices.info(token: token) { (accountInfo, error) in
guard let accountInfo = accountInfo else {
callback?(nil, error)
......@@ -153,7 +191,6 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
])
callback?(accountInfo, nil)
}
}
func update(with request: UpdateAccountRequest, _ callback: ((AccountInfo?, Error?) -> Void)?) {
......@@ -184,12 +221,7 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
guard isLoggedIn else {
preconditionFailure()
}
if let username = accessedDatabase.plain.username {
accessedDatabase.secure.setPassword(nil, for: username)
accessedDatabase.secure.setToken(nil, for: accessedDatabase.secure.tokenKey(for: username))
}
accessedDatabase.plain.username = nil
accessedDatabase.plain.accountInfo = nil
cleanDatabase()
Macros.postNotification(.PIAAccountDidLogout)
callback?(nil)
}
......@@ -266,7 +298,7 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
self.accessedStore.finishTransaction(transaction, success: true)
}
self.accessedDatabase.plain.lastSignupEmail = nil
self.accessedDatabase.plain.username = credentials.username
self.accessedDatabase.plain.publicUsername = credentials.username
self.accessedDatabase.secure.setPassword(credentials.password, for: credentials.username)
let user = UserAccount(credentials: credentials, info: nil)
......@@ -292,14 +324,35 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
callback?(nil, error)
return
}
self.accessedDatabase.plain.username = credentials.username
self.accessedDatabase.secure.setPassword(credentials.password, for: credentials.username)
let user = UserAccount(credentials: credentials, info: nil)
Macros.postNotification(.PIAAccountDidSignup, [
.user: user
])
callback?(user, nil)
self.webServices.token(credentials: credentials) { (token, error) in
guard let token = token else {
callback?(nil, error)
return
}
let tokenComponents = token.split(by: token.count/2)
if let first = tokenComponents.first,
let last = tokenComponents.last {
self.accessedDatabase.plain.publicUsername = credentials.username
self.accessedDatabase.secure.setUsername(first)
self.accessedDatabase.secure.setToken(token,
for: self.accessedDatabase.secure.tokenKey(for: first))
self.accessedDatabase.secure.setPassword(last,
for: first)
}
let user = UserAccount(credentials: credentials, info: nil)
Macros.postNotification(.PIAAccountDidSignup, [
.user: user
])
callback?(user, nil)
}
}
}
......@@ -381,6 +434,19 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
}
}
}
/**
Remove all data from the plain and secure internal database
*/
func cleanDatabase() {
if let username = accessedDatabase.secure.username() {
accessedDatabase.secure.setPassword(nil, for: username)
accessedDatabase.secure.setUsername(nil)
accessedDatabase.secure.setToken(nil, for: accessedDatabase.secure.tokenKey(for: username))
}
accessedDatabase.plain.publicUsername = nil
accessedDatabase.plain.accountInfo = nil
}
#endif
// MARK: WebServicesConsumer
......
......@@ -57,7 +57,7 @@ extension Client {
- Returns: `self`
*/
@discardableResult public func truncate() -> Self {
if let username = plain.username {
if let username = plain.publicUsername {
secure.clear(for: username)
}
plain.clear()
......
......@@ -63,6 +63,21 @@ class KeychainStore: SecureStore {
}
}
extension KeychainStore {
func username() -> String? {
return try? backend.username()
}
func setUsername(_ username: String?) {
if let username = username {
try? backend.set(username: username)
} else {
backend.removeUsername()
}
}
}
extension KeychainStore {
func token(for username: String) -> String? {
......
......@@ -76,7 +76,7 @@ class UserDefaultsStore: PlainStore, ConfigurationAccess {
// MARK: Account
var username: String? {
var publicUsername: String? {
get {
return backend.string(forKey: Entries.username)
}
......
......@@ -11,7 +11,6 @@ import Foundation
/// Simulates account-related operations
public class MockAccountProvider: AccountProvider, WebServicesConsumer {
/// Mocks the outcome of a sign-up operation.
///
/// - Seealso: `AccountProvider.signup(...)`
......@@ -115,6 +114,10 @@ public class MockAccountProvider: AccountProvider, WebServicesConsumer {
public var token: String? {
return "TOKEN"
}
public var publicUsername: String? {
return "p0000000"
}
/// :nodoc:
public var currentUser: UserAccount? {
......@@ -166,6 +169,11 @@ public class MockAccountProvider: AccountProvider, WebServicesConsumer {
delegate.logout(callback)
}
/// :nodoc:
public func cleanDatabase() {
delegate.cleanDatabase()
}
#if os(iOS)
/// :nodoc:
public func listPlanProducts(_ callback: (([Plan : InAppProduct]?, Error?) -> Void)?) {
......
......@@ -311,7 +311,7 @@ protocol WelcomeCompletionDelegate: class {
}
class EphemeralAccountProvider: AccountProvider, ProvidersAccess, InAppAccess {
// XXX: we want legit web services calls, yet allow the option to mock them
private var webServices: WebServices? {
guard let accountProvider = accessedProviders.accountProvider as? WebServicesConsumer else {
......@@ -330,6 +330,8 @@ class EphemeralAccountProvider: AccountProvider, ProvidersAccess, InAppAccess {
var currentUser: UserAccount?
var publicUsername: String?
var currentPasswordReference: Data? {
return nil
}
......@@ -371,6 +373,10 @@ class EphemeralAccountProvider: AccountProvider, ProvidersAccess, InAppAccess {
func logout(_ callback: SuccessLibraryCallback?) {
fatalError("Not implemented")
}
func cleanDatabase() {
fatalError("Not implemented")
}
func listPlanProducts(_ callback: (([Plan : InAppProduct]?, Error?) -> Void)?) {
accessedProviders.accountProvider.listPlanProducts(callback)
......
......@@ -26,6 +26,8 @@ public class Keychain {
private let service: String?
private let accessGroup: String?
private let usernameKey = "USERNAME_KEY"
/**
Default initializer. Uses the default keychain associated with the main bundle identifier.
......@@ -227,6 +229,64 @@ public class Keychain {
}
}
extension Keychain {
// MARK: Username
/// :nodoc:
public func set(username: String) throws {
removeUsername()
var query = [String: Any]()
setScope(query: &query)
query[kSecClass as String] = kSecClassGenericPassword
query[kSecAttrAccount as String] = usernameKey
query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
query[kSecValueData as String] = username.data(using: .utf8)
let status = SecItemAdd(query as CFDictionary, nil)
guard (status == errSecSuccess) else {
throw KeychainError.add
}
}
/// :nodoc:
@discardableResult public func removeUsername() -> Bool {
var query = [String: Any]()
setScope(query: &query)
query[kSecClass as String] = kSecClassGenericPassword
query[kSecAttrAccount as String] = usernameKey
let status = SecItemDelete(query as CFDictionary)
return (status == errSecSuccess)
}
/// :nodoc:
public func username() throws -> String {
var query = [String: Any]()
setScope(query: &query)
query[kSecClass as String] = kSecClassGenericPassword
query[kSecAttrAccount as String] = usernameKey
//query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
query[kSecMatchLimit as String] = kSecMatchLimitOne
query[kSecReturnData as String] = true
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard (status == errSecSuccess) else {
throw KeychainError.notFound
}
guard let data = result as? Data else {
throw KeychainError.notFound
}
guard let token = String(data: data, encoding: .utf8) else {
throw KeychainError.notFound
}
return token
}
}
extension Keychain {
// MARK: Token
......
//
// String+Components.swift
// PIALibrary-iOS
//
// Created by Jose Antonio Blaya Garcia on 28/11/2018.
// Copyright © 2018 London Trust Media. All rights reserved.
//
import Foundation
extension String {
func split(by length: Int) -> [String] {
var startIndex = self.startIndex
var results = [Substring]()
while startIndex < self.endIndex {
let endIndex = self.index(startIndex, offsetBy: length, limitedBy: self.endIndex) ?? self.endIndex
results.append(self[startIndex..<endIndex])
startIndex = endIndex
}
return results.map { String($0) }
}
}
......@@ -23,11 +23,13 @@ class AccountTests: XCTestCase {
Client.database = Client.Database(group: "group.com.privateinternetaccess").truncate()
Client.bootstrap()
Client.providers.accountProvider.cleanDatabase()
}
override func tearDown() {
super.tearDown()
unregisterNotifications()
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment