diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 0b06e86..375b1ad 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -1,5 +1,5 @@ # SPDX-License-Identifier: MIT -# Copyright 2024, Jamf +# Copyright 2026, Jamf name: UnitTests @@ -11,29 +11,29 @@ on: jobs: Test-on-macOS: - runs-on: macos-14 + runs-on: macos-26 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Run macOS tests (including integration) run: swift test Test-on-all-others: - runs-on: macos-14 + runs-on: macos-26 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Run iOS tests - run: xcodebuild test -scheme Haversack-Package -destination 'platform=iOS Simulator,name=iPhone 14' + run: xcodebuild test -scheme Haversack-Package -destination 'platform=iOS Simulator,name=iPhone 17' - name: Run tvOS tests run: xcodebuild test -scheme Haversack-Package -destination 'platform=tvOS Simulator,name=Apple TV 4K (3rd generation)' - name: Run watchOS tests - run: xcodebuild test -scheme Haversack-Package -destination 'platform=watchOS Simulator,name=Apple Watch Series 10 (42mm)' + run: xcodebuild test -scheme Haversack-Package -destination 'platform=watchOS Simulator,name=Apple Watch Series 11 (42mm)' SwiftLint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: GitHub Action for SwiftLint uses: norio-nomura/action-swiftlint@3.2.1 diff --git a/.swiftlint.yml b/.swiftlint.yml index 48a2ff0..ca5099f 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -12,7 +12,7 @@ file_header: severity: error required_pattern: | \/\/ SPDX-License-Identifier: MIT - \/\/ Copyright 2023, Jamf + \/\/ Copyright 2026, Jamf opt_in_rules: - contains_over_filter_count diff --git a/CHANGELOG.md b/CHANGELOG.md index cffe713..57b8247 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased + +## [1.4.0] - 2026-03-18 ### Added - Added convenience methods for accessing mock data values on the `HaversackEphemeralStrategy` +- Added `Sendable` conformance to all public types, protocols, and enums. + +### Changed +- Updated to build using Swift 6. +- Entity types (`CertificateEntity`, `GenericPasswordEntity`, `InternetPasswordEntity`, `IdentityEntity`, `KeyEntity`) converted from classes to structs. +- `PasswordBaseEntity` converted from a base class to a protocol with default implementations. +- `KeychainStorable` protocol now requires `Equatable` conformance. +- `KeychainFile` is now a `final class`. +- Query and configuration properties use `NSLock` for thread-safe access. +- Internal `CFString` dictionary keys migrated to `String` for `Sendable` compatibility. +- Completion handlers and closure properties marked `@Sendable`. ## [1.3.0] - 2024-02-11 ### Added diff --git a/LICENSE b/LICENSE index 0826045..e6f680c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright 2024, Jamf +Copyright 2026, Jamf Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Package.resolved b/Package.resolved index ad0205c..78a555e 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections", "state" : { - "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", - "version" : "1.1.0" + "revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e", + "version" : "1.3.0" } }, { @@ -14,14 +14,14 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-docc-plugin", "state" : { - "revision" : "26ac5758409154cc448d7ab82389c520fa8a8247", - "version" : "1.3.0" + "revision" : "e977f65879f82b375a044c8837597f690c067da6", + "version" : "1.4.6" } }, { "identity" : "swift-docc-symbolkit", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-docc-symbolkit", + "location" : "https://github.com/swiftlang/swift-docc-symbolkit", "state" : { "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", "version" : "1.0.0" diff --git a/Package.swift b/Package.swift index 2f0c125..573b114 100644 --- a/Package.swift +++ b/Package.swift @@ -1,6 +1,6 @@ -// swift-tools-version:5.9 +// swift-tools-version: 5.10 // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import PackageDescription @@ -10,7 +10,7 @@ let package = Package( .macOS(.v10_13), .iOS(.v12), .tvOS(.v12), - .visionOS(.v1), + .visionOS(.v1), .watchOS(.v5) ], products: [ @@ -24,14 +24,15 @@ let package = Package( ], targets: [ .target(name: "Haversack", - dependencies: [ + dependencies: [ .product(name: "OrderedCollections", package: "swift-collections") - ], - resources: [.process("Resources/")]), + ], + resources: [.process("Resources/")]), .target(name: "HaversackCryptoKit", dependencies: ["Haversack"]), .target(name: "HaversackMock", dependencies: ["Haversack"]), .testTarget(name: "HaversackTests", dependencies: ["HaversackMock"], resources: [.copy("TestResources/")]) - ] + ], + swiftLanguageVersions: [.v5, .version("6")] ) diff --git a/README.md b/README.md index 732209a..0e68652 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ Save a password for a website: ```swift let myHaversack = Haversack() - let newPassword = InternetPasswordEntity() + var newPassword = InternetPasswordEntity() newPassword.protocol = .HTTPS newPassword.server = "test.example.com" newPassword.account = "mine" @@ -145,3 +145,4 @@ Before submitting your pull request, please do the following: - Kyle Hammond - Jacob Hearst +- Michael Link diff --git a/Sources/Haversack/Entities/CertificateEntity.swift b/Sources/Haversack/Entities/CertificateEntity.swift index 000d23e..f4a14e7 100644 --- a/Sources/Haversack/Entities/CertificateEntity.swift +++ b/Sources/Haversack/Entities/CertificateEntity.swift @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation import OrderedCollections /// Represents a certificate in the keychain. -public class CertificateEntity: KeychainStorable, KeychainPortable { +public struct CertificateEntity: KeychainStorable, KeychainPortable { /// Uses the `SecCertificate` type to interface with the Security framework. public typealias SecurityFrameworkType = SecCertificate @@ -58,8 +58,8 @@ public class CertificateEntity: KeychainStorable, KeychainPortable { /// The `subjectData` parsed into a dictionary of OIDs to names; read only. public private(set) var subjectStrings: OrderedDictionary? - public required init(from keychainItemRef: SecurityFrameworkType?, data: Data?, - attributes: [String: Any]?, persistentRef: Data?) { + public init(from keychainItemRef: SecurityFrameworkType?, data: Data?, + attributes: [String: Any]?, persistentRef: Data?) { reference = keychainItemRef certificateData = data self.persistentRef = persistentRef @@ -90,7 +90,7 @@ public class CertificateEntity: KeychainStorable, KeychainPortable { /// A simple initializer to use with an existing `SecCertificate` not in the keychain. /// - Parameter keychainItemRef: A `SecCertificate` that is not in a keychain. - public convenience init(from keychainItemRef: SecCertificate) { + public init(from keychainItemRef: SecCertificate) { self.init(from: keychainItemRef, data: nil, attributes: nil, persistentRef: nil) } diff --git a/Sources/Haversack/Entities/Data+X501Name.swift b/Sources/Haversack/Entities/Data+X501Name.swift index 4006bbb..3bf9bf7 100644 --- a/Sources/Haversack/Entities/Data+X501Name.swift +++ b/Sources/Haversack/Entities/Data+X501Name.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation import OrderedCollections diff --git a/Sources/Haversack/Entities/GenericPasswordEntity.swift b/Sources/Haversack/Entities/GenericPasswordEntity.swift index 87ecf92..394aae2 100644 --- a/Sources/Haversack/Entities/GenericPasswordEntity.swift +++ b/Sources/Haversack/Entities/GenericPasswordEntity.swift @@ -1,12 +1,85 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation +@preconcurrency import Security /// Represents a password to anything in the keychain. /// /// The combination of `service` and `account` values is unique per generic password in the keychain. -public class GenericPasswordEntity: PasswordBaseEntity { +public struct GenericPasswordEntity: PasswordBaseEntity { +#if os(macOS) + /// The native Security framework type associated with `PasswordBaseEntity` + /// + /// On macOS uses the `SecKeychainItem` type to interface with the Security framework. + /// On iOS uses the [Data](https://developer.apple.com/documentation/Foundation/Data) + /// type to interface with the Security framework. + public typealias SecurityFrameworkType = SecKeychainItem +#else + /// The native Security framework type associated with `PasswordBaseEntity` + /// + /// On macOS uses the `SecKeychainItem` type to interface with the Security framework. + /// On iOS uses the [Data](https://developer.apple.com/documentation/Foundation/Data) + /// type to interface with the Security framework. + public typealias SecurityFrameworkType = Data +#endif + + /// The keychain item reference, if it has been returned. + public var reference: SecurityFrameworkType? + + /// The persistent keychain item reference, if it has been returned. + public var persistentRef: Data? + + /// When the item was created; read only. + /// - Note: Uses `kSecAttrCreationDate` + public private(set) var creationDate: Date? + + /// When the item was last modified; read only. + /// - Note: Uses `kSecAttrModificationDate` + public private(set) var modificationDate: Date? + + /// The item's creator. + /// - Note: Uses `kSecAttrCreator` + public var creator: Int? // FourCharCode + + /// A description to store alongside the item. + /// + /// In Keychain Access this is the `Kind` field. + /// - Note: Uses `kSecAttrDescription` + public var description: String? + + /// A comment to store alongside the item. + /// + /// In Keychain Access this is the `Comment` field. + /// - Note: Uses `kSecAttrComment`. + public var comment: String? + + /// User-defined group number for passwords + /// - Note: Uses `kSecAttrType` + public var group: Int? // FourCharCode + + /// A user-visible label for the item. + /// + /// In Keychain Access this is the `Name` field. + /// - Note: Uses `kSecAttrLabel` + public var label: String? + + /// Whether you want this to show up in Keychain Access. + /// - Note: Uses `kSecAttrIsInvisible` + public var isInvisible: Bool? + + /// The name of an account within a service associated with the password. + /// + /// In Keychain Access this is the `Account` field. + /// - Note: Uses `kSecAttrAccount` + public var account: String? + + /// The actual password. + /// + /// If this is nil, when saving to the keychain the `kSecAttrIsNegative` is set to `true` instead. + /// - Note: Uses `kSecValueData`. + public var passwordData: Data? + /// The name of the service associated with the password. /// /// In Keychain Access this is the `Where` field. @@ -18,8 +91,8 @@ public class GenericPasswordEntity: PasswordBaseEntity { public var customData: Data? /// Create an empty generic password entity - override public init() { - super.init() + public init() { + // Everything is nil with this constructor. } /// Returns a ``GenericPasswordEntity`` object initialized to correspond to an existing keychain item. @@ -28,11 +101,21 @@ public class GenericPasswordEntity: PasswordBaseEntity { /// - data: If given, the raw unencrypted data of the password. /// - attributes: If given, the attributes of an existing keychain item. /// - persistentRef: If given, a persistent reference to an existing keychain item. - public required init(from keychainItemRef: SecurityFrameworkType?, data: Data?, - attributes: [String: Any]?, persistentRef: Data?) { - super.init(from: keychainItemRef, data: data, attributes: attributes, persistentRef: persistentRef) + public init(from keychainItemRef: SecurityFrameworkType?, data: Data?, + attributes: [String: Any]?, persistentRef: Data?) { + reference = keychainItemRef + passwordData = data + self.persistentRef = persistentRef if let attrs = attributes { + creationDate = attrs[kSecAttrCreationDate as String] as? Date + modificationDate = attrs[kSecAttrModificationDate as String] as? Date + label = attrs[kSecAttrLabel as String] as? String + account = attrs[kSecAttrAccount as String] as? String + group = attrs[kSecAttrType as String] as? Int + comment = attrs[kSecAttrComment as String] as? String + description = attrs[kSecAttrDescription as String] as? String + creator = attrs[kSecAttrCreator as String] as? Int service = attrs[kSecAttrService as String] as? String customData = attrs[kSecAttrGeneric as String] as? Data } @@ -40,8 +123,8 @@ public class GenericPasswordEntity: PasswordBaseEntity { // MARK: - KeychainStorable - override public func entityQuery(includeSecureData: Bool) -> SecurityFrameworkQuery { - var query = super.entityQuery(includeSecureData: includeSecureData) + public func entityQuery(includeSecureData: Bool) -> SecurityFrameworkQuery { + var query = _entityQuery(includeSecureData: includeSecureData) query[kSecClass as String] = kSecClassGenericPassword diff --git a/Sources/Haversack/Entities/IdentityEntity.swift b/Sources/Haversack/Entities/IdentityEntity.swift index 561cd9a..33a7cb7 100644 --- a/Sources/Haversack/Entities/IdentityEntity.swift +++ b/Sources/Haversack/Entities/IdentityEntity.swift @@ -1,10 +1,11 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation +@preconcurrency import Security /// Represents a certificate plus private key in the keychain. -public class IdentityEntity: KeychainStorable, KeychainPortable { +public struct IdentityEntity: KeychainStorable, KeychainPortable { /// Uses the `SecIdentity` type to interface with the Security framework. public typealias SecurityFrameworkType = SecIdentity @@ -20,8 +21,8 @@ public class IdentityEntity: KeychainStorable, KeychainPortable { /// When requesting attributes, this is filled with the certificate info from the identity; read only. public var certificate: CertificateEntity? - public required init(from keychainItemRef: SecurityFrameworkType?, data: Data?, - attributes: [String: Any]?, persistentRef: Data?) { + public init(from keychainItemRef: SecurityFrameworkType?, data: Data?, + attributes: [String: Any]?, persistentRef: Data?) { reference = keychainItemRef self.persistentRef = persistentRef diff --git a/Sources/Haversack/Entities/InternetPasswordEntity.swift b/Sources/Haversack/Entities/InternetPasswordEntity.swift index dbef36f..2261814 100644 --- a/Sources/Haversack/Entities/InternetPasswordEntity.swift +++ b/Sources/Haversack/Entities/InternetPasswordEntity.swift @@ -1,12 +1,85 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation +@preconcurrency import Security /// Represents a password to an account on another computer or website in the keychain. /// /// The combination of `server` and `account` values is unique per internet password in the keychain. -public class InternetPasswordEntity: PasswordBaseEntity { +public struct InternetPasswordEntity: PasswordBaseEntity { +#if os(macOS) + /// The native Security framework type associated with `PasswordBaseEntity` + /// + /// On macOS uses the `SecKeychainItem` type to interface with the Security framework. + /// On iOS uses the [Data](https://developer.apple.com/documentation/Foundation/Data) + /// type to interface with the Security framework. + public typealias SecurityFrameworkType = SecKeychainItem +#else + /// The native Security framework type associated with `PasswordBaseEntity` + /// + /// On macOS uses the `SecKeychainItem` type to interface with the Security framework. + /// On iOS uses the [Data](https://developer.apple.com/documentation/Foundation/Data) + /// type to interface with the Security framework. + public typealias SecurityFrameworkType = Data +#endif + + /// The keychain item reference, if it has been returned. + public var reference: SecurityFrameworkType? + + /// The persistent keychain item reference, if it has been returned. + public var persistentRef: Data? + + /// When the item was created; read only. + /// - Note: Uses `kSecAttrCreationDate` + public private(set) var creationDate: Date? + + /// When the item was last modified; read only. + /// - Note: Uses `kSecAttrModificationDate` + public private(set) var modificationDate: Date? + + /// The item's creator. + /// - Note: Uses `kSecAttrCreator` + public var creator: Int? // FourCharCode + + /// A description to store alongside the item. + /// + /// In Keychain Access this is the `Kind` field. + /// - Note: Uses `kSecAttrDescription` + public var description: String? + + /// A comment to store alongside the item. + /// + /// In Keychain Access this is the `Comment` field. + /// - Note: Uses `kSecAttrComment`. + public var comment: String? + + /// User-defined group number for passwords + /// - Note: Uses `kSecAttrType` + public var group: Int? // FourCharCode + + /// A user-visible label for the item. + /// + /// In Keychain Access this is the `Name` field. + /// - Note: Uses `kSecAttrLabel` + public var label: String? + + /// Whether you want this to show up in Keychain Access. + /// - Note: Uses `kSecAttrIsInvisible` + public var isInvisible: Bool? + + /// The name of an account within a service associated with the password. + /// + /// In Keychain Access this is the `Account` field. + /// - Note: Uses `kSecAttrAccount` + public var account: String? + + /// The actual password. + /// + /// If this is nil, when saving to the keychain the `kSecAttrIsNegative` is set to `true` instead. + /// - Note: Uses `kSecValueData`. + public var passwordData: Data? + /// The Internet security domain. /// - Note: Uses `kSecAttrSecurityDomain` public var securityDomain: String? @@ -17,7 +90,7 @@ public class InternetPasswordEntity: PasswordBaseEntity { /// A communications protocol for internet passwords. /// - Note: Mirrors the `kSecAttrProtocol...` constants. - public enum NetworkProtocol { + public enum NetworkProtocol: Sendable { case FTP case FTPAccount case HTTP @@ -50,46 +123,46 @@ public class InternetPasswordEntity: PasswordBaseEntity { case IRCS case POP3S - private static let translation: [CFString: NetworkProtocol] = [ - kSecAttrProtocolFTP: FTP, - kSecAttrProtocolFTPAccount: FTPAccount, - kSecAttrProtocolHTTP: HTTP, - kSecAttrProtocolIRC: IRC, - kSecAttrProtocolNNTP: NNTP, - kSecAttrProtocolPOP3: POP3, - kSecAttrProtocolSMTP: SMTP, - kSecAttrProtocolSOCKS: SOCKS, - kSecAttrProtocolIMAP: IMAP, - kSecAttrProtocolLDAP: LDAP, - kSecAttrProtocolAppleTalk: appleTalk, - kSecAttrProtocolAFP: AFP, - kSecAttrProtocolTelnet: telnet, - kSecAttrProtocolSSH: SSH, - kSecAttrProtocolFTPS: FTPS, - kSecAttrProtocolHTTPS: HTTPS, - kSecAttrProtocolHTTPProxy: HTTPProxy, - kSecAttrProtocolHTTPSProxy: HTTPSProxy, - kSecAttrProtocolFTPProxy: FTPProxy, - kSecAttrProtocolSMB: SMB, - kSecAttrProtocolRTSP: RTSP, - kSecAttrProtocolRTSPProxy: RTSPProxy, - kSecAttrProtocolDAAP: DAAP, - kSecAttrProtocolEPPC: EPPC, - kSecAttrProtocolIPP: IPP, - kSecAttrProtocolNNTPS: NNTPS, - kSecAttrProtocolLDAPS: LDAPS, - kSecAttrProtocolTelnetS: telnetS, - kSecAttrProtocolIMAPS: IMAPS, - kSecAttrProtocolIRCS: IRCS, - kSecAttrProtocolPOP3S: POP3S + private static let translation: [String: NetworkProtocol] = [ + kSecAttrProtocolFTP as String: FTP, + kSecAttrProtocolFTPAccount as String: FTPAccount, + kSecAttrProtocolHTTP as String: HTTP, + kSecAttrProtocolIRC as String: IRC, + kSecAttrProtocolNNTP as String: NNTP, + kSecAttrProtocolPOP3 as String: POP3, + kSecAttrProtocolSMTP as String: SMTP, + kSecAttrProtocolSOCKS as String: SOCKS, + kSecAttrProtocolIMAP as String: IMAP, + kSecAttrProtocolLDAP as String: LDAP, + kSecAttrProtocolAppleTalk as String: appleTalk, + kSecAttrProtocolAFP as String: AFP, + kSecAttrProtocolTelnet as String: telnet, + kSecAttrProtocolSSH as String: SSH, + kSecAttrProtocolFTPS as String: FTPS, + kSecAttrProtocolHTTPS as String: HTTPS, + kSecAttrProtocolHTTPProxy as String: HTTPProxy, + kSecAttrProtocolHTTPSProxy as String: HTTPSProxy, + kSecAttrProtocolFTPProxy as String: FTPProxy, + kSecAttrProtocolSMB as String: SMB, + kSecAttrProtocolRTSP as String: RTSP, + kSecAttrProtocolRTSPProxy as String: RTSPProxy, + kSecAttrProtocolDAAP as String: DAAP, + kSecAttrProtocolEPPC as String: EPPC, + kSecAttrProtocolIPP as String: IPP, + kSecAttrProtocolNNTPS as String: NNTPS, + kSecAttrProtocolLDAPS as String: LDAPS, + kSecAttrProtocolTelnetS as String: telnetS, + kSecAttrProtocolIMAPS as String: IMAPS, + kSecAttrProtocolIRCS as String: IRCS, + kSecAttrProtocolPOP3S as String: POP3S ] static func make(from securityFrameworkValue: CFString) -> NetworkProtocol? { - return translation[securityFrameworkValue] + return translation[securityFrameworkValue as String] } func securityFrameworkValue() -> CFString { - return Self.translation.first(where: { $1 == self })!.key + return Self.translation.first(where: { $1 == self })!.key as CFString } } @@ -99,7 +172,7 @@ public class InternetPasswordEntity: PasswordBaseEntity { /// An authentication scheme for internet passwords. /// - Note: Mirrors the `kSecAttrAuthenticationType...` constants. - public enum AuthenticationType { + public enum AuthenticationType: Sendable { case NTLM case MSN case DPA @@ -109,23 +182,23 @@ public class InternetPasswordEntity: PasswordBaseEntity { case HTMLForm case `default` - private static let translation: [CFString: AuthenticationType] = [ - kSecAttrAuthenticationTypeNTLM: NTLM, - kSecAttrAuthenticationTypeMSN: MSN, - kSecAttrAuthenticationTypeDPA: DPA, - kSecAttrAuthenticationTypeRPA: RPA, - kSecAttrAuthenticationTypeHTTPBasic: HTTPBasic, - kSecAttrAuthenticationTypeHTTPDigest: HTTPDigest, - kSecAttrAuthenticationTypeHTMLForm: HTMLForm, - kSecAttrAuthenticationTypeDefault: `default` + private static let translation: [String: AuthenticationType] = [ + kSecAttrAuthenticationTypeNTLM as String: NTLM, + kSecAttrAuthenticationTypeMSN as String: MSN, + kSecAttrAuthenticationTypeDPA as String: DPA, + kSecAttrAuthenticationTypeRPA as String: RPA, + kSecAttrAuthenticationTypeHTTPBasic as String: HTTPBasic, + kSecAttrAuthenticationTypeHTTPDigest as String: HTTPDigest, + kSecAttrAuthenticationTypeHTMLForm as String: HTMLForm, + kSecAttrAuthenticationTypeDefault as String: `default` ] static func make(from securityFrameworkValue: CFString) -> AuthenticationType? { - return translation[securityFrameworkValue] + return translation[securityFrameworkValue as String] } func securityFrameworkValue() -> CFString { - return Self.translation.first(where: { $1 == self })!.key + return Self.translation.first(where: { $1 == self })!.key as CFString } } @@ -142,23 +215,37 @@ public class InternetPasswordEntity: PasswordBaseEntity { public var path: String? /// Create an empty internet password entity - override public init() { - super.init() + public init() { + // Everything is nil with this constructor. } - public required init(from keychainItemRef: SecurityFrameworkType?, data: Data?, - attributes: [String: Any]?, persistentRef: Data?) { - super.init(from: keychainItemRef, data: data, attributes: attributes, persistentRef: persistentRef) + public init(from keychainItemRef: SecurityFrameworkType?, data: Data?, + attributes: [String: Any]?, persistentRef: Data?) { + reference = keychainItemRef + passwordData = data + self.persistentRef = persistentRef if let attrs = attributes { + creationDate = attrs[kSecAttrCreationDate as String] as? Date + modificationDate = attrs[kSecAttrModificationDate as String] as? Date + label = attrs[kSecAttrLabel as String] as? String + account = attrs[kSecAttrAccount as String] as? String + group = attrs[kSecAttrType as String] as? Int + comment = attrs[kSecAttrComment as String] as? String + description = attrs[kSecAttrDescription as String] as? String + creator = attrs[kSecAttrCreator as String] as? Int + securityDomain = attrs[kSecAttrSecurityDomain as String] as? String server = attrs[kSecAttrServer as String] as? String + if let possibleProtocol = attrs[kSecAttrProtocol as String] as? String { `protocol` = .make(from: possibleProtocol as CFString) } + if let possibleAuthType = attrs[kSecAttrAuthenticationType as String] as? String { authenticationType = .make(from: possibleAuthType as CFString) } + port = attrs[kSecAttrPort as String] as? Int path = attrs[kSecAttrPath as String] as? String } @@ -166,8 +253,8 @@ public class InternetPasswordEntity: PasswordBaseEntity { // MARK: - KeychainStorable - override public func entityQuery(includeSecureData: Bool) -> SecurityFrameworkQuery { - var query = super.entityQuery(includeSecureData: includeSecureData) + public func entityQuery(includeSecureData: Bool) -> SecurityFrameworkQuery { + var query = _entityQuery(includeSecureData: includeSecureData) query[kSecClass as String] = kSecClassInternetPassword diff --git a/Sources/Haversack/Entities/KeyEntity.swift b/Sources/Haversack/Entities/KeyEntity.swift index fcbc41c..581ccb1 100644 --- a/Sources/Haversack/Entities/KeyEntity.swift +++ b/Sources/Haversack/Entities/KeyEntity.swift @@ -1,10 +1,11 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation +@preconcurrency import Security /// Represents a public or private key in the keychain. -public class KeyEntity: KeychainStorable, KeychainPortable { +public struct KeyEntity: KeychainStorable, KeychainPortable { /// Uses the `SecKey` type to interface with the Security framework. public typealias SecurityFrameworkType = SecKey @@ -53,8 +54,8 @@ public class KeyEntity: KeychainStorable, KeychainPortable { /// Create an empty key entity public init() { } - public required init(from keychainItemRef: SecurityFrameworkType?, data: Data?, - attributes: [String: Any]?, persistentRef: Data?) { + public init(from keychainItemRef: SecurityFrameworkType?, data: Data?, + attributes: [String: Any]?, persistentRef: Data?) { reference = keychainItemRef keyData = data self.persistentRef = persistentRef diff --git a/Sources/Haversack/Entities/KeychainStorable.swift b/Sources/Haversack/Entities/KeychainStorable.swift index c293786..3d4921a 100644 --- a/Sources/Haversack/Entities/KeychainStorable.swift +++ b/Sources/Haversack/Entities/KeychainStorable.swift @@ -1,12 +1,12 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation /// Represents any item that is storable in the keychain. -public protocol KeychainStorable { +public protocol KeychainStorable: Equatable, Sendable { /// This should be one of the SecFoo types, such as `SecCertificate` or `SecKeychainItem` - associatedtype SecurityFrameworkType + associatedtype SecurityFrameworkType: Sendable /// The keychain item reference, if it has been returned. var reference: SecurityFrameworkType? { get } diff --git a/Sources/Haversack/Entities/PasswordBaseEntity.swift b/Sources/Haversack/Entities/PasswordBaseEntity.swift index 200443f..6bcbb8f 100644 --- a/Sources/Haversack/Entities/PasswordBaseEntity.swift +++ b/Sources/Haversack/Entities/PasswordBaseEntity.swift @@ -1,113 +1,84 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation /// Superclass of the ``GenericPasswordEntity`` and the ``InternetPasswordEntity`` that /// handles storage and minor processing of shared data fields. The `PasswordBaseEntity` is never /// instantiated on its own. -public class PasswordBaseEntity: KeychainStorable { -#if os(macOS) - /// The native Security framework type associated with `PasswordBaseEntity` - /// - /// On macOS uses the `SecKeychainItem` type to interface with the Security framework. - /// On iOS uses the [Data](https://developer.apple.com/documentation/Foundation/Data) - /// type to interface with the Security framework. - public typealias SecurityFrameworkType = SecKeychainItem -#else - /// The native Security framework type associated with `PasswordBaseEntity` - /// - /// On macOS uses the `SecKeychainItem` type to interface with the Security framework. - /// On iOS uses the [Data](https://developer.apple.com/documentation/Foundation/Data) - /// type to interface with the Security framework. - public typealias SecurityFrameworkType = Data -#endif - +public protocol PasswordBaseEntity: KeychainStorable { /// The keychain item reference, if it has been returned. - public var reference: SecurityFrameworkType? + var reference: SecurityFrameworkType? { get set } /// The persistent keychain item reference, if it has been returned. - public var persistentRef: Data? + var persistentRef: Data? { get set } /// When the item was created; read only. /// - Note: Uses `kSecAttrCreationDate` - private(set) var creationDate: Date? + var creationDate: Date? { get } /// When the item was last modified; read only. /// - Note: Uses `kSecAttrModificationDate` - private(set) var modificationDate: Date? + var modificationDate: Date? { get } /// The item's creator. /// - Note: Uses `kSecAttrCreator` - public var creator: Int? // FourCharCode + var creator: Int? { get set } // FourCharCode /// A description to store alongside the item. /// /// In Keychain Access this is the `Kind` field. /// - Note: Uses `kSecAttrDescription` - public var description: String? + var description: String? { get set } /// A comment to store alongside the item. /// /// In Keychain Access this is the `Comment` field. /// - Note: Uses `kSecAttrComment`. - public var comment: String? + var comment: String? { get set } /// User-defined group number for passwords /// - Note: Uses `kSecAttrType` - public var group: Int? // FourCharCode + var group: Int? { get set } // FourCharCode /// A user-visible label for the item. /// /// In Keychain Access this is the `Name` field. /// - Note: Uses `kSecAttrLabel` - public var label: String? + var label: String? { get set } /// Whether you want this to show up in Keychain Access. /// - Note: Uses `kSecAttrIsInvisible` - public var isInvisible: Bool? + var isInvisible: Bool? { get set } /// Useful if you want to never store the actual password, but still have a keychain item. - public var isNegative: Bool { - return passwordData == nil - } + var isNegative: Bool { get } /// The name of an account within a service associated with the password. /// /// In Keychain Access this is the `Account` field. /// - Note: Uses `kSecAttrAccount` - public var account: String? + var account: String? { get set } /// The actual password. /// /// If this is nil, when saving to the keychain the `kSecAttrIsNegative` is set to `true` instead. /// - Note: Uses `kSecValueData`. - public var passwordData: Data? - - /// Everything is nil with this constructor. - public init() { } - - public required init(from keychainItemRef: SecurityFrameworkType?, data: Data?, - attributes: [String: Any]?, persistentRef: Data?) { - reference = keychainItemRef - passwordData = data - self.persistentRef = persistentRef - - if let attrs = attributes { - creationDate = attrs[kSecAttrCreationDate as String] as? Date - modificationDate = attrs[kSecAttrModificationDate as String] as? Date - label = attrs[kSecAttrLabel as String] as? String - account = attrs[kSecAttrAccount as String] as? String - group = attrs[kSecAttrType as String] as? Int - comment = attrs[kSecAttrComment as String] as? String - description = attrs[kSecAttrDescription as String] as? String - creator = attrs[kSecAttrCreator as String] as? Int - } - } + var passwordData: Data? { get set } // NOTE: This function has a cyclomatic complexity of 11 instead of the allowed 10. // swiftlint:disable:next cyclomatic_complexity - public func entityQuery(includeSecureData: Bool) -> SecurityFrameworkQuery { + func entityQuery(includeSecureData: Bool) -> SecurityFrameworkQuery +} + +extension PasswordBaseEntity { + /// Useful if you want to never store the actual password, but still have a keychain item. + public var isNegative: Bool { + return passwordData == nil + } + + // swiftlint:disable:next identifier_name + func _entityQuery(includeSecureData: Bool) -> SecurityFrameworkQuery { var newQuery = SecurityFrameworkQuery() if let theReference = reference { diff --git a/Sources/Haversack/GenericPasswordConvertible.swift b/Sources/Haversack/GenericPasswordConvertible.swift index cc5e7be..03350da 100644 --- a/Sources/Haversack/GenericPasswordConvertible.swift +++ b/Sources/Haversack/GenericPasswordConvertible.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation import os.log @@ -7,7 +7,7 @@ import os.log /// Protocol to use with any type that can be converted to a [Data](https://developer.apple.com/documentation/Foundation/Data) representation. /// /// The type can be stored in the keychain as a generic password using the ``GenericPasswordEntity`` type. -public protocol GenericPasswordConvertible { +public protocol GenericPasswordConvertible: Sendable { /// A raw representation of the thing. var rawRepresentation: Data { get } @@ -23,7 +23,7 @@ extension GenericPasswordEntity { /// > Tip: In order to persist the value to the keychain, one of the Haversack `save()` methods should be called /// with the initialized ``GenericPasswordEntity``. /// - Parameter convertible: Any type that can be converted to plain [Data](https://developer.apple.com/documentation/Foundation/Data) - public convenience init(_ convertible: T) { + public init(_ convertible: T) { self.init(from: nil, data: convertible.rawRepresentation, attributes: nil, persistentRef: nil) } diff --git a/Sources/Haversack/Haversack+AsyncAwait.swift b/Sources/Haversack/Haversack+AsyncAwait.swift index e59059a..9f2681f 100644 --- a/Sources/Haversack/Haversack+AsyncAwait.swift +++ b/Sources/Haversack/Haversack+AsyncAwait.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation diff --git a/Sources/Haversack/Haversack.swift b/Sources/Haversack/Haversack.swift index fa52f8f..6beb22f 100644 --- a/Sources/Haversack/Haversack.swift +++ b/Sources/Haversack/Haversack.swift @@ -1,12 +1,13 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation +@preconcurrency import Security /// Represents a connection to a keychain. /// /// Contains keychain search functionality and the ability to add/update/delete items in the keychain. -public struct Haversack { +public struct Haversack: Sendable { /// The configuration that the Haversack was created with. public let configuration: HaversackConfiguration @@ -39,7 +40,7 @@ public struct Haversack { /// - completion: A function/block to be called when the search operation is completed. /// - result: The item or an error will be given to the completion handler. public func first(where query: T, completionQueue: OperationQueue? = nil, - completion: @escaping (_ result: Result) -> Void) { + completion: @escaping @Sendable (_ result: sending Result) -> Void) { configuration.serialQueue.async { let result: Result do { @@ -76,7 +77,7 @@ public struct Haversack { /// - completion: A function/block to be called when the search operation is completed. /// - result: An array of items or an error will be given to the completion handler. public func search(where query: T, completionQueue: OperationQueue? = nil, - completion: @escaping (_ result: Result<[T.Entity], Error>) -> Void) { + completion: @escaping @Sendable (_ result: sending Result<[T.Entity], Error>) -> Void) { configuration.serialQueue.async { let result: Result<[T.Entity], Error> do { @@ -121,7 +122,7 @@ public struct Haversack { /// - result: The original `item` that was saved or an error will be given to the completion handler. public func save(_ item: T, itemSecurity: ItemSecurity, updateExisting: Bool, completionQueue: OperationQueue? = nil, - completion: @escaping (_ result: Result) -> Void) { + completion: @escaping @Sendable (_ result: sending Result) -> Void) { configuration.serialQueue.async { let result: Result @@ -167,7 +168,7 @@ public struct Haversack { /// block; a `nil` represents no error. public func delete(_ item: T, treatNotFoundAsSuccess: Bool = true, completionQueue: OperationQueue? = nil, - completion: @escaping (_ error: Error?) -> Void) { + completion: @escaping @Sendable (_ error: sending Error?) -> Void) { configuration.serialQueue.async { let result: Error? @@ -212,7 +213,7 @@ public struct Haversack { /// block; a `nil` represents no error. public func delete(where query: T, treatNotFoundAsSuccess: Bool = true, completionQueue: OperationQueue? = nil, - completion: @escaping (_ error: Error?) -> Void) { + completion: @escaping @Sendable (_ error: sending Error?) -> Void) { configuration.serialQueue.async { let result: Error? @@ -296,7 +297,7 @@ public struct Haversack { /// - result: A new `SecKey` or an error. public func generateKey(fromConfig config: KeyGenerationConfig, itemSecurity: ItemSecurity, completionQueue: OperationQueue? = nil, - completion: @escaping (_ result: Result) -> Void) { + completion: @escaping @Sendable (_ result: sending Result) -> Void) { configuration.serialQueue.async { let result: Result @@ -395,8 +396,8 @@ public struct Haversack { } #endif - private func call(completionHandler: @escaping (_ result: Result) -> Void, - onQueue queue: OperationQueue?, with result: Result) { + private func call(completionHandler: @escaping @Sendable (_ result: sending Result) -> Void, + onQueue queue: OperationQueue?, with result: Result) { if let actualQueue = queue { actualQueue.addOperation { completionHandler(result) diff --git a/Sources/Haversack/HaversackConfiguration.swift b/Sources/Haversack/HaversackConfiguration.swift index d5f69cd..8997606 100644 --- a/Sources/Haversack/HaversackConfiguration.swift +++ b/Sources/Haversack/HaversackConfiguration.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation @@ -12,7 +12,7 @@ import Foundation /// /// Note that if a different queue is specified, all instances of ``Haversack/Haversack`` should be initialized with /// the same queue so that all keychain access is done atomically. -public struct HaversackConfiguration { +public struct HaversackConfiguration: Sendable { /// The `DispatchQueue` to use in order to serialize all keychain access /// /// If not otherwise specified, a default serial queue will be created with the label "com.jamf.haversack". diff --git a/Sources/Haversack/HaversackError.swift b/Sources/Haversack/HaversackError.swift index b8adb17..eb1efd5 100644 --- a/Sources/Haversack/HaversackError.swift +++ b/Sources/Haversack/HaversackError.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation diff --git a/Sources/Haversack/HaversackStrategy.swift b/Sources/Haversack/HaversackStrategy.swift index b4c39a6..3650309 100644 --- a/Sources/Haversack/HaversackStrategy.swift +++ b/Sources/Haversack/HaversackStrategy.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation import os.log @@ -17,7 +17,7 @@ import Security /// All item searches are logged at the `.info` level. Saving and deleting items are logged at /// the `.default` level. All errors are logged at the `.error` level with additional query details logged at /// the `.debug` level and marked as private. -open class HaversackStrategy { +open class HaversackStrategy: @unchecked Sendable { /// Create a new strategy object public init() { } @@ -243,7 +243,7 @@ open class HaversackStrategy { var outItems: CFArray? let status = SecItemImport(items as CFData, - configuration.fileNameOrExtension, + configuration.fileNameOrExtension as CFString?, &inputFormat, &itemType, configuration.secItemImportFlags, @@ -272,7 +272,7 @@ open class HaversackStrategy { } /// A simple type that looks into a keychain query to see what kind of data it is asking for. -private struct QueryContents { +private struct QueryContents: Sendable { let hasRef: Bool let hasData: Bool let hasPersistentRef: Bool diff --git a/Sources/Haversack/ImportExport/KeychainExportConfig.swift b/Sources/Haversack/ImportExport/KeychainExportConfig.swift index 94b5546..2f369f3 100644 --- a/Sources/Haversack/ImportExport/KeychainExportConfig.swift +++ b/Sources/Haversack/ImportExport/KeychainExportConfig.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation @@ -7,7 +7,7 @@ import Foundation /// Used with ``Haversack`` to export Keychain items /// /// Create a `KeychainExportConfig` and pass it to ``Haversack/Haversack/exportItems(_:config:)`` -public struct KeychainExportConfig { +public struct KeychainExportConfig: Sendable { // MARK: Public public init(outputFormat: SecExternalFormat) { self.outputFormat = outputFormat @@ -33,8 +33,8 @@ public struct KeychainExportConfig { switch strategy { case .promptUser(let prompt, let title): copy.keyImportExportFlags = .securePassphrase - copy.alertPrompt = prompt as CFString - copy.alertTitle = title as CFString + copy.alertPrompt = prompt + copy.alertTitle = title case .useProvided(let provider): copy.passphraseProvider = provider } @@ -51,11 +51,11 @@ public struct KeychainExportConfig { params.flags = keyImportExportFlags if let alertPrompt = alertPrompt { - params.alertPrompt = Unmanaged.passRetained(alertPrompt) + params.alertPrompt = Unmanaged.passRetained(alertPrompt as CFString) } if let alertTitle = alertTitle { - params.alertTitle = Unmanaged.passRetained(alertTitle) + params.alertTitle = Unmanaged.passRetained(alertTitle as CFString) } if let passphrase = passphraseProvider { @@ -66,10 +66,10 @@ public struct KeychainExportConfig { } // MARK: Private - private var alertPrompt: CFString? - private var alertTitle: CFString? + private var alertPrompt: String? + private var alertTitle: String? /// A function that provides the password to the keychain file. - private var passphraseProvider: (() -> String)? + private var passphraseProvider: (@Sendable () -> String)? private var keyImportExportFlags = SecKeyImportExportFlags() } #endif diff --git a/Sources/Haversack/ImportExport/KeychainImportConfig.swift b/Sources/Haversack/ImportExport/KeychainImportConfig.swift index 06c65d1..62260f7 100644 --- a/Sources/Haversack/ImportExport/KeychainImportConfig.swift +++ b/Sources/Haversack/ImportExport/KeychainImportConfig.swift @@ -1,14 +1,15 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation +@preconcurrency import Security #if os(macOS) /// Used with ``Haversack`` to import Keychain items /// /// Create a `KeychainImportConfig` and specify what type of keychain item you're importing, /// then pass it to ``Haversack/Haversack/importItems(_:config:)`` -public struct KeychainImportConfig { +public struct KeychainImportConfig: Sendable { public typealias ImportedEntity = T // MARK: Public @@ -21,7 +22,7 @@ public struct KeychainImportConfig { /// - Returns: A `KeychainImportConfig` struct public func fileNameOrExtension(_ nameOrExtension: String) -> Self { var copy = self - copy.fileNameOrExtension = nameOrExtension as CFString + copy.fileNameOrExtension = nameOrExtension return copy } @@ -152,8 +153,8 @@ public struct KeychainImportConfig { switch strategy { case .promptUser(let prompt, let title): copy.keyImportFlags.insert(.securePassphrase) - copy.alertPrompt = prompt as CFString - copy.alertTitle = title as CFString + copy.alertPrompt = prompt + copy.alertTitle = title case .useProvided(let provider): copy.passphraseProvider = provider } @@ -163,7 +164,7 @@ public struct KeychainImportConfig { // MARK: Internal // SecItemImport parameters - var fileNameOrExtension: CFString? + var fileNameOrExtension: String? var inputFormat: SecExternalFormat = .formatUnknown var itemType: SecExternalItemType = .itemTypeUnknown var secItemImportFlags = SecItemImportExportFlags() @@ -172,10 +173,10 @@ public struct KeychainImportConfig { // SecItemImportExportKeyParameters var accessRef: SecAccess? var keyUsage: KeyUsagePolicy? - var passphraseProvider: (() -> String)? + var passphraseProvider: (@Sendable () -> String)? var keyImportFlags = SecKeyImportExportFlags() - var alertPrompt: CFString? - var alertTitle: CFString? + var alertPrompt: String? + var alertTitle: String? var isExtractable: Bool? var isPermanent: Bool? var isSensitive: Bool? diff --git a/Sources/Haversack/ImportExport/KeychainPortable.swift b/Sources/Haversack/ImportExport/KeychainPortable.swift index ecfe16f..46d6a6d 100644 --- a/Sources/Haversack/ImportExport/KeychainPortable.swift +++ b/Sources/Haversack/ImportExport/KeychainPortable.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation diff --git a/Sources/Haversack/ImportExport/PassphraseStrategy.swift b/Sources/Haversack/ImportExport/PassphraseStrategy.swift index d78aa4d..24685b1 100644 --- a/Sources/Haversack/ImportExport/PassphraseStrategy.swift +++ b/Sources/Haversack/ImportExport/PassphraseStrategy.swift @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation -public enum PassphraseStrategy { +public enum PassphraseStrategy: Sendable { /// Prompt the user to enter the passphrase for the item being imported or exported /// /// - prompt: The prompt to display in the secure passphrase alert panel @@ -11,5 +11,5 @@ public enum PassphraseStrategy { case promptUser(prompt: String, title: String) /// Use the password returned by the specified closure instead of prompting the user - case useProvided(() -> String) + case useProvided(@Sendable () -> String) } diff --git a/Sources/Haversack/ImportExport/PrivateKeyImporting.swift b/Sources/Haversack/ImportExport/PrivateKeyImporting.swift index f86ee89..3c40efa 100644 --- a/Sources/Haversack/ImportExport/PrivateKeyImporting.swift +++ b/Sources/Haversack/ImportExport/PrivateKeyImporting.swift @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf #if os(macOS) import Foundation /// Represents a keychain entity that has a private key component -public protocol PrivateKeyImporting {} +public protocol PrivateKeyImporting: Sendable {} extension KeyEntity: PrivateKeyImporting {} diff --git a/Sources/Haversack/KeyGenerationConfig.swift b/Sources/Haversack/KeyGenerationConfig.swift index 0f06237..2ac557a 100644 --- a/Sources/Haversack/KeyGenerationConfig.swift +++ b/Sources/Haversack/KeyGenerationConfig.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation @@ -7,12 +7,29 @@ import Foundation /// /// Create a `KeyGenerationConfig` and then pass it to ``Haversack/Haversack/generateKey(fromConfig:itemSecurity:)-1r4ki`` /// or one of it's asynchronous variants. -public struct KeyGenerationConfig { +public struct KeyGenerationConfig: Sendable { + private let queryLock = NSLock() + private nonisolated(unsafe) var _query = SecurityFrameworkQuery() /// The keychain config query. /// /// You cannot manipulate this directly. Instead use the fluent methods such as ``labeled(_:)``, /// ``tagged(_:)``, and others in order to build up the key generation configuration. - public private(set) var query = SecurityFrameworkQuery() + public private(set) var query: SecurityFrameworkQuery { + @storageRestrictions(initializes: _query) + init { + _query = newValue + } + get { + queryLock.withLock { + _query + } + } + set { + queryLock.withLock { + _query = newValue + } + } + } /// Initializer for a key _not_ in the Secure Enclave. /// diff --git a/Sources/Haversack/Logs.swift b/Sources/Haversack/Logs.swift index a0a3ff6..ddd6562 100644 --- a/Sources/Haversack/Logs.swift +++ b/Sources/Haversack/Logs.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import os.log diff --git a/Sources/Haversack/Queries/CertificateBaseQuerying.swift b/Sources/Haversack/Queries/CertificateBaseQuerying.swift index cee4ab2..0823384 100644 --- a/Sources/Haversack/Queries/CertificateBaseQuerying.swift +++ b/Sources/Haversack/Queries/CertificateBaseQuerying.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation @@ -33,7 +33,7 @@ public protocol CertificateBaseQuerying: KeychainQuerying { /// /// Refer to the ``KeychainQuerying/stringMatching(options:)-7t0r4`` function and /// ``KeychainStringComparisonOptions`` enum for additional ways to modify the string matching. -public enum CertSubjectMatchType { +public enum CertSubjectMatchType: Sendable { /// The subject of the cert must contain the string, but may have any other prefix and/or suffix. /// /// Refer to the ``KeychainQuerying/stringMatching(options:)-7t0r4`` function and diff --git a/Sources/Haversack/Queries/CertificateQuery.swift b/Sources/Haversack/Queries/CertificateQuery.swift index d52caed..35d9e74 100644 --- a/Sources/Haversack/Queries/CertificateQuery.swift +++ b/Sources/Haversack/Queries/CertificateQuery.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation @@ -7,7 +7,24 @@ import Foundation /// /// Successful searches produce ``CertificateEntity`` objects. public struct CertificateQuery { - public var query: SecurityFrameworkQuery + private let queryLock = NSLock() + private nonisolated(unsafe) var _query: SecurityFrameworkQuery + public var query: SecurityFrameworkQuery { + @storageRestrictions(initializes: _query) + init { + _query = newValue + } + get { + queryLock.withLock { + _query + } + } + set { + queryLock.withLock { + _query = newValue + } + } + } /// Create an ``CertificateQuery`` instance /// - Parameter label: The keychain label of the item. Uses `kSecAttrLabel`. @@ -50,7 +67,7 @@ extension CertificateQuery: CertificateBaseQuerying { /// Specifies the type of a certificate. /// /// Based on `CSSM_CERT_TYPE`. Used with the `kSecAttrCertificateType` attribute of certificates. -public enum CertificateType: Int32 { +public enum CertificateType: Int32, Sendable { case unknown = 0 case x509v1 = 0x01 case x509v2 = 0x02 @@ -77,7 +94,7 @@ public enum CertificateType: Int32 { /// Specifies how a certificate is encoded. /// /// Based on `CSSM_CERT_ENCODING`. Used with the `kSecAttrCertificateEncoding` attribute of certificates. -public enum CertificateEncoding: Int32 { +public enum CertificateEncoding: Int32, Sendable { case unknown = 0 case custom = 0x01 case ber = 0x02 diff --git a/Sources/Haversack/Queries/GenericPasswordQuery.swift b/Sources/Haversack/Queries/GenericPasswordQuery.swift index 710e474..c3865c3 100644 --- a/Sources/Haversack/Queries/GenericPasswordQuery.swift +++ b/Sources/Haversack/Queries/GenericPasswordQuery.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation @@ -7,7 +7,24 @@ import Foundation /// /// Successful searches produce ``GenericPasswordEntity`` objects. public struct GenericPasswordQuery { - public var query: SecurityFrameworkQuery + private let queryLock = NSLock() + private nonisolated(unsafe) var _query: SecurityFrameworkQuery + public var query: SecurityFrameworkQuery { + @storageRestrictions(initializes: _query) + init { + _query = newValue + } + get { + queryLock.withLock { + _query + } + } + set { + queryLock.withLock { + _query = newValue + } + } + } /// Create a GenericPasswordQuery /// - Parameter service: The name of the service associated with the password diff --git a/Sources/Haversack/Queries/IdentityQuery.swift b/Sources/Haversack/Queries/IdentityQuery.swift index a6b9a48..6386667 100644 --- a/Sources/Haversack/Queries/IdentityQuery.swift +++ b/Sources/Haversack/Queries/IdentityQuery.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation import OrderedCollections @@ -9,7 +9,24 @@ import OrderedCollections /// An identity is a certificate plus a private key. /// Successful searches produce ``IdentityEntity`` objects. public struct IdentityQuery { - public var query: SecurityFrameworkQuery + private let queryLock = NSLock() + private nonisolated(unsafe) var _query: SecurityFrameworkQuery + public var query: SecurityFrameworkQuery { + @storageRestrictions(initializes: _query) + init { + _query = newValue + } + get { + queryLock.withLock { + _query + } + } + set { + queryLock.withLock { + _query = newValue + } + } + } /// Create an ``IdentityQuery`` instance /// - Parameter label: The keychain label of the item. Uses `kSecAttrLabel`. diff --git a/Sources/Haversack/Queries/InternetPasswordQuery.swift b/Sources/Haversack/Queries/InternetPasswordQuery.swift index 0508804..493544a 100644 --- a/Sources/Haversack/Queries/InternetPasswordQuery.swift +++ b/Sources/Haversack/Queries/InternetPasswordQuery.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation @@ -7,7 +7,24 @@ import Foundation /// /// Successful searches produce ``InternetPasswordEntity`` objects. public struct InternetPasswordQuery { - public var query: SecurityFrameworkQuery + private let queryLock = NSLock() + private nonisolated(unsafe) var _query: SecurityFrameworkQuery + public var query: SecurityFrameworkQuery { + @storageRestrictions(initializes: _query) + init { + _query = newValue + } + get { + queryLock.withLock { + _query + } + } + set { + queryLock.withLock { + _query = newValue + } + } + } /// Create an ``InternetPasswordQuery`` /// - Parameter server: The domain name or IP address of a server associated with the password diff --git a/Sources/Haversack/Queries/KeyBaseQuerying.swift b/Sources/Haversack/Queries/KeyBaseQuerying.swift index 3a25e20..34e7193 100644 --- a/Sources/Haversack/Queries/KeyBaseQuerying.swift +++ b/Sources/Haversack/Queries/KeyBaseQuerying.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation @@ -39,7 +39,7 @@ extension KeyBaseQuerying { } /// Encapsulates how cryptographic keys may be used. -public struct KeyUsagePolicy: OptionSet { +public struct KeyUsagePolicy: OptionSet, Sendable { public let rawValue: Int public init(rawValue: Int) { diff --git a/Sources/Haversack/Queries/KeyQuery.swift b/Sources/Haversack/Queries/KeyQuery.swift index 41b2c8d..7793b10 100644 --- a/Sources/Haversack/Queries/KeyQuery.swift +++ b/Sources/Haversack/Queries/KeyQuery.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation @@ -9,7 +9,24 @@ import Foundation public struct KeyQuery: KeyBaseQuerying { public typealias Entity = KeyEntity - public var query: SecurityFrameworkQuery + private let queryLock = NSLock() + private nonisolated(unsafe) var _query: SecurityFrameworkQuery + public var query: SecurityFrameworkQuery { + @storageRestrictions(initializes: _query) + init { + _query = newValue + } + get { + queryLock.withLock { + _query + } + } + set { + queryLock.withLock { + _query = newValue + } + } + } /// Create a ``KeyQuery`` instance /// - Parameter label: The keychain label of the item. Uses `kSecAttrLabel`. @@ -109,7 +126,7 @@ public struct KeyQuery: KeyBaseQuerying { } /// Encapsulates the cryptographic key's class (public, private, or symmetric). -public enum KeyClass { +public enum KeyClass: Sendable { /// Represents a private key case `private` /// Represents a public key @@ -117,18 +134,18 @@ public enum KeyClass { /// Represents a symmetric encryption/decryption key case symmetric - private static let translation: [CFString: KeyClass] = [ - kSecAttrKeyClassPrivate: .private, - kSecAttrKeyClassPublic: .public, - kSecAttrKeyClassSymmetric: .symmetric + private static let translation: [String: KeyClass] = [ + kSecAttrKeyClassPrivate as String: .private, + kSecAttrKeyClassPublic as String: .public, + kSecAttrKeyClassSymmetric as String: .symmetric ] static func make(from securityFrameworkValue: CFString) -> KeyClass? { - return translation[securityFrameworkValue] + return translation[securityFrameworkValue as String] } func securityFrameworkValue() -> CFString { - return Self.translation.first(where: { $1 == self })!.key + return Self.translation.first(where: { $1 == self })!.key as CFString } } diff --git a/Sources/Haversack/Queries/KeychainQueryOptions.swift b/Sources/Haversack/Queries/KeychainQueryOptions.swift index 4bf7322..7c9635c 100644 --- a/Sources/Haversack/Queries/KeychainQueryOptions.swift +++ b/Sources/Haversack/Queries/KeychainQueryOptions.swift @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation /// Options for what type of data to return from a keychain query. -public struct KeychainDataOptions: OptionSet { +public struct KeychainDataOptions: OptionSet, Sendable { public let rawValue: Int public init(rawValue: Int) { @@ -30,7 +30,7 @@ public struct KeychainDataOptions: OptionSet { } /// Options for how to compare strings in keychain queries. -public struct KeychainStringComparisonOptions: OptionSet { +public struct KeychainStringComparisonOptions: OptionSet, Sendable { public let rawValue: Int public init(rawValue: Int) { diff --git a/Sources/Haversack/Queries/KeychainQuerying.swift b/Sources/Haversack/Queries/KeychainQuerying.swift index 126b86e..98f8a5d 100644 --- a/Sources/Haversack/Queries/KeychainQuerying.swift +++ b/Sources/Haversack/Queries/KeychainQuerying.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation @@ -7,7 +7,7 @@ import Foundation public typealias SecurityFrameworkQuery = [String: Any] /// Base protocol for the fluent interface to search for a keychain item -public protocol KeychainQuerying { +public protocol KeychainQuerying: Sendable { /// The type of entity that is created when searching the keychain with this query. associatedtype Entity: KeychainStorable diff --git a/Sources/Haversack/Queries/PasswordBaseQuerying.swift b/Sources/Haversack/Queries/PasswordBaseQuerying.swift index a99bba2..ac0b2ab 100644 --- a/Sources/Haversack/Queries/PasswordBaseQuerying.swift +++ b/Sources/Haversack/Queries/PasswordBaseQuerying.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation diff --git a/Sources/Haversack/Security/ItemSecurity.swift b/Sources/Haversack/Security/ItemSecurity.swift index 7ece9c4..e8ff904 100644 --- a/Sources/Haversack/Security/ItemSecurity.swift +++ b/Sources/Haversack/Security/ItemSecurity.swift @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation /// Specify the security of the keychain item -public struct ItemSecurity { +public struct ItemSecurity: Sendable { /// The Haversack standard security for keychain items. /// @@ -12,11 +12,28 @@ public struct ItemSecurity { /// The item is not part of any app group or keychain group. public static let standard = ItemSecurity().retrievableNoThrow(when: .simple(.unlockedThisDeviceOnly)) + private let queryLock = NSLock() + private nonisolated(unsafe) var _query: SecurityFrameworkQuery /// The keychain query. **Do not** manipulate this directly. /// /// You should not manipulate this directly. Instead use the fluent methods such as ``containedIn(appGroup:)`` /// and ``retrievable(when:)`` to build up the query. - public var query: SecurityFrameworkQuery + public var query: SecurityFrameworkQuery { + @storageRestrictions(initializes: _query) + init { + _query = newValue + } + get { + queryLock.withLock { + _query + } + } + set { + queryLock.withLock { + _query = newValue + } + } + } /// Construct an empty ``ItemSecurity`` /// diff --git a/Sources/Haversack/Security/KeychainFile.swift b/Sources/Haversack/Security/KeychainFile.swift index 5a3f265..f2bb84d 100644 --- a/Sources/Haversack/Security/KeychainFile.swift +++ b/Sources/Haversack/Security/KeychainFile.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation import os.log @@ -10,7 +10,7 @@ import os.log public typealias FilePath = String /// A function/block that provides the unencrypted plain `String` password to a keychain file on macOS. -public typealias KeychainPasswordProvider = (_ keychainPath: FilePath) -> String +public typealias KeychainPasswordProvider = @Sendable (_ keychainPath: FilePath) -> String /// Represents a legacy custom keychain file to use with the Security framework. /// @@ -23,7 +23,7 @@ public typealias KeychainPasswordProvider = (_ keychainPath: FilePath) -> String /// Opening, creating, or deleting a custom keychain file is logged at `.default` level. /// Locking/unlocking the keychain file is logged at the `.info` level. /// All errors are logged at the `.error` level. -public class KeychainFile { +public final class KeychainFile: Sendable { /// The path to the system keychain that contains the globally trusted root CA certificates. static let rootCertificatesKeychainPath = "/System/Library/Keychains/SystemRootCertificates.keychain" @@ -59,8 +59,25 @@ public class KeychainFile { /// A function that provides the password to the keychain file. let passwordProvider: KeychainPasswordProvider? + private let referenceLock = NSLock() + private nonisolated(unsafe) var _reference: SecKeychain? /// A reference to the opened keychain. - var reference: SecKeychain? + var reference: SecKeychain? { + @storageRestrictions(initializes: _reference) + init { + _reference = newValue + } + get { + referenceLock.withLock { + _reference + } + } + set { + referenceLock.withLock { + _reference = newValue + } + } + } /// Create an object representing a custom keychain file. /// - Parameters: diff --git a/Sources/Haversack/Security/KeychainItemRetrievability.swift b/Sources/Haversack/Security/KeychainItemRetrievability.swift index 47729c9..934c217 100644 --- a/Sources/Haversack/Security/KeychainItemRetrievability.swift +++ b/Sources/Haversack/Security/KeychainItemRetrievability.swift @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation /// A typesafe way to specify when a keychain item is available for use. -public enum KeychainItemRetrievability: Equatable { +public enum KeychainItemRetrievability: Equatable, Sendable { /// Specify when the keychain item is available /// /// See [Apple: Restricting Keychain Item Accessibility](https://developer.apple.com/documentation/security/keychain_services/keychain_items/restricting_keychain_item_accessibility) diff --git a/Sources/Haversack/Security/RetrievabilityLevel.swift b/Sources/Haversack/Security/RetrievabilityLevel.swift index 534a5c6..82e4c5d 100644 --- a/Sources/Haversack/Security/RetrievabilityLevel.swift +++ b/Sources/Haversack/Security/RetrievabilityLevel.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation @@ -9,7 +9,7 @@ import Foundation /// the accessibility APIs that are used for user experience accommodations. /// - Note: This is a wrapper around the `kSecAttrAccessible...` constants. Haversack does not /// support the deprecated "always available" levels because they provide no security. -public enum RetrievabilityLevel: CaseIterable, Equatable { +public enum RetrievabilityLevel: CaseIterable, Equatable, Sendable { /// Retrievable when the device is unlocked; will synchronize to iCloud Keychain and other devices. /// /// Recommended for standard user applications without background processing. diff --git a/Sources/HaversackCryptoKit/GenericPasswordConvertible+CryptoKit.swift b/Sources/HaversackCryptoKit/GenericPasswordConvertible+CryptoKit.swift index 5e41fac..7daf14a 100644 --- a/Sources/HaversackCryptoKit/GenericPasswordConvertible+CryptoKit.swift +++ b/Sources/HaversackCryptoKit/GenericPasswordConvertible+CryptoKit.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import CryptoKit import Foundation diff --git a/Sources/HaversackCryptoKit/SecKeyConvertible.swift b/Sources/HaversackCryptoKit/SecKeyConvertible.swift index 023c429..6a4b311 100644 --- a/Sources/HaversackCryptoKit/SecKeyConvertible.swift +++ b/Sources/HaversackCryptoKit/SecKeyConvertible.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import CryptoKit import Foundation @@ -9,7 +9,7 @@ import os.log // MARK: - SecKeyConvertible /// Protocol for CryptoKit key types that can be converted to `SecKey` representations. -public protocol SecKeyConvertible { +public protocol SecKeyConvertible: Sendable { /// Creates a key from an X9.63 representation. init(x963Representation: Bytes) throws where Bytes: ContiguousBytes @@ -41,7 +41,7 @@ public extension KeyEntity { /// with the initialized ``KeyEntity``. /// - Parameter key: A CryptoKit elliptic key /// - Throws: Throws an `NSError` if there is a problem converting to a `SecKey` - convenience init(_ key: T) throws { + init(_ key: T) throws { let attributes = [kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom, kSecAttrKeyClass: kSecAttrKeyClassPrivate] as [String: Any] var possibleError: Unmanaged? diff --git a/Sources/HaversackMock/HaversackEphemeralStrategy+mocking.swift b/Sources/HaversackMock/HaversackEphemeralStrategy+mocking.swift index 80f1783..05f1eff 100644 --- a/Sources/HaversackMock/HaversackEphemeralStrategy+mocking.swift +++ b/Sources/HaversackMock/HaversackEphemeralStrategy+mocking.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Haversack import Security diff --git a/Sources/HaversackMock/HaversackEphemeralStrategy.swift b/Sources/HaversackMock/HaversackEphemeralStrategy.swift index 591e3ca..29ffe40 100644 --- a/Sources/HaversackMock/HaversackEphemeralStrategy.swift +++ b/Sources/HaversackMock/HaversackEphemeralStrategy.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation import Haversack @@ -7,7 +7,7 @@ import Haversack /// A strategy which uses a simple dictionary to import, export, search, store, and delete data instead of hitting an actual keychain. /// /// The keys of the ``mockData`` dictionary are calculated from the queries that are sent through Haversack. -open class HaversackEphemeralStrategy: HaversackStrategy { +open class HaversackEphemeralStrategy: HaversackStrategy, @unchecked Sendable { /// The dictionary that is used for storage of keychain items /// /// Items can be added into or removed from this dictionary manually. diff --git a/Tests/HaversackTests/CertificateIntegrationTests.swift b/Tests/HaversackTests/CertificateIntegrationTests.swift index fb9aabf..8348275 100644 --- a/Tests/HaversackTests/CertificateIntegrationTests.swift +++ b/Tests/HaversackTests/CertificateIntegrationTests.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import XCTest import Haversack diff --git a/Tests/HaversackTests/Data+X501NameTests.swift b/Tests/HaversackTests/Data+X501NameTests.swift index c460487..23c68ca 100644 --- a/Tests/HaversackTests/Data+X501NameTests.swift +++ b/Tests/HaversackTests/Data+X501NameTests.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import OrderedCollections import XCTest diff --git a/Tests/HaversackTests/EphemeralStrategyTests.swift b/Tests/HaversackTests/EphemeralStrategyTests.swift index 1ed23cc..6caf7fc 100644 --- a/Tests/HaversackTests/EphemeralStrategyTests.swift +++ b/Tests/HaversackTests/EphemeralStrategyTests.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import XCTest import Haversack @@ -20,7 +20,7 @@ final class EphemeralStrategyTests: XCTestCase { .matching(account: "luke") .returning(.reference) - let expectedEntity = InternetPasswordEntity() + var expectedEntity = InternetPasswordEntity() expectedEntity.protocol = .appleTalk try haversack.setSearchFirstMock(where: pwQuery, mockValue: expectedEntity) @@ -39,7 +39,7 @@ final class EphemeralStrategyTests: XCTestCase { let pwQuery = GenericPasswordQuery(service: testService) .returning(.reference) - let expectedEntity = GenericPasswordEntity() + var expectedEntity = GenericPasswordEntity() expectedEntity.service = testService try haversack.setSearchFirstMock(where: pwQuery, mockValue: expectedEntity) @@ -74,7 +74,7 @@ final class EphemeralStrategyTests: XCTestCase { let pwQuery = GenericPasswordQuery(service: testService) .returning(.reference) - let expectedEntity = GenericPasswordEntity() + var expectedEntity = GenericPasswordEntity() expectedEntity.service = testService try haversack.setSearchFirstMock(where: pwQuery, mockValue: expectedEntity) @@ -94,7 +94,7 @@ final class EphemeralStrategyTests: XCTestCase { let pwQuery = GenericPasswordQuery(service: testService) .returning(.reference) - let expectedEntity = GenericPasswordEntity() + var expectedEntity = GenericPasswordEntity() expectedEntity.service = testService try haversack.setSearchMock(where: pwQuery, mockValue: [expectedEntity]) @@ -110,7 +110,7 @@ final class EphemeralStrategyTests: XCTestCase { func testSetSaveMock() throws { // Given - let mockEntity = GenericPasswordEntity() + var mockEntity = GenericPasswordEntity() let testService = "unit.test" mockEntity.service = testService diff --git a/Tests/HaversackTests/GenericPasswordConvertibleTests.swift b/Tests/HaversackTests/GenericPasswordConvertibleTests.swift index a293836..ea8d373 100644 --- a/Tests/HaversackTests/GenericPasswordConvertibleTests.swift +++ b/Tests/HaversackTests/GenericPasswordConvertibleTests.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import XCTest import Haversack @@ -53,12 +53,12 @@ final class GenericPasswordConvertibleTests: XCTestCase { let testInstance = MyTestType(aString: "unit test", anInt: 7) // when - let entity = GenericPasswordEntity(testInstance) + var entity = GenericPasswordEntity(testInstance) entity.service = "testing" let actual = try haversack.save(entity, itemSecurity: .standard, updateExisting: false) // then - XCTAssertIdentical(actual, entity) + XCTAssertEqual(actual, entity) let insertedData = try XCTUnwrap(strategy.mockData["classgenppdmnakusvcetestv_Data"] as? SecurityFrameworkQuery) XCTAssertEqual(insertedData.count, 4) XCTAssertEqual(insertedData[kSecClass as String] as? String, kSecClassGenericPassword as String) @@ -68,7 +68,7 @@ final class GenericPasswordConvertibleTests: XCTestCase { func testMyTestTypeCanLoad() throws { // given - let expectedEntity = GenericPasswordEntity() + var expectedEntity = GenericPasswordEntity() expectedEntity.service = "test_load" expectedEntity.passwordData = #"{"aString":"yes we can","anInt":65}"#.data(using: .utf8) strategy.mockData["classgenpm_Limitm_Lir_Datasvcetest"] = expectedEntity @@ -87,7 +87,7 @@ final class GenericPasswordConvertibleTests: XCTestCase { func testSecondTypeCanLoad() throws { // given - let expectedEntity = GenericPasswordEntity() + var expectedEntity = GenericPasswordEntity() expectedEntity.service = "test_load_2" expectedEntity.passwordData = "Hello".data(using: .utf8) strategy.mockData["classgenpm_Limitm_Lir_Datasvcetest"] = expectedEntity @@ -104,7 +104,7 @@ final class GenericPasswordConvertibleTests: XCTestCase { func testAttemptedLoadOfWrongTypeThrows() throws { // given - let expectedEntity = GenericPasswordEntity() + var expectedEntity = GenericPasswordEntity() expectedEntity.service = "test_load" expectedEntity.passwordData = #"{"aString":"yes we can","anInt":65}"#.data(using: .utf8) strategy.mockData["classgenpm_Limitm_Lir_Datasvcetest"] = expectedEntity diff --git a/Tests/HaversackTests/GenericPasswordIntegrationTests.swift b/Tests/HaversackTests/GenericPasswordIntegrationTests.swift index facb6d2..42cddd2 100644 --- a/Tests/HaversackTests/GenericPasswordIntegrationTests.swift +++ b/Tests/HaversackTests/GenericPasswordIntegrationTests.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import XCTest import Haversack @@ -25,7 +25,7 @@ final class GenericPasswordIntegrationTests: XCTestCase { func testSaveOverExistingGenericPassword() throws { // given let customData = try XCTUnwrap("some test".data(using: .utf8)) - let givenPassword = GenericPasswordEntity() + var givenPassword = GenericPasswordEntity() givenPassword.customData = customData givenPassword.passwordData = "top secret".data(using: .utf8) try haversack.save(givenPassword, itemSecurity: .standard, updateExisting: false) @@ -35,7 +35,7 @@ final class GenericPasswordIntegrationTests: XCTestCase { } // when - we try to overwrite the password with a new value. - let newPassword = GenericPasswordEntity() + var newPassword = GenericPasswordEntity() newPassword.customData = customData newPassword.passwordData = "new secret".data(using: .utf8) try haversack.save(newPassword, itemSecurity: .standard, updateExisting: true) diff --git a/Tests/HaversackTests/HaversackAsyncAwaitTests.swift b/Tests/HaversackTests/HaversackAsyncAwaitTests.swift index ab5c2f1..c2f0268 100644 --- a/Tests/HaversackTests/HaversackAsyncAwaitTests.swift +++ b/Tests/HaversackTests/HaversackAsyncAwaitTests.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation import Haversack @@ -13,7 +13,7 @@ final class HaversackAsyncAwaitTests: XCTestCase { private let sampleDomain = "example.com" private let sampleEntity: InternetPasswordEntity = { - let entity = InternetPasswordEntity() + var entity = InternetPasswordEntity() entity.server = "example.com" return entity }() diff --git a/Tests/HaversackTests/HaversackTests.swift b/Tests/HaversackTests/HaversackTests.swift index 75e342e..3568e9c 100644 --- a/Tests/HaversackTests/HaversackTests.swift +++ b/Tests/HaversackTests/HaversackTests.swift @@ -1,23 +1,23 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation import Haversack import HaversackMock import XCTest -final class HaversackTests: XCTestCase { +final class HaversackTests: XCTestCase, @unchecked Sendable { var haversack: Haversack! var strategy: HaversackEphemeralStrategy! private let sampleDomain = "example.com" private let sampleEntity: InternetPasswordEntity = { - let entity = InternetPasswordEntity() + var entity = InternetPasswordEntity() entity.server = "example.com" return entity }() private let sampleKey: KeyEntity = { - let entity = KeyEntity() + var entity = KeyEntity() entity.label = "example.com" entity.keySizeInBits = 2048 return entity diff --git a/Tests/HaversackTests/IdentityIntegrationTests.swift b/Tests/HaversackTests/IdentityIntegrationTests.swift index 6fb51c3..054a40c 100644 --- a/Tests/HaversackTests/IdentityIntegrationTests.swift +++ b/Tests/HaversackTests/IdentityIntegrationTests.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import XCTest import Haversack diff --git a/Tests/HaversackTests/InternetPasswordIntegrationTests.swift b/Tests/HaversackTests/InternetPasswordIntegrationTests.swift index 9719072..0e5a7a5 100644 --- a/Tests/HaversackTests/InternetPasswordIntegrationTests.swift +++ b/Tests/HaversackTests/InternetPasswordIntegrationTests.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import XCTest import Haversack @@ -31,7 +31,7 @@ final class InternetPasswordIntegrationTests: XCTestCase { try keychainFile.attemptToOpenOrCreate() // then - let newPassword = InternetPasswordEntity() + var newPassword = InternetPasswordEntity() newPassword.authenticationType = .HTTPDigest newPassword.account = "Chewbacca" newPassword.path = "falcon/holotable" @@ -45,7 +45,7 @@ final class InternetPasswordIntegrationTests: XCTestCase { XCTAssertNotNil(savedPassword) - let genericPassword = GenericPasswordEntity() + var genericPassword = GenericPasswordEntity() genericPassword.account = "Mac Custom File tests" genericPassword.service = "General Han Solo" genericPassword.comment = "You know, sometimes I amaze myself" @@ -106,7 +106,7 @@ final class InternetPasswordIntegrationTests: XCTestCase { .matching(account: "Chewbacca") .returning(.attributes) let queryFinished = expectation(description: "search finished") - var queryResult: InternetPasswordEntity? + nonisolated(unsafe) var queryResult: InternetPasswordEntity? // when haversack.first(where: pwQuery, completionQueue: .main) { (result) in @@ -130,7 +130,7 @@ final class InternetPasswordIntegrationTests: XCTestCase { func testAddInternetPW() throws { // given - let newPassword = InternetPasswordEntity() + var newPassword = InternetPasswordEntity() newPassword.protocol = .HTTPS newPassword.server = "testing.example.com" newPassword.account = "mine too" @@ -151,7 +151,7 @@ final class InternetPasswordIntegrationTests: XCTestCase { func testUpdateInternetPWExistingThrows() throws { // given - let newPassword = InternetPasswordEntity() + var newPassword = InternetPasswordEntity() newPassword.protocol = .HTTPS newPassword.server = "update.example.com" newPassword.account = "mine too" @@ -166,7 +166,7 @@ final class InternetPasswordIntegrationTests: XCTestCase { } // when - let attemptToUpdate = InternetPasswordEntity() + var attemptToUpdate = InternetPasswordEntity() attemptToUpdate.protocol = .HTTPS attemptToUpdate.server = "update.example.com" attemptToUpdate.account = "mine too" @@ -180,7 +180,7 @@ final class InternetPasswordIntegrationTests: XCTestCase { func testUpdateInternetPW() throws { // given - let newPassword = InternetPasswordEntity() + var newPassword = InternetPasswordEntity() newPassword.protocol = .HTTPS newPassword.server = "update.example.com" newPassword.account = "mine too" @@ -194,7 +194,7 @@ final class InternetPasswordIntegrationTests: XCTestCase { try? haversack.delete(savedPassword) } - let attemptToUpdate = InternetPasswordEntity() + var attemptToUpdate = InternetPasswordEntity() attemptToUpdate.protocol = .HTTPS attemptToUpdate.server = "update.example.com" attemptToUpdate.account = "mine too" @@ -214,7 +214,7 @@ final class InternetPasswordIntegrationTests: XCTestCase { func testInternetPWMatchLabelFirst() throws { // given - let newPassword = InternetPasswordEntity() + var newPassword = InternetPasswordEntity() newPassword.label = "The test label" newPassword.passwordData = "top secret".data(using: .utf8) // Use login keychain @@ -238,7 +238,7 @@ final class InternetPasswordIntegrationTests: XCTestCase { func testInternetPWMatchLabelSearch() throws { // given - let newPassword = InternetPasswordEntity() + var newPassword = InternetPasswordEntity() newPassword.label = "The test label" newPassword.passwordData = "top secret".data(using: .utf8) // Use login keychain @@ -263,7 +263,7 @@ final class InternetPasswordIntegrationTests: XCTestCase { func testInternetPWFirstForDataWorks() throws { // given - let newPassword = InternetPasswordEntity() + var newPassword = InternetPasswordEntity() newPassword.label = "The test label" newPassword.passwordData = "top secret".data(using: .utf8) // Use login keychain diff --git a/Tests/HaversackTests/KeyGenerationConfigTests.swift b/Tests/HaversackTests/KeyGenerationConfigTests.swift index 383b671..593d792 100644 --- a/Tests/HaversackTests/KeyGenerationConfigTests.swift +++ b/Tests/HaversackTests/KeyGenerationConfigTests.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import XCTest import Haversack diff --git a/Tests/HaversackTests/KeyGenerationIntegrationTests.swift b/Tests/HaversackTests/KeyGenerationIntegrationTests.swift index 4c4ec99..70c4615 100644 --- a/Tests/HaversackTests/KeyGenerationIntegrationTests.swift +++ b/Tests/HaversackTests/KeyGenerationIntegrationTests.swift @@ -1,7 +1,8 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import XCTest +@preconcurrency import Security import Haversack #if os(macOS) @@ -54,7 +55,7 @@ final class KeyGenerationIntegrationTests: XCTestCase { .labeled("Async Key") .tagged(theTag) let keyGenerated = expectation(description: "new key has been generated") - var newKey: SecKey? + nonisolated(unsafe) var newKey: SecKey? // when haversack.generateKey(fromConfig: keyInfo, itemSecurity: .standard, diff --git a/Tests/HaversackTests/KeychainExportConfigTests.swift b/Tests/HaversackTests/KeychainExportConfigTests.swift index bd79c0d..3a735a4 100644 --- a/Tests/HaversackTests/KeychainExportConfigTests.swift +++ b/Tests/HaversackTests/KeychainExportConfigTests.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import XCTest @testable import Haversack diff --git a/Tests/HaversackTests/KeychainExportIntegrationTests.swift b/Tests/HaversackTests/KeychainExportIntegrationTests.swift index 6143896..e5df889 100644 --- a/Tests/HaversackTests/KeychainExportIntegrationTests.swift +++ b/Tests/HaversackTests/KeychainExportIntegrationTests.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import XCTest import Haversack diff --git a/Tests/HaversackTests/KeychainFileIntegrationTests.swift b/Tests/HaversackTests/KeychainFileIntegrationTests.swift index e85fb87..4f96e05 100644 --- a/Tests/HaversackTests/KeychainFileIntegrationTests.swift +++ b/Tests/HaversackTests/KeychainFileIntegrationTests.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation @@ -22,7 +22,7 @@ final class KeychainFileIntegrationTests: XCTestCase { let config = HaversackConfiguration(keychain: keychainFile) let haversack = Haversack(configuration: config) - let newPassword = InternetPasswordEntity() + var newPassword = InternetPasswordEntity() newPassword.server = "test.example.com" newPassword.label = "unit test password" newPassword.passwordData = "top secret".data(using: .utf8) diff --git a/Tests/HaversackTests/KeychainImportConfigTests.swift b/Tests/HaversackTests/KeychainImportConfigTests.swift index 1c1c1c3..fc922b7 100644 --- a/Tests/HaversackTests/KeychainImportConfigTests.swift +++ b/Tests/HaversackTests/KeychainImportConfigTests.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import XCTest @testable import Haversack diff --git a/Tests/HaversackTests/KeychainImportIntegrationTests.swift b/Tests/HaversackTests/KeychainImportIntegrationTests.swift index bf42c9f..9a0a5e0 100644 --- a/Tests/HaversackTests/KeychainImportIntegrationTests.swift +++ b/Tests/HaversackTests/KeychainImportIntegrationTests.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import XCTest import Haversack diff --git a/Tests/HaversackTests/QueryCertificateTests.swift b/Tests/HaversackTests/QueryCertificateTests.swift index 23b0d3f..2e8ce19 100644 --- a/Tests/HaversackTests/QueryCertificateTests.swift +++ b/Tests/HaversackTests/QueryCertificateTests.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import XCTest import Haversack diff --git a/Tests/HaversackTests/QueryIdentityTests.swift b/Tests/HaversackTests/QueryIdentityTests.swift index 5f6b3f8..a76e46a 100644 --- a/Tests/HaversackTests/QueryIdentityTests.swift +++ b/Tests/HaversackTests/QueryIdentityTests.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import XCTest import Haversack diff --git a/Tests/HaversackTests/QueryKeyTests.swift b/Tests/HaversackTests/QueryKeyTests.swift index bffb921..ca9b7ff 100644 --- a/Tests/HaversackTests/QueryKeyTests.swift +++ b/Tests/HaversackTests/QueryKeyTests.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import XCTest import Haversack diff --git a/Tests/HaversackTests/QueryPasswordTests.swift b/Tests/HaversackTests/QueryPasswordTests.swift index 90675b0..ac5b5d3 100644 --- a/Tests/HaversackTests/QueryPasswordTests.swift +++ b/Tests/HaversackTests/QueryPasswordTests.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import XCTest import Haversack diff --git a/Tests/HaversackTests/SystemKeychainIntegrationTests.swift b/Tests/HaversackTests/SystemKeychainIntegrationTests.swift index 0d80f6b..99ebc75 100644 --- a/Tests/HaversackTests/SystemKeychainIntegrationTests.swift +++ b/Tests/HaversackTests/SystemKeychainIntegrationTests.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import XCTest import Haversack @@ -25,7 +25,7 @@ final class SystemKeychainIntegrationTests: XCTestCase { func testSearchSystemKeychainMock() throws { // given let testService = "system.pw" - let expectedEntity = GenericPasswordEntity() + var expectedEntity = GenericPasswordEntity() expectedEntity.service = testService strategy.mockData["classgenpm_Limitm_Lim_SearchListr_Refsvcesyst"] = expectedEntity @@ -42,7 +42,7 @@ final class SystemKeychainIntegrationTests: XCTestCase { func testSaveToSystemKeychainMock() throws { // given - let aPassword = GenericPasswordEntity() + var aPassword = GenericPasswordEntity() aPassword.service = "A password" aPassword.passwordData = "super secret".data(using: .utf8) diff --git a/Tests/HaversackTests/XCTestCase+TestResources.swift b/Tests/HaversackTests/XCTestCase+TestResources.swift index b7f8a30..ba05050 100644 --- a/Tests/HaversackTests/XCTestCase+TestResources.swift +++ b/Tests/HaversackTests/XCTestCase+TestResources.swift @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright 2023, Jamf +// Copyright 2026, Jamf import Foundation import XCTest @@ -10,7 +10,7 @@ extension XCTestCase { /// - named: The full name of the file to load. For example `TestData.xml` /// - relativeToPath: Filled in automatically by the compiler to find the test file on disk. /// - Returns: The URL of the file within the test bundle; or nil if the test file cannot be found. - func getURLForTestResource(named: String, relativeToPath: StaticString = #file) -> URL { + func getURLForTestResource(named: String, relativeToPath: StaticString = #filePath) -> URL { let path = URL(fileURLWithPath: "\(relativeToPath)") let testURL = path.deletingLastPathComponent().appendingPathComponent("TestResources").appendingPathComponent(named) // Causes a wait of 0.002 seconds; why this is important I don't know.