<commit_before>//
//  KeychainUtilities.swift
//  piwigoKit
//
//  Created by <NAME> on 09/06/2021.
//  Copyright © 2021 Piwigo.org. All rights reserved.
//

import Foundation

public
class KeychainUtilities : NSObject {
    
    // Access group
    private class
    func getAccessGroup() -> String {
        let teamID = Bundle.main.infoDictionary!["AppIdentifierPrefix"] as! String
        let bundleID = Bundle.main.bundleIdentifier!.components(separatedBy: ".")
        let pos = bundleID.firstIndex(of: "piwigo")
        let mainBundleID = bundleID[0...pos!].joined(separator: ".")
        return teamID + mainBundleID
    }
    
    // MARK: - Piwigo & HTTP Authentication
    /// - https://www.osstatus.com/search/results?platform=all&framework=all&search=0
    // Convention
    /// Piwigo credentials are identified in the Keychain with:
    /// - service: <host>
    /// - account: <username>
    /// HTTP credentials are stored in the Keychain with:
    /// - service: <scheme>:<host>
    /// - account: <httpUsername>
    public class
    func setPassword(_ password:String, forService service:String, account:String) {
        // Check input parameters
        guard service.isEmpty == false, account.isEmpty == false, password.isEmpty == false,
              let passwordData = password.data(using: .utf8) else { return }
        
        // Prepare query
        let searchQuery = [kSecClass as String                : kSecClassGenericPassword,
                           kSecAttrService as String          : service,
                           kSecAttrAccount as String          : account,
                           kSecAttrAccessGroup as String      : getAccessGroup(),
                           kSecAttrSynchronizable as String   : kSecAttrSynchronizableAny] as [String : Any]

        // Already in Keychain?
        var status = SecItemCopyMatching(searchQuery as CFDictionary, nil)
        switch status {
        case errSecSuccess:
            // Already existing —> update it
            let query = [kSecValueData as String            : passwordData,
                         kSecAttrAccessible as String       : kSecAttrAccessibleAfterFirstUnlock] as [String : Any]
            status = SecItemUpdate(searchQuery as CFDictionary, query as CFDictionary)

        case errSecItemNotFound:
            // Create new item in Keychain
            let query = [kSecClass as String                : kSecClassGenericPassword,
                         kSecAttrService as String          : service,
                         kSecAttrAccount as String          : account,
                         kSecAttrAccessGroup as String      : getAccessGroup(),
                         kSecAttrSynchronizable as String   : kSecAttrSynchronizableAny,
                         kSecValueData as String            : passwordData,
                         kSecAttrAccessible as String       : kSecAttrAccessibleAfterFirstUnlock] as [String : Any]
            status = SecItemAdd(query as CFDictionary, nil)
            
        default:
            // Log error
            logOSStatus(status)
        }
        
        if status != errSecSuccess { logOSStatus(status) }
        return
    }
    
    public class
    func password(forService service:String, account:String) -> String {
        // Check input parameters
        guard service.isEmpty == false, account.isEmpty == false else { return "" }

        // Prepare query
        let query = [kSecClass as String                : kSecClassGenericPassword,
                     kSecAttrService as String          : service,
                     kSecAttrAccount as String          : account,
                     kSecAttrAccessGroup as String      : getAccessGroup(),
                     kSecAttrSynchronizable as String   : kSecAttrSynchronizableAny,
                     kSecReturnData as String           : kCFBooleanTrue!,
                     kSecMatchLimit as String           : kSecMatchLimitOne] as [String: Any]

        // Apply the query
        var data: CFTypeRef?
        let status = SecItemCopyMatching(query as CFDictionary, &data)

        // Results?
        guard status == errSecSuccess else {
            if status == errSecItemNotFound {
                // Security item not found -> try old method
                /// Piwigo credentials are identified in the Keychain with:
                /// - generic: "PiwigoLogin"
                /// - attribute: <username>
                let kKeychainAppID = "PiwigoLogin"
                let query = [kSecClass as String            : kSecClassGenericPassword,
                             kSecAttrGeneric as String      : kKeychainAppID,
                             kSecReturnAttributes as String : kCFBooleanTrue!,
                             kSecMatchLimit as String       : kSecMatchLimitOne] as [String: Any]

                var data: CFTypeRef?
                let status = SecItemCopyMatching(query as CFDictionary, &data)
                guard status == errSecSuccess else {
                    logOSStatus(status)
                    return ""
                }
                
                // Did found username
                guard let username = String(data: data as! Data, encoding: .utf8),
                      username.isEmpty == false else { return "" }
                if username == NetworkVars.username {
                    // Retrieve password
                    let query = [kSecClass as String            : kSecClassGenericPassword,
                                 kSecAttrGeneric as String      : kKeychainAppID,
                                 kSecAttrAccount as String      : username,
                                 kSecAttrAccessGroup as String  : getAccessGroup(),
                                 kSecMatchLimit as String       : kSecMatchLimitOne] as [String: Any]

                    var data: CFTypeRef?
                    let status = SecItemCopyMatching(query as CFDictionary, &data)
                    guard status == errSecSuccess else {
                        logOSStatus(status)
                        return ""
                    }
                    guard let password = String(data: data as! Data, encoding: .utf8),
                          password.isEmpty == false else { return "" }
                    return password
                }
            }
            logOSStatus(status)
            return ""
        }
        guard let password = String(data: data as! Data, encoding: .utf8),
              password.isEmpty == false else {
            logOSStatus(status)
            return ""
        }
        return password
    }
    
