<commit_before>//
//  Password.swift
//  pass
//
//  Created by <NAME> on 2/2/2017.
//  Copyright © 2017 <NAME>. All rights reserved.
//

import Foundation
import SwiftyUserDefaults
import OneTimePassword
import Base32

struct AdditionField {
    var title: String
    var content: String
}

enum PasswordChange: Int {
    case path = 0x01
    case content = 0x02
    case none = 0x00
}

public class Password {
    public static let otpKeywords = ["otp_secret", "otp_type", "otp_algorithm", "otp_period", "otp_digits", "otp_counter", "otpauth"]

    public var name = ""
    public var url: URL?
    public var namePath: String {
        get {
            if url == nil {
                return ""
            }
            return url!.deletingPathExtension().path
        }
    }
    public var password = ""
    public var changed: Int = 0
    public var plainText = ""
    
    private var additions = [String: String]()
    private var firstLineIsOTPField = false
    private var otpToken: Token?
    
    public enum OtpType {
        case totp, hotp, none
    }
    
    public var otpType: OtpType {
        get {
            guard let token = self.otpToken else {
                return OtpType.none
            }
            switch token.generator.factor {
            case .counter:
                return OtpType.hotp
            case .timer:
                return OtpType.totp
            }
        }
    }
    
    public init(name: String, url: URL?, plainText: String) {
        self.initEverything(name: name, url: url, plainText: plainText)
    }
    
    public func updatePassword(name: String, url: URL?, plainText: String) {
        if self.plainText != plainText || self.url != url {
            if self.plainText != plainText {
                changed = changed|PasswordChange.content.rawValue
            }
            if self.url != url {
                changed = changed|PasswordChange.path.rawValue
            }
            self.initEverything(name: name, url: url, plainText: plainText)
        }
    }
    
    private func initEverything(name: String, url: URL?, plainText: String) {
        self.name = name
        self.url = url
        self.plainText = plainText
        additions.removeAll()
        
        // split the plain text
        let plainTextSplit = plainText.characters.split(maxSplits: 1, omittingEmptySubsequences: false) {
            $0 == "\n" || $0 == "\r\n"
            }.map(String.init)
    
        // get password
        password  = plainTextSplit.first ?? ""
        
        // get additonal fields
        if plainTextSplit.count == 2 {
            var unknownIndex = 0
            plainTextSplit[1].enumerateLines() { line, _ in
                if !line.isEmpty {
                    var (key, value) = Password.getKeyValuePair(from: line)
                    if key == nil {
                        unknownIndex += 1
                        key = "unknown \(unknownIndex)"
                    }
                    self.additions[key!] = value
                }
            }
        }
        
        // check whether the first line of the plainText looks like an otp entry
        let (key, value) = Password.getKeyValuePair(from: self.password)
        if Password.otpKeywords.contains(key ?? "") {
            firstLineIsOTPField = true
            self.additions[key!] = value
        } else {
            firstLineIsOTPField = false
        }
        
        // construct the otp token
        self.updateOtpToken()
    }
    
    public func getFilteredAdditions() -> [String: String] {
        var filteredAdditions = [String: String]()
        additions.forEach { (key: String, value: String) in
            if key.lowercased() != "username" && key.lowercased() != "login" && key.lowercased() != "password" &&
                (!key.hasPrefix("unknown") || !SharedDefaults[.isHideUnknownOn]) &&
                (!Password.otpKeywords.contains(key) || !SharedDefaults[.isHideOTPOn]) {
                filteredAdditions[key] = value
            }
        }
        return filteredAdditions
    }
    
    public func getUsername() -> String? {
        return getAdditionValue(withKey: "username", caseSensitive: false)
    }
    
    public func getLogin() -> String? {
        return getAdditionValue(withKey: "login", caseSensitive: false)
    }
    
    public func getURLString() -> String? {
        return getAdditionValue(withKey: "url", caseSensitive: false)
    }
    
    // return a key-value pair from the line
    // key might be nil, if there is no ":" in the line
    private static func getKeyValuePair(from line: String) -> (key: String?, value: String) {
        let items = line.components(separatedBy: ": ").map{String($0).trimmingCharacters(in: .whitespaces)}
        var key : String? = nil
        var value = ""
        if items.count == 1 || (items[0].isEmpty && items[1].isEmpty) {
            // no ": " found, or empty on both sides of ": "
            value = line
            // otpauth special case
            if value.hasPrefix("otpauth://") {
                key = "otpauth"
            }
        } else {
            if !items[0].isEmpty {
                key = items[0]
            }
            value = items[1]
        }
        return (key, value)
    }
    
