Joe Vitamins
Joe Vitamins

Reputation: 63

SecRequestSharedWebCredential credentials contains 'Passwords not saved'?

We retrieve any saved passwords through the function:

SecRequestSharedWebCredential(NULL, NULL, ^(CFArrayRef credentials, CFErrorRef error) {
    if (!error && CFArrayGetCount(credentials)) {
        CFDictionaryRef credential = CFArrayGetValueAtIndex(credentials, 0);
        if (credential > 0) {
            CFDictionaryRef credential = CFArrayGetValueAtIndex(credentials, 0);
            NSString *username = CFDictionaryGetValue(credential, kSecAttrAccount);
            NSString *password = CFDictionaryGetValue(credential, kSecSharedPassword);
            dispatch_async(dispatch_get_main_queue(), ^{
                //Updates the UI here.
            });
        }
    }
});

The issue is that on IOS 9.3.3 iPhone 6 A1524, we get the prompt with an entry called 'Passwords not saved'. There is no error message to suggest the no passwords have been found. Because the array > 0, it completes the form with the entry.

Why is this the case? We thought the prompt does not appear if no passwords are stored under your entitled domains.

Any suggestions?

Thank you.

Upvotes: 4

Views: 1933

Answers (2)

albertodebortoli
albertodebortoli

Reputation: 1838

I bumped into the same issue and wanted to add that the spaces in "Passwords not saved" are not real spaces. Not sure why, maybe something odd when converting from CFString.

Either way, since Apple documentation is still in ObjC and their Security framework is still very much CoreFoundation-heavy, I thought it'd be nice to post the whole Swift 5 code I've written for the Shared Web Credentials wrapper.

It has nice error management logic (to adjust since you might not have the same ErrorBuilder API). About the weird spaces, when copied from Xcode to StackOverflow, they turn into real spaces, hence the extra logic in the String extension.

There is nothing better online from what I've seen.

//
//  CredentialsRepository.swift
//  Created by Alberto De Bortoli on 26/07/2019.
//

import Foundation

public typealias Username = String
public typealias Password = String

public struct Credentials {
    public let username: Username
    public let password: Password
}

public enum GetCredentialsResult {
    case success(Credentials)
    case cancelled
    case failure(Error)
}

public enum SaveCredentialsResult {
    case success
    case failure(Error)
}

protocol CredentialsRepository {
    func getCredentials(completion: @escaping (GetCredentialsResult) -> Void)
    func saveCredentials(_ credentials: Credentials, completion: @escaping (SaveCredentialsResult) -> Void)
}
//
//  SharedWebCredentialsController.swift
//  Created by Alberto De Bortoli on 26/07/2019.
//

class SharedWebCredentialsController {

    let domain: String

    init(domain: String) {
        self.domain = domain
    }
}

extension SharedWebCredentialsController: CredentialsRepository {

    func getCredentials(completion: @escaping (GetCredentialsResult) -> Void) {
        SecRequestSharedWebCredential(domain as CFString, .none) { cfArrayCredentials, cfError in
            switch (cfArrayCredentials, cfError) {
            case (_, .some(let cfError)):
                let underlyingError = NSError(domain: CFErrorGetDomain(cfError) as String,
                                              code: CFErrorGetCode(cfError),
                                              userInfo: (CFErrorCopyUserInfo(cfError) as? Dictionary))
                let error = ErrorBuilder.error(forCode: .sharedWebCredentialsFetchFailure, underlyingError: underlyingError)
                DispatchQueue.main.async {
                    completion(.failure(error))
                }

            case (.some(let cfArrayCredentials), _):
                if let credentials = cfArrayCredentials as? [[String: String]], credentials.count > 0,
                    let entry = credentials.first,
                    // let domain = entry[kSecAttrServer as String]
                    let username = entry[kSecAttrAccount as String],
                    let password = entry[kSecSharedPassword as String] {
                    DispatchQueue.main.async {
                        if username.isValidUsername() {
                            completion(.success(Credentials(username: username, password: password)))
                        }
                        else {
                            let error = ErrorBuilder.error(forCode: .sharedWebCredentialsFetchFailure, underlyingError: nil)
                            completion(.failure(error))
                        }
                    }
                }
                else {
                    DispatchQueue.main.async {
                        completion(.cancelled)
                    }
                }

            case (.none, .none):
                DispatchQueue.main.async {
                    completion(.cancelled)
                }
            }
        }
    }

    func saveCredentials(_ credentials: Credentials, completion: @escaping (SaveCredentialsResult) -> Void) {
        SecAddSharedWebCredential(domain as CFString, credentials.username as CFString, credentials.password as CFString) { cfError in
            switch cfError {
            case .some(let cfError):
                let underlyingError = NSError(domain: CFErrorGetDomain(cfError) as String,
                                              code: CFErrorGetCode(cfError),
                                              userInfo: (CFErrorCopyUserInfo(cfError) as? Dictionary))
                let error = ErrorBuilder.error(forCode: .sharedWebCredentialsSaveFailure, underlyingError: underlyingError)
                DispatchQueue.main.async {
                    completion(.failure(error))
                }
            case .none:
                DispatchQueue.main.async {
                    completion(.success)
                }
            }
        }
    }
}
extension String {

    fileprivate func isValidUsername() -> Bool {
        // https://stackoverflow.com/questions/38698565/secrequestsharedwebcredential-credentials-contains-passwords-not-saved
        // don't touch the 'Passwords not saved', the spaces are not what they seem (value copied from debugger)
        guard self != "Passwords not saved" else { return false }
        let containsAllInvalidWords = contains("Passwords") && contains("not") && contains("saved")
        return !containsAllInvalidWords
    }
}

Upvotes: 1

the_dude_abides
the_dude_abides

Reputation: 563

I'm checking for this in viewDidLoad() for my Auth view controller. The code is a bit different than above, gleaned from several other SO answers.

Swift 3:

SecRequestSharedWebCredential(Configuration.webBaseFQDN as CFString, nil, { (credentials, error) in

    if let error = error {
        print("ERROR: credentials")
        print(error)
    }

    guard let credentials = credentials, CFArrayGetCount(credentials) > 0 else {
        // Did not find a shared web credential.
        return
    }

    guard CFArrayGetCount(credentials) == 1 else {
        // There should be exactly one credential.
        return
    }

    let unsafeCredential = CFArrayGetValueAtIndex(credentials, 0)
    let credential = unsafeBitCast(unsafeCredential, to: CFDictionary.self)

    let unsafeEmail = CFDictionaryGetValue(credential, Unmanaged.passUnretained(kSecAttrAccount).toOpaque())
    let email = unsafeBitCast(unsafeEmail, to: CFString.self) as String

    let unsafePassword = CFDictionaryGetValue(credential, Unmanaged.passUnretained(kSecSharedPassword).toOpaque())
    let password = unsafeBitCast(unsafePassword, to: CFString.self) as String

    if self.isValidEmail(email) && self.isValidPassword(password) {
        self.usedSharedWebCredentials = true
        self.doSignIn(email: email, password: password)
    }
})

The extra check at the end for isValidEmail(_:) and isValidPassword(_:) handles the case where SecRequeestSharedWebCredential() returns "Passwords not saved" in the first credential (email).

Hopefully someone can explain why this is happening, but if not, at least there's a way to trap this scenario.

I'd also like to add that I've seen this up to iOS 10.2.1

Upvotes: 1

Related Questions