mirror of
https://github.com/maxgoedjen/secretive.git
synced 2026-03-10 11:37:23 +01:00
More cleanup
This commit is contained in:
@@ -6,7 +6,7 @@ import PackageDescription
|
||||
let package = Package(
|
||||
name: "SecretivePackages",
|
||||
platforms: [
|
||||
.macOS(.v15)
|
||||
.macOS(.v14)
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
@@ -27,13 +27,16 @@ let package = Package(
|
||||
.library(
|
||||
name: "Brief",
|
||||
targets: ["Brief"]),
|
||||
.library(
|
||||
name: "Common",
|
||||
targets: ["Common"]),
|
||||
],
|
||||
dependencies: [
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "SecretKit",
|
||||
dependencies: [],
|
||||
dependencies: ["Common"],
|
||||
swiftSettings: swiftSettings
|
||||
),
|
||||
.testTarget(
|
||||
@@ -43,17 +46,17 @@ let package = Package(
|
||||
),
|
||||
.target(
|
||||
name: "SecureEnclaveSecretKit",
|
||||
dependencies: ["SecretKit"],
|
||||
dependencies: ["Common", "SecretKit"],
|
||||
swiftSettings: swiftSettings
|
||||
),
|
||||
.target(
|
||||
name: "SmartCardSecretKit",
|
||||
dependencies: ["SecretKit"],
|
||||
dependencies: ["Common", "SecretKit"],
|
||||
swiftSettings: swiftSettings
|
||||
),
|
||||
.target(
|
||||
name: "SecretAgentKit",
|
||||
dependencies: ["SecretKit", "SecretAgentKitHeaders"],
|
||||
dependencies: ["Common", "SecretKit", "SecretAgentKitHeaders"],
|
||||
swiftSettings: swiftSettings
|
||||
),
|
||||
.systemLibrary(
|
||||
@@ -65,13 +68,18 @@ let package = Package(
|
||||
,
|
||||
.target(
|
||||
name: "Brief",
|
||||
dependencies: [],
|
||||
dependencies: ["Common"],
|
||||
swiftSettings: swiftSettings
|
||||
),
|
||||
.testTarget(
|
||||
name: "BriefTests",
|
||||
dependencies: ["Brief"]
|
||||
),
|
||||
.target(
|
||||
name: "Common",
|
||||
dependencies: [],
|
||||
swiftSettings: swiftSettings
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import Foundation
|
||||
import Observation
|
||||
import Synchronization
|
||||
import os
|
||||
import Common
|
||||
|
||||
/// A concrete implementation of ``UpdaterProtocol`` which considers the current release and OS version.
|
||||
@Observable public final class Updater: UpdaterProtocol, ObservableObject, Sendable {
|
||||
|
||||
public var update: Release? {
|
||||
_update.withLock { $0 }
|
||||
_update.lockedValue
|
||||
}
|
||||
private let _update: Mutex<Release?> = .init(nil)
|
||||
private let _update: OSAllocatedUnfairLock<Release?> = .init(uncheckedState: nil)
|
||||
public let testBuild: Bool
|
||||
|
||||
/// The current OS version.
|
||||
@@ -53,9 +54,7 @@ import Synchronization
|
||||
guard !release.critical else { return }
|
||||
defaults.set(true, forKey: release.name)
|
||||
await MainActor.run {
|
||||
_update.withLock { value in
|
||||
value = nil
|
||||
}
|
||||
_update.lockedValue = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,9 +75,7 @@ extension Updater {
|
||||
let latestVersion = SemVer(release.name)
|
||||
if latestVersion > currentVersion {
|
||||
await MainActor.run {
|
||||
_update.withLock { value in
|
||||
value = release
|
||||
}
|
||||
_update.lockedValue = release
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Foundation
|
||||
import Synchronization
|
||||
import os
|
||||
|
||||
/// A protocol for retreiving the latest available version of an app.
|
||||
public protocol UpdaterProtocol: Observable {
|
||||
|
||||
14
Sources/Packages/Sources/Common/Locks.swift
Normal file
14
Sources/Packages/Sources/Common/Locks.swift
Normal file
@@ -0,0 +1,14 @@
|
||||
import os
|
||||
|
||||
public extension OSAllocatedUnfairLock where State: Sendable {
|
||||
|
||||
var lockedValue: State {
|
||||
get {
|
||||
withLock { $0 }
|
||||
}
|
||||
nonmutating set {
|
||||
withLock { $0 = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import Foundation
|
||||
import OSLog
|
||||
import Synchronization
|
||||
import os
|
||||
|
||||
/// Manages storage and lookup for OpenSSH certificates.
|
||||
public final class OpenSSHCertificateHandler: Sendable {
|
||||
@@ -8,7 +8,7 @@ public final class OpenSSHCertificateHandler: Sendable {
|
||||
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory())
|
||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "OpenSSHCertificateHandler")
|
||||
private let writer = OpenSSHKeyWriter()
|
||||
private let keyBlobsAndNames: Mutex<[AnySecret: (Data, Data)]> = .init([:])
|
||||
private let keyBlobsAndNames: OSAllocatedUnfairLock<[AnySecret: (Data, Data)]> = .init(uncheckedState: [:])
|
||||
|
||||
/// Initializes an OpenSSHCertificateHandler.
|
||||
public init() {
|
||||
@@ -32,10 +32,7 @@ public final class OpenSSHCertificateHandler: Sendable {
|
||||
/// - Parameter secret: The secret to check for a certificate.
|
||||
/// - Returns: A boolean describing whether or not the certificate handler has a certifiicate associated with a given secret
|
||||
public func hasCertificate<SecretType: Secret>(for secret: SecretType) -> Bool {
|
||||
keyBlobsAndNames.withLock {
|
||||
$0[AnySecret(secret)] != nil
|
||||
}
|
||||
|
||||
keyBlobsAndNames.lockedValue[AnySecret(secret)] != nil
|
||||
}
|
||||
|
||||
|
||||
@@ -67,9 +64,7 @@ public final class OpenSSHCertificateHandler: Sendable {
|
||||
/// - Parameter secret: The secret to search for a certificate with
|
||||
/// - Returns: A (``Data``, ``Data``) tuple containing the certificate and certificate name, respectively.
|
||||
public func keyBlobAndName<SecretType: Secret>(for secret: SecretType) throws -> (Data, Data)? {
|
||||
keyBlobsAndNames.withLock {
|
||||
$0[AnySecret(secret)]
|
||||
}
|
||||
keyBlobsAndNames.lockedValue[AnySecret(secret)]
|
||||
}
|
||||
|
||||
/// Attempts to find an OpenSSH Certificate that corresponds to a ``Secret``
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import Foundation
|
||||
import Observation
|
||||
import Synchronization
|
||||
import os
|
||||
import Common
|
||||
|
||||
/// A "Store Store," which holds a list of type-erased stores.
|
||||
@Observable public final class SecretStoreList: Sendable {
|
||||
|
||||
/// The Stores managed by the SecretStoreList.
|
||||
public var stores: [AnySecretStore] {
|
||||
__stores.withLock { $0 }
|
||||
__stores.lockedValue
|
||||
}
|
||||
private let __stores: Mutex<[AnySecretStore]> = .init([])
|
||||
private let __stores: OSAllocatedUnfairLock<[AnySecretStore]> = .init(uncheckedState: [])
|
||||
|
||||
/// A modifiable store, if one is available.
|
||||
public var modifiableStore: AnySecretStoreModifiable? {
|
||||
__modifiableStore.withLock { $0 }
|
||||
}
|
||||
private let __modifiableStore: Mutex<AnySecretStoreModifiable?> = .init(nil)
|
||||
private let __modifiableStore: OSAllocatedUnfairLock<AnySecretStoreModifiable?> = .init(uncheckedState: nil)
|
||||
|
||||
/// Initializes a SecretStoreList.
|
||||
public init() {
|
||||
@@ -31,9 +32,7 @@ import Synchronization
|
||||
/// Adds a non-type-erased modifiable SecretStore.
|
||||
public func add<SecretStoreType: SecretStoreModifiable>(store: SecretStoreType) {
|
||||
let modifiable = AnySecretStoreModifiable(modifiable: store)
|
||||
__modifiableStore.withLock {
|
||||
$0 = modifiable
|
||||
}
|
||||
__modifiableStore.lockedValue = modifiable
|
||||
__stores.withLock {
|
||||
$0.append(modifiable)
|
||||
}
|
||||
@@ -41,15 +40,11 @@ import Synchronization
|
||||
|
||||
/// A boolean describing whether there are any Stores available.
|
||||
public var anyAvailable: Bool {
|
||||
__stores.withLock {
|
||||
$0.reduce(false, { $0 || $1.isAvailable })
|
||||
}
|
||||
__stores.lockedValue.contains(where: \.isAvailable)
|
||||
}
|
||||
|
||||
public var allSecrets: [AnySecret] {
|
||||
__stores.withLock {
|
||||
$0.flatMap(\.secrets)
|
||||
}
|
||||
__stores.lockedValue.flatMap(\.secrets)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import Security
|
||||
import CryptoKit
|
||||
@preconcurrency import LocalAuthentication
|
||||
import SecretKit
|
||||
import Synchronization
|
||||
import os
|
||||
import Common
|
||||
|
||||
extension SecureEnclave {
|
||||
|
||||
@@ -17,11 +18,11 @@ extension SecureEnclave {
|
||||
public let id = UUID()
|
||||
public let name = String(localized: "secure_enclave")
|
||||
public var secrets: [Secret] {
|
||||
_secrets.withLock { $0 }
|
||||
_secrets.lockedValue
|
||||
}
|
||||
private let _secrets: Mutex<[Secret]> = .init([])
|
||||
private let _secrets: OSAllocatedUnfairLock<[Secret]> = .init(uncheckedState: [])
|
||||
|
||||
private let persistedAuthenticationContexts: Mutex<[Secret: PersistentAuthenticationContext]> = .init([:])
|
||||
private let persistedAuthenticationContexts: OSAllocatedUnfairLock<[Secret: PersistentAuthenticationContext]> = .init(uncheckedState: [:])
|
||||
|
||||
/// Initializes a Store.
|
||||
public init() {
|
||||
@@ -105,42 +106,40 @@ extension SecureEnclave {
|
||||
}
|
||||
|
||||
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) throws -> Data {
|
||||
let context: Mutex<LAContext>
|
||||
// if let existing = persistedAuthenticationContexts.withLock({ $0 })[secret], existing.valid {
|
||||
// context = existing.context
|
||||
// } else {
|
||||
var context: LAContext
|
||||
if let existing = persistedAuthenticationContexts.lockedValue[secret], existing.valid {
|
||||
context = existing.context
|
||||
} else {
|
||||
let newContext = LAContext()
|
||||
newContext.localizedCancelTitle = String(localized: "auth_context_request_deny_button")
|
||||
context = .init(newContext)
|
||||
// }
|
||||
return try context.withLock { context in
|
||||
context.localizedReason = String(localized: "auth_context_request_signature_description_\(provenance.origin.displayName)_\(secret.name)")
|
||||
let attributes = KeychainDictionary([
|
||||
kSecClass: kSecClassKey,
|
||||
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
|
||||
kSecAttrApplicationLabel: secret.id as CFData,
|
||||
kSecAttrKeyType: Constants.keyType,
|
||||
kSecAttrTokenID: kSecAttrTokenIDSecureEnclave,
|
||||
kSecAttrApplicationTag: Constants.keyTag,
|
||||
kSecUseAuthenticationContext: context,
|
||||
kSecReturnRef: true
|
||||
])
|
||||
var untyped: CFTypeRef?
|
||||
let status = SecItemCopyMatching(attributes, &untyped)
|
||||
if status != errSecSuccess {
|
||||
throw KeychainError(statusCode: status)
|
||||
}
|
||||
guard let untypedSafe = untyped else {
|
||||
throw KeychainError(statusCode: errSecSuccess)
|
||||
}
|
||||
let key = untypedSafe as! SecKey
|
||||
var signError: SecurityError?
|
||||
|
||||
guard let signature = SecKeyCreateSignature(key, .ecdsaSignatureMessageX962SHA256, data as CFData, &signError) else {
|
||||
throw SigningError(error: signError)
|
||||
}
|
||||
return signature as Data
|
||||
context = newContext
|
||||
}
|
||||
context.localizedReason = String(localized: "auth_context_request_signature_description_\(provenance.origin.displayName)_\(secret.name)")
|
||||
let attributes = KeychainDictionary([
|
||||
kSecClass: kSecClassKey,
|
||||
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
|
||||
kSecAttrApplicationLabel: secret.id as CFData,
|
||||
kSecAttrKeyType: Constants.keyType,
|
||||
kSecAttrTokenID: kSecAttrTokenIDSecureEnclave,
|
||||
kSecAttrApplicationTag: Constants.keyTag,
|
||||
kSecUseAuthenticationContext: context,
|
||||
kSecReturnRef: true
|
||||
])
|
||||
var untyped: CFTypeRef?
|
||||
let status = SecItemCopyMatching(attributes, &untyped)
|
||||
if status != errSecSuccess {
|
||||
throw KeychainError(statusCode: status)
|
||||
}
|
||||
guard let untypedSafe = untyped else {
|
||||
throw KeychainError(statusCode: errSecSuccess)
|
||||
}
|
||||
let key = untypedSafe as! SecKey
|
||||
var signError: SecurityError?
|
||||
|
||||
guard let signature = SecKeyCreateSignature(key, .ecdsaSignatureMessageX962SHA256, data as CFData, &signError) else {
|
||||
throw SigningError(error: signError)
|
||||
}
|
||||
return signature as Data
|
||||
}
|
||||
|
||||
public func verify(signature: Data, for data: Data, with secret: Secret) throws -> Bool {
|
||||
@@ -179,7 +178,7 @@ extension SecureEnclave {
|
||||
}
|
||||
|
||||
public func existingPersistedAuthenticationContext(secret: Secret) -> PersistedAuthenticationContext? {
|
||||
guard let persisted = persistedAuthenticationContexts.withLock({ $0 })[secret], persisted.valid else { return nil }
|
||||
guard let persisted = persistedAuthenticationContexts.lockedValue[secret], persisted.valid else { return nil }
|
||||
return persisted
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Foundation
|
||||
import Synchronization
|
||||
import os
|
||||
import Observation
|
||||
import Security
|
||||
import CryptoTokenKit
|
||||
@@ -19,7 +19,7 @@ extension SmartCard {
|
||||
/// An implementation of Store backed by a Smart Card.
|
||||
@Observable public final class Store: SecretStore {
|
||||
|
||||
private let state: Mutex<State> = .init(.init())
|
||||
private let state: OSAllocatedUnfairLock<State> = .init(uncheckedState: .init())
|
||||
public var isAvailable: Bool {
|
||||
state.withLock { $0.isAvailable }
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import Foundation
|
||||
import os
|
||||
import Testing
|
||||
import CryptoKit
|
||||
import Synchronization
|
||||
@testable import SecretKit
|
||||
@testable import SecretAgentKit
|
||||
import Common
|
||||
|
||||
@Suite struct AgentTests {
|
||||
|
||||
@@ -91,7 +92,7 @@ import Synchronization
|
||||
@Test func witnessSignature() async {
|
||||
let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature)
|
||||
let list = storeList(with: [Constants.Secrets.ecdsa256Secret])
|
||||
let witnessed: Mutex<Bool> = .init(false)
|
||||
let witnessed: OSAllocatedUnfairLock<Bool> = .init(uncheckedState: false)
|
||||
let witness = StubWitness(speakNow: { _, trace in
|
||||
return false
|
||||
}, witness: { _, trace in
|
||||
@@ -106,8 +107,8 @@ import Synchronization
|
||||
@Test func requestTracing() async {
|
||||
let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature)
|
||||
let list = storeList(with: [Constants.Secrets.ecdsa256Secret])
|
||||
let speakNowTrace: Mutex<SigningRequestProvenance?> = .init(nil)
|
||||
let witnessTrace: Mutex<SigningRequestProvenance?> = .init(nil)
|
||||
let speakNowTrace: OSAllocatedUnfairLock<SigningRequestProvenance?> = .init(uncheckedState: nil)
|
||||
let witnessTrace: OSAllocatedUnfairLock<SigningRequestProvenance?> = .init(uncheckedState: nil)
|
||||
let witness = StubWitness(speakNow: { _, trace in
|
||||
speakNowTrace.lockedValue = trace
|
||||
return false
|
||||
@@ -145,19 +146,6 @@ import Synchronization
|
||||
|
||||
}
|
||||
|
||||
extension Mutex where Value: Sendable {
|
||||
|
||||
var lockedValue: Value {
|
||||
get {
|
||||
withLock { $0 }
|
||||
}
|
||||
nonmutating set {
|
||||
withLock { $0 = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AgentTests {
|
||||
|
||||
func storeList(with secrets: [Stub.Secret]) -> SecretStoreList {
|
||||
|
||||
Reference in New Issue
Block a user