    public class
    func deletePassword(forService service:String, account:String) {
        // Check input parameters
        guard service.isEmpty == false, account.isEmpty == false else { return }

        // Prepare query
        let query = [kSecClass as String                : kSecClassGenericPassword,
                     kSecAttrService as String          : service,
                     kSecAttrAccount as String          : account,
                     kSecAttrAccessGroup as String      : getAccessGroup(),
                     kSecAttrSynchronizable as String   : kSecAttrSynchronizableAny] as [String: Any]

        // Apply the query
        let status = SecItemDelete(query as CFDictionary)
        if status != errSecSuccess { logOSStatus(status) }
        return
    }
    
    public class
    func logOSStatus(_ status:OSStatus) {
        #if DEBUG
        if #available(iOSApplicationExtension 11.3, *) {
            let msg = SecCopyErrorMessageString(status, nil)
            debugPrint("••> OSStatus Error #\(status): \(msg as String?)")
        } else {
            let url = "https://www.osstatus.com/search/results?platform=all&framework=all&search=\(status)"
            debugPrint("••> OSStatus Error #\(status): \(url)")
        }<commit_msg>Replaces debugPrint with print<commit_after>//
//  KeychainUtilities.swift
//  piwigoKit
//
//  Created by <NAME> on 09/06/2021.
//  Copyright © 2021 Piwigo.org. All rights reserved.
//

import Foundation

public
class KeychainUtilities : NSObject {
    
    // Access group
    private class
    func getAccessGroup() -> String {
        let teamID = Bundle.main.infoDictionary!["AppIdentifierPrefix"] as! String
        let bundleID = Bundle.main.bundleIdentifier!.components(separatedBy: ".")
        let pos = bundleID.firstIndex(of: "piwigo")
        let mainBundleID = bundleID[0...pos!].joined(separator: ".")
        return teamID + mainBundleID
    }
    
    // MARK: - Piwigo & HTTP Authentication
    /// - https://www.osstatus.com/search/results?platform=all&framework=all&search=0
    // Convention
    /// Piwigo credentials are identified in the Keychain with:
    /// - service: <host>
    /// - account: <username>
    /// HTTP credentials are stored in the Keychain with:
    /// - service: <scheme>:<host>
    /// - account: <httpUsername>
    public class
    func setPassword(_ password:String, forService service:String, account:String) {
        // Check input parameters
        guard service.isEmpty == false, account.isEmpty == false, password.isEmpty == false,
              let passwordData = password.data(using: .utf8) else { return }
        
        // Prepare query
        let searchQuery = [kSecClass as String                : kSecClassGenericPassword,
                           kSecAttrService as String          : service,
                           kSecAttrAccount as String          : account,
                           kSecAttrAccessGroup as String      : getAccessGroup(),
                           kSecAttrSynchronizable as String   : kSecAttrSynchronizableAny] as [String : Any]

        // Already in Keychain?
        var status = SecItemCopyMatching(searchQuery as CFDictionary, nil)
        switch status {
        case errSecSuccess:
            // Already existing —> update it
            let query = [kSecValueData as String            : passwordData,
                         kSecAttrAccessible as String       : kSecAttrAccessibleAfterFirstUnlock] as [String : Any]
            status = SecItemUpdate(searchQuery as CFDictionary, query as CFDictionary)

        case errSecItemNotFound:
            // Create new item in Keychain
            let query = [kSecClass as String                : kSecClassGenericPassword,
                         kSecAttrService as String          : service,
                         kSecAttrAccount as String          : account,
                         kSecAttrAccessGroup as String      : getAccessGroup(),
                         kSecAttrSynchronizable as String   : kSecAttrSynchronizableAny,
                         kSecValueData as String            : passwordData,
                         kSecAttrAccessible as String       : kSecAttrAccessibleAfterFirstUnlock] as [String : Any]
            status = SecItemAdd(query as CFDictionary, nil)
            
        default:
            // Log error
            logOSStatus(status)
        }
        
        if status != errSecSuccess { logOSStatus(status) }
        return
    }
    
