Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions .github/workflows/swift.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: MIT
# Copyright 2024, Jamf
# Copyright 2026, Jamf

name: UnitTests

Expand All @@ -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
2 changes: 1 addition & 1 deletion .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -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
Expand Down
10 changes: 5 additions & 5 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 8 additions & 7 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -10,7 +10,7 @@ let package = Package(
.macOS(.v10_13),
.iOS(.v12),
.tvOS(.v12),
.visionOS(.v1),
.visionOS(.v1),
.watchOS(.v5)
],
products: [
Expand All @@ -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")]
)
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -145,3 +145,4 @@ Before submitting your pull request, please do the following:

- Kyle Hammond
- Jacob Hearst
- Michael Link
10 changes: 5 additions & 5 deletions Sources/Haversack/Entities/CertificateEntity.swift
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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<String, String>?

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
Expand Down Expand Up @@ -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)
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/Haversack/Entities/Data+X501Name.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright 2023, Jamf
// Copyright 2026, Jamf

import Foundation
import OrderedCollections
Expand Down
101 changes: 92 additions & 9 deletions Sources/Haversack/Entities/GenericPasswordEntity.swift
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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.
Expand All @@ -28,20 +101,30 @@ 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
}
}

// 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

Expand Down
9 changes: 5 additions & 4 deletions Sources/Haversack/Entities/IdentityEntity.swift
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand Down
Loading