mirror of
				https://github.com/maxgoedjen/secretive.git
				synced 2025-11-04 01:10:56 +00:00 
			
		
		
		
	Merge branch 'main' into patch-1
This commit is contained in:
		
						commit
						de4da44524
					
				@ -32,3 +32,9 @@ SecretKit is a collection of protocols describing secrets and stores.
 | 
			
		||||
### Authentication Persistence
 | 
			
		||||
 | 
			
		||||
- ``PersistedAuthenticationContext``
 | 
			
		||||
 | 
			
		||||
### Errors
 | 
			
		||||
 | 
			
		||||
- ``KeychainError``
 | 
			
		||||
- ``SigningError``
 | 
			
		||||
- ``SecurityError``
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,7 @@ public class AnySecretStore: SecretStore {
 | 
			
		||||
    private let _name: () -> String
 | 
			
		||||
    private let _secrets: () -> [AnySecret]
 | 
			
		||||
    private let _sign: (Data, AnySecret, SigningRequestProvenance) throws -> Data
 | 
			
		||||
    private let _verify: (Data, Data, AnySecret) throws -> Bool
 | 
			
		||||
    private let _existingPersistedAuthenticationContext: (AnySecret) -> PersistedAuthenticationContext?
 | 
			
		||||
    private let _persistAuthentication: (AnySecret, TimeInterval) throws -> Void
 | 
			
		||||
    private let _reloadSecrets: () -> Void
 | 
			
		||||
@ -23,6 +24,7 @@ public class AnySecretStore: SecretStore {
 | 
			
		||||
        _id = { secretStore.id }
 | 
			
		||||
        _secrets = { secretStore.secrets.map { AnySecret($0) } }
 | 
			
		||||
        _sign = { try secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType, for: $2) }
 | 
			
		||||
        _verify = { try secretStore.verify(signature: $0, for: $1, with: $2.base as! SecretStoreType.SecretType) }
 | 
			
		||||
        _existingPersistedAuthenticationContext = { secretStore.existingPersistedAuthenticationContext(secret: $0.base as! SecretStoreType.SecretType) }
 | 
			
		||||
        _persistAuthentication = { try secretStore.persistAuthentication(secret: $0.base as! SecretStoreType.SecretType, forDuration: $1) }
 | 
			
		||||
        _reloadSecrets = { secretStore.reloadSecrets() }
 | 
			
		||||
@ -51,6 +53,10 @@ public class AnySecretStore: SecretStore {
 | 
			
		||||
        try _sign(data, secret, provenance)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public func verify(signature: Data, for data: Data, with secret: AnySecret) throws -> Bool {
 | 
			
		||||
        try _verify(signature, data, secret)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public func existingPersistedAuthenticationContext(secret: AnySecret) -> PersistedAuthenticationContext? {
 | 
			
		||||
        _existingPersistedAuthenticationContext(secret)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1,5 +0,0 @@
 | 
			
		||||
import Foundation
 | 
			
		||||
 | 
			
		||||
public func KeychainDictionary(_ dictionary: [CFString: Any]) -> CFDictionary {
 | 
			
		||||
    dictionary as CFDictionary
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										71
									
								
								Sources/Packages/Sources/SecretKit/KeychainTypes.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								Sources/Packages/Sources/SecretKit/KeychainTypes.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,71 @@
 | 
			
		||||
import Foundation
 | 
			
		||||
 | 
			
		||||
public typealias SecurityError = Unmanaged<CFError>
 | 
			
		||||
 | 
			
		||||
/// Wraps a Swift dictionary in a CFDictionary.
 | 
			
		||||
/// - Parameter dictionary: The Swift dictionary to wrap.
 | 
			
		||||
/// - Returns: A CFDictionary containing the keys and values.
 | 
			
		||||
public func KeychainDictionary(_ dictionary: [CFString: Any]) -> CFDictionary {
 | 
			
		||||
    dictionary as CFDictionary
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public extension CFError {
 | 
			
		||||
 | 
			
		||||
    /// The CFError returned when a verification operation fails.
 | 
			
		||||
    static let verifyError = CFErrorCreate(nil, NSOSStatusErrorDomain as CFErrorDomain, CFIndex(errSecVerifyFailed), nil)!
 | 
			
		||||
 | 
			
		||||
    /// Equality operation that only considers domain and code.
 | 
			
		||||
    static func ~=(lhs: CFError, rhs: CFError) -> Bool {
 | 
			
		||||
        CFErrorGetDomain(lhs) == CFErrorGetDomain(rhs) && CFErrorGetCode(lhs) == CFErrorGetCode(rhs)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// A wrapper around an error code reported by a Keychain API.
 | 
			
		||||
public struct KeychainError: Error {
 | 
			
		||||
    /// The status code involved, if one was reported.
 | 
			
		||||
    public let statusCode: OSStatus?
 | 
			
		||||
 | 
			
		||||
    /// Initializes a KeychainError with an optional error code.
 | 
			
		||||
    /// - Parameter statusCode: The status code returned by the keychain operation, if one is applicable.
 | 
			
		||||
    public init(statusCode: OSStatus?) {
 | 
			
		||||
        self.statusCode = statusCode
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// A signing-related error.
 | 
			
		||||
public struct SigningError: Error {
 | 
			
		||||
    /// The underlying error reported by the API, if one was returned.
 | 
			
		||||
    public let error: SecurityError?
 | 
			
		||||
 | 
			
		||||
    /// Initializes a SigningError with an optional SecurityError.
 | 
			
		||||
    /// - Parameter statusCode: The SecurityError, if one is applicable.
 | 
			
		||||
    public init(error: SecurityError?) {
 | 
			
		||||
        self.error = error
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public extension SecretStore {
 | 
			
		||||
 | 
			
		||||
    /// Returns the appropriate keychian signature algorithm to use for a given secret.
 | 
			
		||||
    /// - Parameters:
 | 
			
		||||
    ///   - secret: The secret which will be used for signing.
 | 
			
		||||
    ///   - allowRSA: Whether or not RSA key types should be permited.
 | 
			
		||||
    /// - Returns: The appropriate algorithm.
 | 
			
		||||
    func signatureAlgorithm(for secret: SecretType, allowRSA: Bool = false) -> SecKeyAlgorithm {
 | 
			
		||||
        switch (secret.algorithm, secret.keySize) {
 | 
			
		||||
        case (.ellipticCurve, 256):
 | 
			
		||||
            return .ecdsaSignatureMessageX962SHA256
 | 
			
		||||
        case (.ellipticCurve, 384):
 | 
			
		||||
            return .ecdsaSignatureMessageX962SHA384
 | 
			
		||||
        case (.rsa, 1024), (.rsa, 2048):
 | 
			
		||||
            guard allowRSA else { fatalError() }
 | 
			
		||||
            return .rsaSignatureMessagePKCS1v15SHA512
 | 
			
		||||
        default:
 | 
			
		||||
            fatalError()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -64,6 +64,10 @@ extension OpenSSHKeyWriter {
 | 
			
		||||
        switch algorithm {
 | 
			
		||||
        case .ellipticCurve:
 | 
			
		||||
            return "ecdsa-sha2-nistp" + String(describing: length)
 | 
			
		||||
        case .rsa:
 | 
			
		||||
            // All RSA keys use the same 512 bit hash function, per
 | 
			
		||||
            // https://security.stackexchange.com/questions/255074/why-are-rsa-sha2-512-and-rsa-sha2-256-supported-but-not-reported-by-ssh-q-key
 | 
			
		||||
            return "rsa-sha2-512"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -76,6 +80,9 @@ extension OpenSSHKeyWriter {
 | 
			
		||||
        switch algorithm {
 | 
			
		||||
        case .ellipticCurve:
 | 
			
		||||
            return "nistp" + String(describing: length)
 | 
			
		||||
        case .rsa:
 | 
			
		||||
            // All RSA keys use the same 512 bit hash function
 | 
			
		||||
            return "rsa-sha2-512"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,7 @@ public protocol Secret: Identifiable, Hashable {
 | 
			
		||||
public enum Algorithm: Hashable {
 | 
			
		||||
 | 
			
		||||
    case ellipticCurve
 | 
			
		||||
    case rsa
 | 
			
		||||
 | 
			
		||||
    /// Initializes the Algorithm with a secAttr representation of an algorithm.
 | 
			
		||||
    /// - Parameter secAttr: the secAttr, represented as an NSNumber.
 | 
			
		||||
@ -28,8 +29,19 @@ public enum Algorithm: Hashable {
 | 
			
		||||
        switch secAttrString {
 | 
			
		||||
        case kSecAttrKeyTypeEC:
 | 
			
		||||
            self = .ellipticCurve
 | 
			
		||||
        case kSecAttrKeyTypeRSA:
 | 
			
		||||
            self = .rsa
 | 
			
		||||
        default:
 | 
			
		||||
            fatalError()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public var secAttrKeyType: CFString {
 | 
			
		||||
        switch self {
 | 
			
		||||
        case .ellipticCurve:
 | 
			
		||||
            return kSecAttrKeyTypeEC
 | 
			
		||||
        case .rsa:
 | 
			
		||||
            return kSecAttrKeyTypeRSA
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -23,6 +23,14 @@ public protocol SecretStore: ObservableObject, Identifiable {
 | 
			
		||||
    /// - Returns: The signed data.
 | 
			
		||||
    func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data
 | 
			
		||||
 | 
			
		||||
    /// Verifies that a signature is valid over a specified payload.
 | 
			
		||||
    /// - Parameters:
 | 
			
		||||
    ///   - signature: The signature over the data.
 | 
			
		||||
    ///   - data: The data to verify the signature of.
 | 
			
		||||
    ///   - secret: The secret whose signature to verify.
 | 
			
		||||
    /// - Returns: Whether the signature was verified.
 | 
			
		||||
    func verify(signature: Data, for data: Data, with secret: SecretType) throws -> Bool
 | 
			
		||||
 | 
			
		||||
    /// Checks to see if there is currently a valid persisted authentication for a given secret.
 | 
			
		||||
    /// - Parameters:
 | 
			
		||||
    ///   - secret: The ``Secret`` to check if there is a persisted authentication for.
 | 
			
		||||
 | 
			
		||||
@ -101,7 +101,7 @@ extension SecureEnclave {
 | 
			
		||||
            reloadSecretsInternal()
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data {
 | 
			
		||||
        public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) throws -> Data {
 | 
			
		||||
            let context: LAContext
 | 
			
		||||
            if let existing = persistedAuthenticationContexts[secret], existing.valid {
 | 
			
		||||
                context = existing.context
 | 
			
		||||
@ -138,6 +138,41 @@ extension SecureEnclave {
 | 
			
		||||
            return signature as Data
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public func verify(signature: Data, for data: Data, with secret: Secret) throws -> Bool {
 | 
			
		||||
            let context = LAContext()
 | 
			
		||||
            context.localizedReason = "verify a signature using secret \"\(secret.name)\""
 | 
			
		||||
            context.localizedCancelTitle = "Deny"
 | 
			
		||||
            let attributes = KeychainDictionary([
 | 
			
		||||
                kSecClass: kSecClassKey,
 | 
			
		||||
                kSecAttrKeyClass: kSecAttrKeyClassPrivate,
 | 
			
		||||
                kSecAttrApplicationLabel: secret.id as CFData,
 | 
			
		||||
                kSecAttrKeyType: Constants.keyType,
 | 
			
		||||
                kSecAttrTokenID: kSecAttrTokenIDSecureEnclave,
 | 
			
		||||
                kSecAttrApplicationTag: Constants.keyTag,
 | 
			
		||||
                kSecUseAuthenticationContext: context,
 | 
			
		||||
                kSecReturnRef: true
 | 
			
		||||
                ])
 | 
			
		||||
            var verifyError: SecurityError?
 | 
			
		||||
            var untyped: CFTypeRef?
 | 
			
		||||
            let status = SecItemCopyMatching(attributes, &untyped)
 | 
			
		||||
            if status != errSecSuccess {
 | 
			
		||||
                throw KeychainError(statusCode: status)
 | 
			
		||||
            }
 | 
			
		||||
            guard let untypedSafe = untyped else {
 | 
			
		||||
                throw KeychainError(statusCode: errSecSuccess)
 | 
			
		||||
            }
 | 
			
		||||
            let key = untypedSafe as! SecKey
 | 
			
		||||
            let verified = SecKeyVerifySignature(key, .ecdsaSignatureMessageX962SHA256, data as CFData, signature as CFData, &verifyError)
 | 
			
		||||
            if !verified, let verifyError {
 | 
			
		||||
                if verifyError.takeUnretainedValue() ~= .verifyError {
 | 
			
		||||
                    return false
 | 
			
		||||
                } else {
 | 
			
		||||
                    throw SigningError(error: verifyError)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return verified
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public func existingPersistedAuthenticationContext(secret: Secret) -> PersistedAuthenticationContext? {
 | 
			
		||||
            guard let persisted = persistedAuthenticationContexts[secret], persisted.valid else { return nil }
 | 
			
		||||
            return persisted
 | 
			
		||||
@ -260,34 +295,12 @@ extension SecureEnclave.Store {
 | 
			
		||||
            ])
 | 
			
		||||
        let status = SecItemAdd(attributes, nil)
 | 
			
		||||
        if status != errSecSuccess {
 | 
			
		||||
            throw SecureEnclave.KeychainError(statusCode: status)
 | 
			
		||||
            throw KeychainError(statusCode: status)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension SecureEnclave {
 | 
			
		||||
 | 
			
		||||
    /// A wrapper around an error code reported by a Keychain API.
 | 
			
		||||
    public struct KeychainError: Error {
 | 
			
		||||
        /// The status code involved, if one was reported.
 | 
			
		||||
        public let statusCode: OSStatus?
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// A signing-related error.
 | 
			
		||||
    public struct SigningError: Error {
 | 
			
		||||
        /// The underlying error reported by the API, if one was returned.
 | 
			
		||||
        public let error: SecurityError?
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension SecureEnclave {
 | 
			
		||||
 | 
			
		||||
    public typealias SecurityError = Unmanaged<CFError>
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension SecureEnclave {
 | 
			
		||||
 | 
			
		||||
    enum Constants {
 | 
			
		||||
 | 
			
		||||
@ -6,9 +6,3 @@
 | 
			
		||||
 | 
			
		||||
- ``Secret``
 | 
			
		||||
- ``Store``
 | 
			
		||||
 | 
			
		||||
### Errors
 | 
			
		||||
 | 
			
		||||
- ``KeychainError``
 | 
			
		||||
- ``SigningError``
 | 
			
		||||
- ``SecurityError``
 | 
			
		||||
 | 
			
		||||
@ -45,7 +45,7 @@ extension SmartCard {
 | 
			
		||||
            fatalError("Keys must be deleted on the smart card.")
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data {
 | 
			
		||||
        public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) throws -> Data {
 | 
			
		||||
            guard let tokenID = tokenID else { fatalError() }
 | 
			
		||||
            let context = LAContext()
 | 
			
		||||
            context.localizedReason = "sign a request from \"\(provenance.origin.displayName)\" using secret \"\(secret.name)\""
 | 
			
		||||
@ -68,26 +68,40 @@ extension SmartCard {
 | 
			
		||||
            }
 | 
			
		||||
            let key = untypedSafe as! SecKey
 | 
			
		||||
            var signError: SecurityError?
 | 
			
		||||
            let signatureAlgorithm: SecKeyAlgorithm
 | 
			
		||||
            switch (secret.algorithm, secret.keySize) {
 | 
			
		||||
            case (.ellipticCurve, 256):
 | 
			
		||||
                signatureAlgorithm = .ecdsaSignatureMessageX962SHA256
 | 
			
		||||
            case (.ellipticCurve, 384):
 | 
			
		||||
                signatureAlgorithm = .ecdsaSignatureMessageX962SHA384
 | 
			
		||||
            default:
 | 
			
		||||
                fatalError()
 | 
			
		||||
            }
 | 
			
		||||
            guard let signature = SecKeyCreateSignature(key, signatureAlgorithm, data as CFData, &signError) else {
 | 
			
		||||
            guard let signature = SecKeyCreateSignature(key, signatureAlgorithm(for: secret, allowRSA: true), data as CFData, &signError) else {
 | 
			
		||||
                throw SigningError(error: signError)
 | 
			
		||||
            }
 | 
			
		||||
            return signature as Data
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        public func verify(signature: Data, for data: Data, with secret: Secret) throws -> Bool {
 | 
			
		||||
            let attributes = KeychainDictionary([
 | 
			
		||||
                kSecAttrKeyType: secret.algorithm.secAttrKeyType,
 | 
			
		||||
                kSecAttrKeySizeInBits: secret.keySize,
 | 
			
		||||
                kSecAttrKeyClass: kSecAttrKeyClassPublic
 | 
			
		||||
            ])
 | 
			
		||||
            var verifyError: SecurityError?
 | 
			
		||||
            let untyped: CFTypeRef? = SecKeyCreateWithData(secret.publicKey as CFData, attributes, &verifyError)
 | 
			
		||||
            guard let untypedSafe = untyped else {
 | 
			
		||||
                throw KeychainError(statusCode: errSecSuccess)
 | 
			
		||||
            }
 | 
			
		||||
            let key = untypedSafe as! SecKey
 | 
			
		||||
            let verified = SecKeyVerifySignature(key, signatureAlgorithm(for: secret, allowRSA: true), data as CFData, signature as CFData, &verifyError)
 | 
			
		||||
            if !verified, let verifyError {
 | 
			
		||||
                if verifyError.takeUnretainedValue() ~= .verifyError {
 | 
			
		||||
                    return false
 | 
			
		||||
                } else {
 | 
			
		||||
                    throw SigningError(error: verifyError)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return verified
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public func existingPersistedAuthenticationContext(secret: SmartCard.Secret) -> PersistedAuthenticationContext? {
 | 
			
		||||
        public func existingPersistedAuthenticationContext(secret: Secret) -> PersistedAuthenticationContext? {
 | 
			
		||||
            nil
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public func persistAuthentication(secret: SmartCard.Secret, forDuration: TimeInterval) throws {
 | 
			
		||||
        public func persistAuthentication(secret: Secret, forDuration: TimeInterval) throws {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// Reloads all secrets from the store.
 | 
			
		||||
@ -140,7 +154,6 @@ extension SmartCard.Store {
 | 
			
		||||
        let attributes = KeychainDictionary([
 | 
			
		||||
            kSecClass: kSecClassKey,
 | 
			
		||||
            kSecAttrTokenID: tokenID,
 | 
			
		||||
            kSecAttrKeyType: kSecAttrKeyTypeEC, // Restrict to EC
 | 
			
		||||
            kSecReturnRef: true,
 | 
			
		||||
            kSecMatchLimit: kSecMatchLimitAll,
 | 
			
		||||
            kSecReturnAttributes: true
 | 
			
		||||
@ -148,7 +161,7 @@ extension SmartCard.Store {
 | 
			
		||||
        var untyped: CFTypeRef?
 | 
			
		||||
        SecItemCopyMatching(attributes, &untyped)
 | 
			
		||||
        guard let typed = untyped as? [[CFString: Any]] else { return }
 | 
			
		||||
        let wrapped: [SmartCard.Secret] = typed.map {
 | 
			
		||||
        let wrapped = typed.map {
 | 
			
		||||
            let name = $0[kSecAttrLabel] as? String ?? "Unnamed"
 | 
			
		||||
            let tokenID = $0[kSecAttrApplicationLabel] as! Data
 | 
			
		||||
            let algorithm = Algorithm(secAttr: $0[kSecAttrKeyType] as! NSNumber)
 | 
			
		||||
@ -164,6 +177,88 @@ extension SmartCard.Store {
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// MARK: Smart Card specific encryption/decryption/verification
 | 
			
		||||
extension SmartCard.Store {
 | 
			
		||||
 | 
			
		||||
    /// Encrypts a payload with a specified key.
 | 
			
		||||
    /// - Parameters:
 | 
			
		||||
    ///   - data: The payload to encrypt.
 | 
			
		||||
    ///   - secret: The secret to encrypt with.
 | 
			
		||||
    /// - Returns: The encrypted data.
 | 
			
		||||
    /// - Warning: Encryption functions are deliberately only exposed on a library level, and are not exposed in Secretive itself to prevent users from data loss. Any pull requests which expose this functionality in the app will not be merged.
 | 
			
		||||
    public func encrypt(data: Data, with secret: SecretType) throws -> Data {
 | 
			
		||||
        let context = LAContext()
 | 
			
		||||
        context.localizedReason = "encrypt data using secret \"\(secret.name)\""
 | 
			
		||||
        context.localizedCancelTitle = "Deny"
 | 
			
		||||
        let attributes = KeychainDictionary([
 | 
			
		||||
            kSecAttrKeyType: secret.algorithm.secAttrKeyType,
 | 
			
		||||
            kSecAttrKeySizeInBits: secret.keySize,
 | 
			
		||||
            kSecAttrKeyClass: kSecAttrKeyClassPublic,
 | 
			
		||||
            kSecUseAuthenticationContext: context
 | 
			
		||||
        ])
 | 
			
		||||
        var encryptError: SecurityError?
 | 
			
		||||
        let untyped: CFTypeRef? = SecKeyCreateWithData(secret.publicKey as CFData, attributes, &encryptError)
 | 
			
		||||
        guard let untypedSafe = untyped else {
 | 
			
		||||
            throw KeychainError(statusCode: errSecSuccess)
 | 
			
		||||
        }
 | 
			
		||||
        let key = untypedSafe as! SecKey
 | 
			
		||||
        guard let signature = SecKeyCreateEncryptedData(key, encryptionAlgorithm(for: secret), data as CFData, &encryptError) else {
 | 
			
		||||
            throw SigningError(error: encryptError)
 | 
			
		||||
        }
 | 
			
		||||
        return signature as Data
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Decrypts a payload with a specified key.
 | 
			
		||||
    /// - Parameters:
 | 
			
		||||
    ///   - data: The payload to decrypt.
 | 
			
		||||
    ///   - secret: The secret to decrypt with.
 | 
			
		||||
    /// - Returns: The decrypted data.
 | 
			
		||||
    /// - Warning: Encryption functions are deliberately only exposed on a library level, and are not exposed in Secretive itself to prevent users from data loss. Any pull requests which expose this functionality in the app will not be merged.
 | 
			
		||||
    public func decrypt(data: Data, with secret: SecretType) throws -> Data {
 | 
			
		||||
        guard let tokenID = tokenID else { fatalError() }
 | 
			
		||||
        let context = LAContext()
 | 
			
		||||
        context.localizedReason = "decrypt data using secret \"\(secret.name)\""
 | 
			
		||||
        context.localizedCancelTitle = "Deny"
 | 
			
		||||
        let attributes = KeychainDictionary([
 | 
			
		||||
            kSecClass: kSecClassKey,
 | 
			
		||||
            kSecAttrKeyClass: kSecAttrKeyClassPrivate,
 | 
			
		||||
            kSecAttrApplicationLabel: secret.id as CFData,
 | 
			
		||||
            kSecAttrTokenID: tokenID,
 | 
			
		||||
            kSecUseAuthenticationContext: context,
 | 
			
		||||
            kSecReturnRef: true
 | 
			
		||||
        ])
 | 
			
		||||
        var untyped: CFTypeRef?
 | 
			
		||||
        let status = SecItemCopyMatching(attributes, &untyped)
 | 
			
		||||
        if status != errSecSuccess {
 | 
			
		||||
            throw KeychainError(statusCode: status)
 | 
			
		||||
        }
 | 
			
		||||
        guard let untypedSafe = untyped else {
 | 
			
		||||
            throw KeychainError(statusCode: errSecSuccess)
 | 
			
		||||
        }
 | 
			
		||||
        let key = untypedSafe as! SecKey
 | 
			
		||||
        var encryptError: SecurityError?
 | 
			
		||||
        guard let signature = SecKeyCreateDecryptedData(key, encryptionAlgorithm(for: secret), data as CFData, &encryptError) else {
 | 
			
		||||
            throw SigningError(error: encryptError)
 | 
			
		||||
        }
 | 
			
		||||
        return signature as Data
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func encryptionAlgorithm(for secret: SecretType) -> SecKeyAlgorithm {
 | 
			
		||||
        switch (secret.algorithm, secret.keySize) {
 | 
			
		||||
        case (.ellipticCurve, 256):
 | 
			
		||||
            return .eciesEncryptionCofactorVariableIVX963SHA256AESGCM
 | 
			
		||||
        case (.ellipticCurve, 384):
 | 
			
		||||
            return .eciesEncryptionCofactorVariableIVX963SHA256AESGCM
 | 
			
		||||
        case (.rsa, 1024), (.rsa, 2048):
 | 
			
		||||
            return .rsaEncryptionOAEPSHA512AESGCM
 | 
			
		||||
        default:
 | 
			
		||||
            fatalError()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension TKTokenWatcher {
 | 
			
		||||
 | 
			
		||||
    /// All available tokens, excluding the Secure Enclave.
 | 
			
		||||
@ -172,25 +267,3 @@ extension TKTokenWatcher {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension SmartCard {
 | 
			
		||||
 | 
			
		||||
    /// A wrapper around an error code reported by a Keychain API.
 | 
			
		||||
    public struct KeychainError: Error {
 | 
			
		||||
        /// The status code involved.
 | 
			
		||||
        public let statusCode: OSStatus
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// A signing-related error.
 | 
			
		||||
    public struct SigningError: Error {
 | 
			
		||||
        /// The underlying error reported by the API, if one was returned.
 | 
			
		||||
        public let error: SecurityError?
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension SmartCard {
 | 
			
		||||
 | 
			
		||||
    public typealias SecurityError = Unmanaged<CFError>
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -61,8 +61,17 @@ class AgentTests: XCTestCase {
 | 
			
		||||
        var rs = r
 | 
			
		||||
        rs.append(s)
 | 
			
		||||
        let signature = try! P256.Signing.ECDSASignature(rawRepresentation: rs)
 | 
			
		||||
        let valid = try! P256.Signing.PublicKey(x963Representation: Constants.Secrets.ecdsa256Secret.publicKey).isValidSignature(signature, for: dataToSign)
 | 
			
		||||
        XCTAssertTrue(valid)
 | 
			
		||||
        let referenceValid = try! P256.Signing.PublicKey(x963Representation: Constants.Secrets.ecdsa256Secret.publicKey).isValidSignature(signature, for: dataToSign)
 | 
			
		||||
        let store = list.stores.first!
 | 
			
		||||
        let derVerifies = try! store.verify(signature: signature.derRepresentation, for: dataToSign, with: AnySecret(Constants.Secrets.ecdsa256Secret))
 | 
			
		||||
        let invalidRandomSignature = try? store.verify(signature: "invalid".data(using: .utf8)!, for: dataToSign, with: AnySecret(Constants.Secrets.ecdsa256Secret))
 | 
			
		||||
        let invalidRandomData = try? store.verify(signature: signature.derRepresentation, for: "invalid".data(using: .utf8)!, with: AnySecret(Constants.Secrets.ecdsa256Secret))
 | 
			
		||||
        let invalidWrongKey = try? store.verify(signature: signature.derRepresentation, for: dataToSign, with: AnySecret(Constants.Secrets.ecdsa384Secret))
 | 
			
		||||
        XCTAssertTrue(referenceValid)
 | 
			
		||||
        XCTAssertTrue(derVerifies)
 | 
			
		||||
        XCTAssert(invalidRandomSignature == false)
 | 
			
		||||
        XCTAssert(invalidRandomData == false)
 | 
			
		||||
        XCTAssert(invalidWrongKey == false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // MARK: Witness protocol
 | 
			
		||||
 | 
			
		||||
@ -58,16 +58,30 @@ extension Stub {
 | 
			
		||||
                kSecAttrKeyClass: kSecAttrKeyClassPrivate
 | 
			
		||||
                ])
 | 
			
		||||
                , nil)!
 | 
			
		||||
            let signatureAlgorithm: SecKeyAlgorithm
 | 
			
		||||
            switch secret.keySize {
 | 
			
		||||
            case 256:
 | 
			
		||||
                signatureAlgorithm = .ecdsaSignatureMessageX962SHA256
 | 
			
		||||
            case 384:
 | 
			
		||||
                signatureAlgorithm = .ecdsaSignatureMessageX962SHA384
 | 
			
		||||
            default:
 | 
			
		||||
                fatalError()
 | 
			
		||||
            return SecKeyCreateSignature(privateKey, signatureAlgorithm(for: secret), data as CFData, nil)! as Data
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public func verify(signature: Data, for data: Data, with secret: Stub.Secret) throws -> Bool {
 | 
			
		||||
            let attributes = KeychainDictionary([
 | 
			
		||||
                kSecAttrKeyType: secret.algorithm.secAttrKeyType,
 | 
			
		||||
                kSecAttrKeySizeInBits: secret.keySize,
 | 
			
		||||
                kSecAttrKeyClass: kSecAttrKeyClassPublic
 | 
			
		||||
            ])
 | 
			
		||||
            var verifyError: Unmanaged<CFError>?
 | 
			
		||||
            let untyped: CFTypeRef? = SecKeyCreateWithData(secret.publicKey as CFData, attributes, &verifyError)
 | 
			
		||||
            guard let untypedSafe = untyped else {
 | 
			
		||||
                throw NSError(domain: "test", code: 0, userInfo: nil)
 | 
			
		||||
            }
 | 
			
		||||
            return SecKeyCreateSignature(privateKey, signatureAlgorithm, data as CFData, nil)! as Data
 | 
			
		||||
            let key = untypedSafe as! SecKey
 | 
			
		||||
            let verified = SecKeyVerifySignature(key, signatureAlgorithm(for: secret), data as CFData, signature as CFData, &verifyError)
 | 
			
		||||
            if let verifyError {
 | 
			
		||||
                if verifyError.takeUnretainedValue() ~= .verifyError {
 | 
			
		||||
                    return false
 | 
			
		||||
                } else {
 | 
			
		||||
                    throw NSError(domain: "test", code: 0, userInfo: nil)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return verified
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public func existingPersistedAuthenticationContext(secret: Stub.Secret) -> PersistedAuthenticationContext? {
 | 
			
		||||
 | 
			
		||||
@ -40,6 +40,10 @@ extension Preview {
 | 
			
		||||
            return data
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        func verify(signature data: Data, for signature: Data, with secret: Preview.Secret) throws -> Bool {
 | 
			
		||||
            true
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        func existingPersistedAuthenticationContext(secret: Preview.Secret) -> PersistedAuthenticationContext? {
 | 
			
		||||
            nil
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -34,7 +34,7 @@ struct EmptyStoreImmutableView: View {
 | 
			
		||||
        VStack {
 | 
			
		||||
            Text("No Secrets").bold()
 | 
			
		||||
            Text("Use your Smart Card's management tool to create a secret.")
 | 
			
		||||
            Text("Secretive only supports Elliptic Curve keys.")
 | 
			
		||||
            Text("Secretive supports EC256, EC384, RSA1024, and RSA2048 keys.")
 | 
			
		||||
        }.frame(maxWidth: .infinity, maxHeight: .infinity)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user