    public class
    func password(forService service:String, account:String) -> String {
        // Check input parameters
        guard service.isEmpty == false, account.isEmpty == false else { return "" }

        // Prepare query
        let query = [kSecClass as String                : kSecClassGenericPassword,
                     kSecAttrService as String          : service,
                     kSecAttrAccount as String          : account,
                     kSecAttrAccessGroup as String      : getAccessGroup(),
                     kSecAttrSynchronizable as String   : kSecAttrSynchronizableAny,
                     kSecReturnData as String           : kCFBooleanTrue!,
                     kSecMatchLimit as String           : kSecMatchLimitOne] as [String: Any]

        // Apply the query
        var data: CFTypeRef?
        let status = SecItemCopyMatching(query as CFDictionary, &data)

        // Results?
        guard status == errSecSuccess else {
            if status == errSecItemNotFound {
                // Security item not found -> try old method
                /// Piwigo credentials are identified in the Keychain with:
                /// - generic: "PiwigoLogin"
                /// - attribute: <username>
                let kKeychainAppID = "PiwigoLogin"
                let query = [kSecClass as String            : kSecClassGenericPassword,
                             kSecAttrGeneric as String      : kKeychainAppID,
                             kSecReturnAttributes as String : kCFBooleanTrue!,
                             kSecMatchLimit as String       : kSecMatchLimitOne] as [String: Any]

                var data: CFTypeRef?
                let status = SecItemCopyMatching(query as CFDictionary, &data)
                guard status == errSecSuccess else {
                    logOSStatus(status)
                    return ""
                }
                
                // Did found username
                guard let username = String(data: data as! Data, encoding: .utf8),
                      username.isEmpty == false else { return "" }
                if username == NetworkVars.username {
                    // Retrieve password
                    let query = [kSecClass as String            : kSecClassGenericPassword,
                                 kSecAttrGeneric as String      : kKeychainAppID,
                                 kSecAttrAccount as String      : username,
                                 kSecAttrAccessGroup as String  : getAccessGroup(),
                                 kSecMatchLimit as String       : kSecMatchLimitOne] as [String: Any]

                    var data: CFTypeRef?
                    let status = SecItemCopyMatching(query as CFDictionary, &data)
                    guard status == errSecSuccess else {
                        logOSStatus(status)
                        return ""
                    }
                    guard let password = String(data: data as! Data, encoding: .utf8),
                          password.isEmpty == false else { return "" }
                    return password
                }
            }
            logOSStatus(status)
            return ""
        }
        guard let password = String(data: data as! Data, encoding: .utf8),
              password.isEmpty == false else {
            logOSStatus(status)
            return ""
        }
        return password
    }
    
    public class
    func deletePassword(forService service:String, account:String) {
        // Check input parameters
        guard service.isEmpty == false, account.isEmpty == false else { return }

        // Prepare query
        let query = [kSecClass as String                : kSecClassGenericPassword,
                     kSecAttrService as String          : service,
                     kSecAttrAccount as String          : account,
                     kSecAttrAccessGroup as String      : getAccessGroup(),
                     kSecAttrSynchronizable as String   : kSecAttrSynchronizableAny] as [String: Any]

        // Apply the query
        let status = SecItemDelete(query as CFDictionary)
        if status != errSecSuccess { logOSStatus(status) }
        return
    }
    
    public class
    func logOSStatus(_ status:OSStatus) {
        #if DEBUG
        if #available(iOSApplicationExtension 11.3, *) {
            let msg = SecCopyErrorMessageString(status, nil)
            print("••> OSStatus Error #\(status): \(msg as String?)")
        } else {
            let url = "https://www.osstatus.com/search/results?platform=all&framework=all&search=\(status)"
            print("••> OSStatus Error #\(status): \(url)")
        }
{'fraction_non_alphanumeric': 0.0746127094530509, 'fraction_numerical': 0.002655706607650964, 'mean_word_length': 1.53056, 'pattern_counts': {'":': 0, '<': 17, '<?xml version=': 0, '>': 29, 'https://': 4, 'lorem ipsum': 0, 'www.': 4, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 8, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4880070', 'n_tokens_mistral': 3823, 'n_tokens_neox': 3662, 'n_words': 1061}