philips-software/cogito

View on GitHub
workspaces/cogito-ios-app/Cogito/Key store/AppPassword.swift

Summary

Maintainability
A
0 mins
Test Coverage
D
66%
import KeychainAccess
import Security

private let appPasswordKey = "appPassword"

class AppPassword {
    let keychain: KeychainType
    let passwordLength: Int

    init(keychain: KeychainType = Keychain(), passwordLength: Int) {
        self.keychain = keychain
        self.passwordLength = passwordLength
    }

    func use(_ withPassword: @escaping PasswordCallback) {
        let callback = { (password, error) in
            DispatchQueue.main.async {
                withPassword(password, error)
            }
        }

        DispatchQueue.global().async { [weak self] in
            guard let this = self else {
                callback(nil, nil)
                return
            }

            if let password = this.loadPassword() {
                callback(password, nil)
            } else {
                this.generatePassword(onComplete: callback)
            }
        }
    }

    private func generatePassword(onComplete callback: PasswordCallback) {
        var password: String?
        do {
            password = try keychain.generatePassword(lengthInBytes: passwordLength)
        } catch let error {
            callback(nil, error.localizedDescription)
            return
        }
        if let error = store(password: password!) {
            callback(nil, error)
            return
        }
        callback(password!, nil)
    }

    private func loadPassword() -> String? {
        do {
            let password = try keychain
                .withAuthenticationPrompt("Authenticate to unlock your secure account data")
                .get(appPasswordKey)
            return password
        } catch {
            return nil
        }
    }

    private func store(password: String) -> String? {
        do {
            try keychain
                .withAccessibility(.whenUnlocked, authenticationPolicy: .userPresence)
                .withAuthenticationPrompt("Authenticate to securely store account data")
                .set(password, key: appPasswordKey)
            return nil
        } catch let error {
            return error.localizedDescription
        }
    }

    func reset() throws {
        try keychain.remove(appPasswordKey)
    }

    typealias PasswordCallback = (_ password: String?, _ error: String?) -> Void
}

protocol KeychainType {
    func withAuthenticationPrompt(_ authenticationPrompt: String) -> KeychainType
    func withAccessibility(_ accessibility: Accessibility,
                           authenticationPolicy: AuthenticationPolicy) -> KeychainType
    func get(_ key: String) throws -> String?
    func set(_ value: String, key: String) throws
    func generatePassword(lengthInBytes: Int) throws -> String
    func remove(_ key: String) throws
}

extension Keychain: KeychainType {
    func withAuthenticationPrompt(_ authenticationPrompt: String) -> KeychainType {
        return self.authenticationPrompt(authenticationPrompt)
    }

    func withAccessibility(_ accessibility: Accessibility,
                           authenticationPolicy: AuthenticationPolicy) -> KeychainType {
        return self.accessibility(accessibility,
                                  authenticationPolicy: authenticationPolicy)
    }

    func generatePassword(lengthInBytes: Int) throws -> String {
        var bytes = [UInt8](repeating: 0, count: lengthInBytes)
        let result = SecRandomCopyBytes(kSecRandomDefault, lengthInBytes, &bytes)
        guard result == errSecSuccess else {
            throw GeneratePasswordError(resultCode: result)
        }
        return Data(bytes: bytes).hexEncodedString()
    }
}

extension Data {
    func hexEncodedString() -> String {
        return map { String(format: "%02hhx", $0) }.joined()
    }
}

struct GeneratePasswordError: Error {
    let resultCode: Int32
}