Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 0 additions & 23 deletions Loop/Managers/KeychainManager+Loop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,10 @@
import Foundation


private let DexcomShareURL = URL(string: "https://share1.dexcom.com")!
private let NightscoutAccount = "NightscoutAPI"


extension KeychainManager {
func setDexcomShareUsername(_ username: String?, password: String?) throws {
let credentials: InternetCredentials?

if let username = username, let password = password {
credentials = InternetCredentials(username: username, password: password, url: DexcomShareURL)
} else {
credentials = nil
}

try replaceInternetCredentials(credentials, forURL: DexcomShareURL)
}

func getDexcomShareCredentials() -> (username: String, password: String)? {
do {
let credentials = try getInternetCredentials(url: DexcomShareURL)

return (username: credentials.username, password: credentials.password)
} catch {
return nil
}
}

func setNightscoutURL(_ url: URL?, secret: String?) {
let credentials: InternetCredentials?

Expand Down
32 changes: 25 additions & 7 deletions Loop/Managers/KeychainManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ struct KeychainManager {
return query
}

private func queryForInternetPassword(account: String? = nil, url: URL? = nil) -> Query {
private func queryForInternetPassword(account: String? = nil, url: URL? = nil, label: String? = nil) -> Query {
var query = self.query(by: kSecClassInternetPassword)

if let account = account {
Expand All @@ -68,6 +68,10 @@ struct KeychainManager {
}
}

if let label = label {
query[kSecAttrLabel as String] = label as NSObject?
}

return query
}

Expand Down Expand Up @@ -135,8 +139,8 @@ struct KeychainManager {

// MARK – Internet Passwords

func setInternetPassword(_ password: String, forAccount account: String, atURL url: URL) throws {
var query = try updatedQuery(queryForInternetPassword(account: account, url: url), withPassword: password)
func setInternetPassword(_ password: String, account: String, atURL url: URL, label: String? = nil) throws {
var query = try updatedQuery(queryForInternetPassword(account: account, url: url, label: label), withPassword: password)

query[kSecAttrAccount as String] = account as NSObject?

Expand All @@ -146,6 +150,10 @@ struct KeychainManager {
}
}

if let label = label {
query[kSecAttrLabel as String] = label as NSObject?
}

let statusCode = SecItemAdd(query as CFDictionary, nil)

guard statusCode == errSecSuccess else {
Expand All @@ -159,7 +167,17 @@ struct KeychainManager {
try delete(query)

if let credentials = credentials {
try setInternetPassword(credentials.password, forAccount: credentials.username, atURL: credentials.url)
try setInternetPassword(credentials.password, account: credentials.username, atURL: credentials.url)
}
}

func replaceInternetCredentials(_ credentials: InternetCredentials?, forLabel label: String) throws {
let query = queryForInternetPassword(label: label)

try delete(query)

if let credentials = credentials {
try setInternetPassword(credentials.password, account: credentials.username, atURL: credentials.url, label: label)
}
}

Expand All @@ -169,12 +187,12 @@ struct KeychainManager {
try delete(query)

if let credentials = credentials {
try setInternetPassword(credentials.password, forAccount: credentials.username, atURL: credentials.url)
try setInternetPassword(credentials.password, account: credentials.username, atURL: credentials.url)
}
}

func getInternetCredentials(account: String? = nil, url: URL? = nil) throws -> InternetCredentials {
var query = queryForInternetPassword(account: account, url: url)
func getInternetCredentials(account: String? = nil, url: URL? = nil, label: String? = nil) throws -> InternetCredentials {
var query = queryForInternetPassword(account: account, url: url, label: label)

query[kSecReturnData as String] = kCFBooleanTrue
query[kSecReturnAttributes as String] = kCFBooleanTrue
Expand Down
8 changes: 4 additions & 4 deletions Loop/Managers/RemoteDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,17 @@ final class RemoteDataManager {

var shareService: ShareService {
didSet {
try! keychain.setDexcomShareUsername(shareService.username, password: shareService.password)
try! keychain.setDexcomShareUsername(shareService.username, password: shareService.password, url: shareService.url)
}
}

private let keychain = KeychainManager()

init() {
if let (username, password) = keychain.getDexcomShareCredentials() {
shareService = ShareService(username: username, password: password)
if let (username, password, url) = keychain.getDexcomShareCredentials() {
shareService = ShareService(username: username, password: password, url: url)
} else {
shareService = ShareService(username: nil, password: nil)
shareService = ShareService(username: nil, password: nil, url: nil)
}

if let (siteURL, APISecret) = keychain.getNightscoutCredentials() {
Expand Down
24 changes: 18 additions & 6 deletions Loop/Models/ServiceAuthentication/ServiceCredential.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,32 @@
import UIKit


// Represents a credential for a service, including its text input traits
/// Represents a credential for a service, including its text input traits
struct ServiceCredential {
// The localized title of the credential (e.g. "Username")
/// The localized title of the credential (e.g. "Username")
let title: String

// The localized placeholder text to assist text input
/// The localized placeholder text to assist text input
let placeholder: String?

// Whether the credential is considered secret. Correponds to the `secureTextEntry` trait.
/// Whether the credential is considered secret. Correponds to the `secureTextEntry` trait.
let isSecret: Bool

// The type of keyboard to use to enter the credential
/// The type of keyboard to use to enter the credential
let keyboardType: UIKeyboardType

// The credential value
/// The credential value
var value: String?

/// A set of valid values for presenting a selection. The first item is the default.
var options: [(title: String, value: String)]?

init(title: String, placeholder: String? = nil, isSecret: Bool, keyboardType: UIKeyboardType = .asciiCapable, value: String?, options: [(title: String, value: String)]? = nil) {
self.title = title
self.placeholder = placeholder
self.isSecret = isSecret
self.keyboardType = keyboardType
self.value = value
self.options = options
}
}
67 changes: 62 additions & 5 deletions Loop/Models/ServiceAuthentication/ShareService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,32 @@ class ShareService: ServiceAuthentication {

let title: String = NSLocalizedString("Dexcom Share", comment: "The title of the Dexcom Share service")

init(username: String?, password: String?) {
init(username: String?, password: String?, url: URL?) {
credentials = [
ServiceCredential(
title: NSLocalizedString("Username", comment: "The title of the Dexcom share username credential"),
placeholder: nil,
isSecret: false,
keyboardType: .asciiCapable,
value: username
),
ServiceCredential(
title: NSLocalizedString("Password", comment: "The title of the Dexcom share password credential"),
placeholder: nil,
isSecret: true,
keyboardType: .asciiCapable,
value: password
),
ServiceCredential(
title: NSLocalizedString("Server", comment: "The title of the Dexcom share server URL credential"),
isSecret: false,
value: url?.absoluteString,
options: [
(title: NSLocalizedString("US", comment: "U.S. share server option title"),
value: DexcomShareURL.absoluteString)
]
)
]

if let username = username, let password = password {
if let username = username, let password = password, url != nil {
isAuthorized = true
client = ShareClient(username: username, password: password)
}
Expand All @@ -51,10 +58,18 @@ class ShareService: ServiceAuthentication {
return credentials[1].value
}

var url: URL? {
guard let urlString = credentials[2].value else {
return nil
}

return URL(string: urlString)
}

var isAuthorized: Bool = false

func verify(_ completion: @escaping (_ success: Bool, _ error: Error?) -> Void) {
guard let username = username, let password = password else {
guard let username = username, let password = password, url != nil else {
completion(false, nil)
return
}
Expand All @@ -69,7 +84,49 @@ class ShareService: ServiceAuthentication {
func reset() {
credentials[0].value = nil
credentials[1].value = nil
credentials[2].value = nil
isAuthorized = false
client = nil
}
}


private let DexcomShareURL = URL(string: "https://share1.dexcom.com")!
private let DexcomShareServiceLabel = "DexcomShare1"


extension KeychainManager {
func setDexcomShareUsername(_ username: String?, password: String?, url: URL?) throws {
let credentials: InternetCredentials?

if let username = username, let password = password, let url = url {
credentials = InternetCredentials(username: username, password: password, url: url)
} else {
credentials = nil
}

// Replace the legacy URL-keyed credentials
try replaceInternetCredentials(nil, forURL: DexcomShareURL)

try replaceInternetCredentials(credentials, forLabel: DexcomShareServiceLabel)
}

func getDexcomShareCredentials() -> (username: String, password: String, url: URL)? {
do { // Silence all errors and return nil
do {
let credentials = try getInternetCredentials(label: DexcomShareServiceLabel)

return (username: credentials.username, password: credentials.password, url: credentials.url)
} catch KeychainManagerError.copy {
// Fetch and replace the legacy URL-keyed credentials
let credentials = try getInternetCredentials(url: DexcomShareURL)

try setDexcomShareUsername(credentials.username, password: credentials.password, url: credentials.url)

return (username: credentials.username, password: credentials.password, url: credentials.url)
}
} catch {
return nil
}
}
}
46 changes: 35 additions & 11 deletions Loop/View Controllers/AuthenticationViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ final class AuthenticationViewController<T: ServiceAuthentication>: UITableViewC
}
}

var credentials: [ServiceCredential] {
switch state {
case .authorized:
return authentication.credentials.filter({ !$0.isSecret })
default:
return authentication.credentials
}
}

init(authentication: T) {
self.authentication = authentication

Expand Down Expand Up @@ -89,12 +98,7 @@ final class AuthenticationViewController<T: ServiceAuthentication>: UITableViewC
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch Section(rawValue: section)! {
case .credentials:
switch state {
case .authorized:
return authentication.credentials.filter({ !$0.isSecret }).count
default:
return authentication.credentials.count
}
return credentials.count
case .button:
return 1
}
Expand Down Expand Up @@ -126,16 +130,23 @@ final class AuthenticationViewController<T: ServiceAuthentication>: UITableViewC
case .credentials:
let cell = tableView.dequeueReusableCell(withIdentifier: AuthenticationTableViewCell.className, for: indexPath) as! AuthenticationTableViewCell

let credential = authentication.credentials[indexPath.row]
let credentials = self.credentials
let credential = credentials[indexPath.row]

cell.titleLabel.text = credential.title
cell.textField.tag = indexPath.row
cell.textField.keyboardType = credential.keyboardType
cell.textField.isSecureTextEntry = credential.isSecret
cell.textField.returnKeyType = (indexPath.row < authentication.credentials.count - 1) ? .next : .done
cell.textField.returnKeyType = (indexPath.row < credentials.count - 1) ? .next : .done
cell.textField.text = credential.value
cell.textField.placeholder = credential.placeholder ?? NSLocalizedString("Required", comment: "The default placeholder string for a credential")

if let options = credential.options {
let picker = CredentialOptionPicker(options: options)
picker.value = credential.value

cell.credentialOptionPicker = picker
}

cell.textField.delegate = self

switch state {
Expand All @@ -149,7 +160,7 @@ final class AuthenticationViewController<T: ServiceAuthentication>: UITableViewC
}
}

private func validate() {
fileprivate func validate() {
state = .verifying
}

Expand All @@ -173,7 +184,16 @@ final class AuthenticationViewController<T: ServiceAuthentication>: UITableViewC
// MARK: - UITextFieldDelegate

func textFieldDidEndEditing(_ textField: UITextField) {
authentication.credentials[textField.tag].value = textField.text
let point = tableView.convert(textField.frame.origin, from: textField.superview)

guard case .unauthorized = state,
let indexPath = tableView.indexPathForRow(at: point),
let cell = tableView.cellForRow(at: IndexPath(row: indexPath.row, section: indexPath.section)) as? AuthenticationTableViewCell
else {
return
}

authentication.credentials[indexPath.row].value = cell.value
}

func textFieldShouldReturn(_ textField: UITextField) -> Bool {
Expand All @@ -191,6 +211,10 @@ final class AuthenticationViewController<T: ServiceAuthentication>: UITableViewC

return true
}

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
return textField.inputView == nil
}
}


Expand Down
Loading