    public func getAdditionsPlainText() -> String {
        // lines starting from the second
        let plainTextSplit = plainText.characters.split(maxSplits: 1, omittingEmptySubsequences: false) {
            $0 == "\n" || $0 == "\r\n"
            }.map(String.init)
        if plainTextSplit.count == 1 {
            return ""
        } else {
            return plainTextSplit[1]
        }
    }
    
    private func getPlainText() -> String {
        return self.plainText
    }
    
    public func getPlainData() -> Data {
        return getPlainText().data(using: .utf8)!
    }
    
    private func getAdditionValue(withKey key: String, caseSensitive: Bool = true) -> String? {
        if caseSensitive {
            return additions[key]
        } else {
            let searchKey = key.lowercased()
            for k in additions.keys {
                if searchKey == k.lowercased() {
                    return additions[k]
                }
            }
            return nil
        }
    }
    
    /*
     Set otpType and otpToken, if we are able to construct a valid token.
     
     Example of TOTP otpauth
     (Key Uri Format: https://github.com/google/google-authenticator/wiki/Key-Uri-Format)
     otpauth://totp/totp-secret?secret=AAAAAAAAAAAAAAAA&issuer=totp-secret
     
     Example of TOTP fields [Legacy, lower priority]
     otp_secret: secretsecretsecretsecretsecretsecret
     otp_type: totp
     otp_algorithm: sha1 (default: sha1, optional)
     otp_period: 30 (default: 30, optional)
     otp_digits: 6 (default: 6, optional)
     
     Example of HOTP fields [Legacy, lower priority]
     otp_secret: secretsecretsecretsecretsecretsecret
     otp_type: hotp
     otp_counter: 1
     otp_digits: 6 (default: 6, optional)
     
     */
    private func updateOtpToken() {
        self.otpToken = nil
        
        // get otpauth, if we are able to generate a token, return
        if var otpauthString = getAdditionValue(withKey: "otpauth") {
            if !otpauthString.hasPrefix("otpauth:") {<commit_msg>Fix issue #13 ( Additions display randomly)<commit_after>//
//  Password.swift
//  pass
//
//  Created by <NAME> on 2/2/2017.
//  Copyright © 2017 <NAME>. All rights reserved.
//

import Foundation
import SwiftyUserDefaults
import OneTimePassword
import Base32

struct AdditionField {
    var title: String
    var content: String
}

enum PasswordChange: Int {
    case path = 0x01
    case content = 0x02
    case none = 0x00
}

public class Password {
    public static let otpKeywords = ["otp_secret", "otp_type", "otp_algorithm", "otp_period", "otp_digits", "otp_counter", "otpauth"]

    public var name = ""
    public var url: URL?
    public var namePath: String {
        get {
            if url == nil {
                return ""
            }
            return url!.deletingPathExtension().path
        }
    }
    public var password = ""
    public var changed: Int = 0
    public var plainText = ""
    
    private var additions = [(String, String)]()
    private var firstLineIsOTPField = false
    private var otpToken: Token?
    
    public enum OtpType {
        case totp, hotp, none
    }
    
    public var otpType: OtpType {
        get {
            guard let token = self.otpToken else {
                return OtpType.none
            }
            switch token.generator.factor {
            case .counter:
                return OtpType.hotp
            case .timer:
                return OtpType.totp
            }
        }
    }
    
    public init(name: String, url: URL?, plainText: String) {
        self.initEverything(name: name, url: url, plainText: plainText)
    }
    
    public func updatePassword(name: String, url: URL?, plainText: String) {
        if self.plainText != plainText || self.url != url {
            if self.plainText != plainText {
                changed = changed|PasswordChange.content.rawValue
            }
            if self.url != url {
                changed = changed|PasswordChange.path.rawValue
            }
            self.initEverything(name: name, url: url, plainText: plainText)
        }
    }
    
    private func initEverything(name: String, url: URL?, plainText: String) {
        self.name = name
        self.url = url
        self.plainText = plainText
        additions.removeAll()
        
        // split the plain text
        let plainTextSplit = plainText.characters.split(maxSplits: 1, omittingEmptySubsequences: false) {
            $0 == "\n" || $0 == "\r\n"
            }.map(String.init)
    
        // get password
        password  = plainTextSplit.first ?? ""
        
        // get additonal fields
        if plainTextSplit.count == 2 {
            var unknownIndex = 0
            plainTextSplit[1].enumerateLines() { line, _ in
                if !line.isEmpty {
                    var (key, value) = Password.getKeyValuePair(from: line)
                    if key == nil {
                        unknownIndex += 1
                        key = "unknown \(unknownIndex)"
                    }
                    self.additions.append((key!, value))
                }
            }
        }
        
        // check whether the first line of the plainText looks like an otp entry
        let (key, value) = Password.getKeyValuePair(from: self.password)
        if Password.otpKeywords.contains(key ?? "") {
            firstLineIsOTPField = true
            self.additions.append((key!, value))
        } else {
            firstLineIsOTPField = false
        }
        
        // construct the otp token
        self.updateOtpToken()
    }
    
    public func getFilteredAdditions() -> [(String, String)] {
        var filteredAdditions = [(String, String)]()
        additions.forEach { (key: String, value: String) in
            if key.lowercased() != "username" && key.lowercased() != "login" && key.lowercased() != "password" &&
                (!key.hasPrefix("unknown") || !SharedDefaults[.isHideUnknownOn]) &&
                (!Password.otpKeywords.contains(key) || !SharedDefaults[.isHideOTPOn]) {
                filteredAdditions.append((key, value))
            }
        }
        return filteredAdditions
    }
    
    public func getUsername() -> String? {
        return getAdditionValue(withKey: "username", caseSensitive: false)
    }
    
    public func getLogin() -> String? {
        return getAdditionValue(withKey: "login", caseSensitive: false)
    }
    
    public func getURLString() -> String? {
        return getAdditionValue(withKey: "url", caseSensitive: false)
    }
    
    // return a key-value pair from the line
    // key might be nil, if there is no ":" in the line
    private static func getKeyValuePair(from line: String) -> (key: String?, value: String) {
        let items = line.components(separatedBy: ": ").map{String($0).trimmingCharacters(in: .whitespaces)}
        var key : String? = nil
        var value = ""
        if items.count == 1 || (items[0].isEmpty && items[1].isEmpty) {
            // no ": " found, or empty on both sides of ": "
            value = line
            // otpauth special case
            if value.hasPrefix("otpauth://") {
                key = "otpauth"
            }
        } else {
            if !items[0].isEmpty {
                key = items[0]
            }
            value = items[1]
        }
        return (key, value)
    }
    
    public func getAdditionsPlainText() -> String {
        // lines starting from the second
        let plainTextSplit = plainText.characters.split(maxSplits: 1, omittingEmptySubsequences: false) {
            $0 == "\n" || $0 == "\r\n"
            }.map(String.init)
        if plainTextSplit.count == 1 {
            return ""
        } else {
            return plainTextSplit[1]
        }
    }
    
    private func getPlainText() -> String {
        return self.plainText
    }
    
    public func getPlainData() -> Data {
        return getPlainText().data(using: .utf8)!
    }
    
    private func getAdditionValue(withKey key: String, caseSensitive: Bool = true) -> String? {
        let searchKey = caseSensitive ? key : key.lowercased()
        for (currentKey, value) in additions {
            let currentKeyTrans = caseSensitive ? currentKey : currentKey.lowercased()
            if searchKey == currentKeyTrans {
                return value
            }
        }
        return nil
    }
    
    /*
     Set otpType and otpToken, if we are able to construct a valid token.
     
     Example of TOTP otpauth
     (Key Uri Format: https://github.com/google/google-authenticator/wiki/Key-Uri-Format)
     otpauth://totp/totp-secret?secret=AAAAAAAAAAAAAAAA&issuer=totp-secret
     
     Example of TOTP fields [Legacy, lower priority]
     otp_secret: secretsecretsecretsecretsecretsecret
     otp_type: totp
     otp_algorithm: sha1 (default: sha1, optional)
     otp_period: 30 (default: 30, optional)
     otp_digits: 6 (default: 6, optional)
     
     Example of HOTP fields [Legacy, lower priority]
     otp_secret: secretsecretsecretsecretsecretsecret
     otp_type: hotp
     otp_counter: 1
     otp_digits: 6 (default: 6, optional)
     
     */
    private func updateOtpToken() {
        self.otpToken = nil
        
        // get otpauth, if we are able to generate a token, return
        if var otpauthString = getAdditionValue(withKey: "otpauth") {
            if !otpauthString.hasPrefix("otpauth:") {
{'fraction_non_alphanumeric': 0.10244731475186948, 'fraction_numerical': 0.00734194425560843, 'mean_word_length': 1.7893439514599925, 'pattern_counts': {'":': 8, '<': 7, '<?xml version=': 0, '>': 25, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 2, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '900238', 'n_tokens_mistral': 4304, 'n_tokens_neox': 4122, 'n_words': 1108}