mirror of
https://github.com/maxgoedjen/secretive.git
synced 2025-08-31 01:20:57 +00:00
Merge
This commit is contained in:
commit
d34d26884e
2
.github/workflows/nightly.yml
vendored
2
.github/workflows/nightly.yml
vendored
@ -20,7 +20,7 @@ jobs:
|
|||||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||||
run: ./.github/scripts/signing.sh
|
run: ./.github/scripts/signing.sh
|
||||||
- name: Set Environment
|
- name: Set Environment
|
||||||
run: sudo xcrun xcode-select -s /Applications/Xcode_26_beta_5.app
|
run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app
|
||||||
- name: Update Build Number
|
- name: Update Build Number
|
||||||
env:
|
env:
|
||||||
RUN_ID: ${{ github.run_id }}
|
RUN_ID: ${{ github.run_id }}
|
||||||
|
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@ -21,9 +21,9 @@ jobs:
|
|||||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||||
run: ./.github/scripts/signing.sh
|
run: ./.github/scripts/signing.sh
|
||||||
- name: Set Environment
|
- name: Set Environment
|
||||||
run: sudo xcrun xcode-select -s /Applications/Xcode_26_beta_5.app
|
run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app
|
||||||
- name: Test
|
- name: Test
|
||||||
run: swift build --build-system swiftbuild --package-path Sources/Packages
|
run: swift test --build-system swiftbuild --package-path Sources/Packages
|
||||||
build:
|
build:
|
||||||
# runs-on: macOS-latest
|
# runs-on: macOS-latest
|
||||||
runs-on: macos-15
|
runs-on: macos-15
|
||||||
@ -44,7 +44,7 @@ jobs:
|
|||||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||||
run: ./.github/scripts/signing.sh
|
run: ./.github/scripts/signing.sh
|
||||||
- name: Set Environment
|
- name: Set Environment
|
||||||
run: sudo xcrun xcode-select -s /Applications/Xcode_26_beta_5.app
|
run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app
|
||||||
- name: Update Build Number
|
- name: Update Build Number
|
||||||
env:
|
env:
|
||||||
TAG_NAME: ${{ github.ref }}
|
TAG_NAME: ${{ github.ref }}
|
||||||
|
8
.github/workflows/test.yml
vendored
8
.github/workflows/test.yml
vendored
@ -9,6 +9,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
- name: Set Environment
|
- name: Set Environment
|
||||||
run: sudo xcrun xcode-select -s /Applications/Xcode_26_beta_5.app
|
run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app
|
||||||
- name: Test
|
- name: Test Main Packages
|
||||||
run: swift build --build-system swiftbuild --package-path Sources/Packages
|
run: swift test --build-system swiftbuild --package-path Sources/Packages
|
||||||
|
- name: Test SecretKit Packages
|
||||||
|
run: swift test --build-system swiftbuild
|
||||||
|
69
Package.swift
Normal file
69
Package.swift
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
// swift-tools-version:6.2
|
||||||
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
// This is basically the same package as `Sources/Packages/Package.swift`, but thinned slightly.
|
||||||
|
// Ideally this would be the same package, but SPM requires it to be at the root of the project,
|
||||||
|
// and Xcode does _not_ like that, so they're separate.
|
||||||
|
let package = Package(
|
||||||
|
name: "SecretKit",
|
||||||
|
defaultLocalization: "en",
|
||||||
|
platforms: [
|
||||||
|
.macOS(.v14)
|
||||||
|
],
|
||||||
|
products: [
|
||||||
|
.library(
|
||||||
|
name: "SecretKit",
|
||||||
|
targets: ["SecretKit"]),
|
||||||
|
.library(
|
||||||
|
name: "SecureEnclaveSecretKit",
|
||||||
|
targets: ["SecureEnclaveSecretKit"]),
|
||||||
|
.library(
|
||||||
|
name: "SmartCardSecretKit",
|
||||||
|
targets: ["SmartCardSecretKit"]),
|
||||||
|
],
|
||||||
|
dependencies: [
|
||||||
|
],
|
||||||
|
targets: [
|
||||||
|
.target(
|
||||||
|
name: "SecretKit",
|
||||||
|
dependencies: [],
|
||||||
|
path: "Sources/Packages/Sources/SecretKit",
|
||||||
|
resources: [localization],
|
||||||
|
swiftSettings: swiftSettings
|
||||||
|
),
|
||||||
|
.testTarget(
|
||||||
|
name: "SecretKitTests",
|
||||||
|
dependencies: ["SecretKit", "SecureEnclaveSecretKit", "SmartCardSecretKit"],
|
||||||
|
path: "Sources/Packages/Tests/SecretKitTests",
|
||||||
|
swiftSettings: swiftSettings
|
||||||
|
),
|
||||||
|
.target(
|
||||||
|
name: "SecureEnclaveSecretKit",
|
||||||
|
dependencies: ["SecretKit"],
|
||||||
|
path: "Sources/Packages/Sources/SecureEnclaveSecretKit",
|
||||||
|
resources: [localization],
|
||||||
|
swiftSettings: swiftSettings
|
||||||
|
),
|
||||||
|
.target(
|
||||||
|
name: "SmartCardSecretKit",
|
||||||
|
dependencies: ["SecretKit"],
|
||||||
|
path: "Sources/Packages/Sources/SmartCardSecretKit",
|
||||||
|
resources: [localization],
|
||||||
|
swiftSettings: swiftSettings
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
var localization: Resource {
|
||||||
|
.process("../../Localizable.xcstrings")
|
||||||
|
}
|
||||||
|
|
||||||
|
var swiftSettings: [PackageDescription.SwiftSetting] {
|
||||||
|
[
|
||||||
|
.swiftLanguageMode(.v6),
|
||||||
|
// This freaks out Xcode in a dependency context.
|
||||||
|
// .treatAllWarnings(as: .error),
|
||||||
|
]
|
||||||
|
}
|
@ -49,7 +49,7 @@ There's a [FAQ here](FAQ.md).
|
|||||||
|
|
||||||
### Auditable Build Process
|
### Auditable Build Process
|
||||||
|
|
||||||
Builds are produced by GitHub Actions with an auditable build and release generation process. Each build has a "Document SHAs" step, which will output SHA checksums for the build produced by the GitHub Action, so you can verify that the source code for a given build corresponds to any given release.
|
Builds are produced by GitHub Actions with an auditable build and release generation process. Starting with Secretive 3.0, builds are attested using [GitHub Artifact Attestation](https://docs.github.com/en/actions/concepts/security/artifact-attestations). Attestations are viewable in the build log for a build, and also on the [main attestation page](https://github.com/maxgoedjen/secretive/attestations).
|
||||||
|
|
||||||
### A Note Around Code Signing and Keychains
|
### A Note Around Code Signing and Keychains
|
||||||
|
|
||||||
|
@ -36,47 +36,47 @@ let package = Package(
|
|||||||
name: "SecretKit",
|
name: "SecretKit",
|
||||||
dependencies: [],
|
dependencies: [],
|
||||||
resources: [localization],
|
resources: [localization],
|
||||||
swiftSettings: swiftSettings
|
swiftSettings: swiftSettings,
|
||||||
),
|
),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "SecretKitTests",
|
name: "SecretKitTests",
|
||||||
dependencies: ["SecretKit", "SecureEnclaveSecretKit", "SmartCardSecretKit"],
|
dependencies: ["SecretKit", "SecureEnclaveSecretKit", "SmartCardSecretKit"],
|
||||||
swiftSettings: swiftSettings
|
swiftSettings: swiftSettings,
|
||||||
),
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "SecureEnclaveSecretKit",
|
name: "SecureEnclaveSecretKit",
|
||||||
dependencies: ["SecretKit"],
|
dependencies: ["SecretKit"],
|
||||||
resources: [localization],
|
resources: [localization],
|
||||||
swiftSettings: swiftSettings
|
swiftSettings: swiftSettings,
|
||||||
),
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "SmartCardSecretKit",
|
name: "SmartCardSecretKit",
|
||||||
dependencies: ["SecretKit"],
|
dependencies: ["SecretKit"],
|
||||||
resources: [localization],
|
resources: [localization],
|
||||||
swiftSettings: swiftSettings
|
swiftSettings: swiftSettings,
|
||||||
),
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "SecretAgentKit",
|
name: "SecretAgentKit",
|
||||||
dependencies: ["SecretKit", "SecretAgentKitHeaders"],
|
dependencies: ["SecretKit", "SecretAgentKitHeaders"],
|
||||||
resources: [localization],
|
resources: [localization],
|
||||||
swiftSettings: swiftSettings
|
swiftSettings: swiftSettings,
|
||||||
),
|
),
|
||||||
.systemLibrary(
|
.systemLibrary(
|
||||||
name: "SecretAgentKitHeaders"
|
name: "SecretAgentKitHeaders",
|
||||||
),
|
),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "SecretAgentKitTests",
|
name: "SecretAgentKitTests",
|
||||||
dependencies: ["SecretAgentKit"])
|
dependencies: ["SecretAgentKit"],
|
||||||
,
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "Brief",
|
name: "Brief",
|
||||||
dependencies: [],
|
dependencies: [],
|
||||||
resources: [localization],
|
resources: [localization],
|
||||||
swiftSettings: swiftSettings
|
swiftSettings: swiftSettings,
|
||||||
),
|
),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "BriefTests",
|
name: "BriefTests",
|
||||||
dependencies: ["Brief"]
|
dependencies: ["Brief"],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -3,7 +3,7 @@ import Foundation
|
|||||||
/// Type eraser for Secret.
|
/// Type eraser for Secret.
|
||||||
public struct AnySecret: Secret, @unchecked Sendable {
|
public struct AnySecret: Secret, @unchecked Sendable {
|
||||||
|
|
||||||
let base: Any
|
public let base: Any
|
||||||
private let hashable: AnyHashable
|
private let hashable: AnyHashable
|
||||||
private let _id: () -> AnyHashable
|
private let _id: () -> AnyHashable
|
||||||
private let _name: () -> String
|
private let _name: () -> String
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
|
||||||
|
|
||||||
/// Type eraser for SecretStore.
|
/// Type eraser for SecretStore.
|
||||||
public class AnySecretStore: SecretStore, @unchecked Sendable {
|
open class AnySecretStore: SecretStore, @unchecked Sendable {
|
||||||
|
|
||||||
let base: any Sendable
|
let base: any Sendable
|
||||||
private let _isAvailable: @MainActor @Sendable () -> Bool
|
private let _isAvailable: @MainActor @Sendable () -> Bool
|
||||||
@ -10,7 +9,6 @@ public class AnySecretStore: SecretStore, @unchecked Sendable {
|
|||||||
private let _name: @MainActor @Sendable () -> String
|
private let _name: @MainActor @Sendable () -> String
|
||||||
private let _secrets: @MainActor @Sendable () -> [AnySecret]
|
private let _secrets: @MainActor @Sendable () -> [AnySecret]
|
||||||
private let _sign: @Sendable (Data, AnySecret, SigningRequestProvenance) async throws -> Data
|
private let _sign: @Sendable (Data, AnySecret, SigningRequestProvenance) async throws -> Data
|
||||||
private let _verify: @Sendable (Data, Data, AnySecret) async throws -> Bool
|
|
||||||
private let _existingPersistedAuthenticationContext: @Sendable (AnySecret) async -> PersistedAuthenticationContext?
|
private let _existingPersistedAuthenticationContext: @Sendable (AnySecret) async -> PersistedAuthenticationContext?
|
||||||
private let _persistAuthentication: @Sendable (AnySecret, TimeInterval) async throws -> Void
|
private let _persistAuthentication: @Sendable (AnySecret, TimeInterval) async throws -> Void
|
||||||
private let _reloadSecrets: @Sendable () async -> Void
|
private let _reloadSecrets: @Sendable () async -> Void
|
||||||
@ -22,7 +20,6 @@ public class AnySecretStore: SecretStore, @unchecked Sendable {
|
|||||||
_id = { secretStore.id }
|
_id = { secretStore.id }
|
||||||
_secrets = { secretStore.secrets.map { AnySecret($0) } }
|
_secrets = { secretStore.secrets.map { AnySecret($0) } }
|
||||||
_sign = { try await secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType, for: $2) }
|
_sign = { try await secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType, for: $2) }
|
||||||
_verify = { try await secretStore.verify(signature: $0, for: $1, with: $2.base as! SecretStoreType.SecretType) }
|
|
||||||
_existingPersistedAuthenticationContext = { await secretStore.existingPersistedAuthenticationContext(secret: $0.base as! SecretStoreType.SecretType) }
|
_existingPersistedAuthenticationContext = { await secretStore.existingPersistedAuthenticationContext(secret: $0.base as! SecretStoreType.SecretType) }
|
||||||
_persistAuthentication = { try await secretStore.persistAuthentication(secret: $0.base as! SecretStoreType.SecretType, forDuration: $1) }
|
_persistAuthentication = { try await secretStore.persistAuthentication(secret: $0.base as! SecretStoreType.SecretType, forDuration: $1) }
|
||||||
_reloadSecrets = { await secretStore.reloadSecrets() }
|
_reloadSecrets = { await secretStore.reloadSecrets() }
|
||||||
@ -48,10 +45,6 @@ public class AnySecretStore: SecretStore, @unchecked Sendable {
|
|||||||
try await _sign(data, secret, provenance)
|
try await _sign(data, secret, provenance)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func verify(signature: Data, for data: Data, with secret: AnySecret) async throws -> Bool {
|
|
||||||
try await _verify(signature, data, secret)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func existingPersistedAuthenticationContext(secret: AnySecret) async -> PersistedAuthenticationContext? {
|
public func existingPersistedAuthenticationContext(secret: AnySecret) async -> PersistedAuthenticationContext? {
|
||||||
await _existingPersistedAuthenticationContext(secret)
|
await _existingPersistedAuthenticationContext(secret)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
|
||||||
|
|
||||||
/// Manages access to Secrets, and performs signature operations on data using those Secrets.
|
/// Manages access to Secrets, and performs signature operations on data using those Secrets.
|
||||||
public protocol SecretStore: Identifiable, Sendable {
|
public protocol SecretStore: Identifiable, Sendable {
|
||||||
@ -23,14 +22,6 @@ public protocol SecretStore: Identifiable, Sendable {
|
|||||||
/// - Returns: The signed data.
|
/// - Returns: The signed data.
|
||||||
func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) async throws -> Data
|
func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) async throws -> Data
|
||||||
|
|
||||||
/// Verifies that a signature is valid over a specified payload.
|
|
||||||
/// - Parameters:
|
|
||||||
/// - signature: The signature over the data.
|
|
||||||
/// - data: The data to verify the signature of.
|
|
||||||
/// - secret: The secret whose signature to verify.
|
|
||||||
/// - Returns: Whether the signature was verified.
|
|
||||||
func verify(signature: Data, for data: Data, with secret: SecretType) async throws -> Bool
|
|
||||||
|
|
||||||
/// Checks to see if there is currently a valid persisted authentication for a given secret.
|
/// Checks to see if there is currently a valid persisted authentication for a given secret.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - secret: The ``Secret`` to check if there is a persisted authentication for.
|
/// - secret: The ``Secret`` to check if there is a persisted authentication for.
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
|
||||||
import SecretKit
|
import SecretKit
|
||||||
|
|
||||||
extension SecureEnclave {
|
extension SecureEnclave {
|
||||||
|
@ -73,41 +73,6 @@ extension SecureEnclave {
|
|||||||
return signature as Data
|
return signature as Data
|
||||||
}
|
}
|
||||||
|
|
||||||
public func verify(signature: Data, for data: Data, with secret: Secret) throws -> Bool {
|
|
||||||
let context = LAContext()
|
|
||||||
context.localizedReason = String(localized: .authContextRequestVerifyDescription(secretName: secret.name))
|
|
||||||
context.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
|
|
||||||
let attributes = KeychainDictionary([
|
|
||||||
kSecClass: kSecClassKey,
|
|
||||||
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
|
|
||||||
kSecAttrApplicationLabel: secret.id as CFData,
|
|
||||||
kSecAttrKeyType: Constants.keyType,
|
|
||||||
kSecAttrTokenID: kSecAttrTokenIDSecureEnclave,
|
|
||||||
kSecAttrApplicationTag: Constants.keyTag,
|
|
||||||
kSecUseAuthenticationContext: context,
|
|
||||||
kSecReturnRef: true
|
|
||||||
])
|
|
||||||
var verifyError: SecurityError?
|
|
||||||
var untyped: CFTypeRef?
|
|
||||||
let status = SecItemCopyMatching(attributes, &untyped)
|
|
||||||
if status != errSecSuccess {
|
|
||||||
throw KeychainError(statusCode: status)
|
|
||||||
}
|
|
||||||
guard let untypedSafe = untyped else {
|
|
||||||
throw KeychainError(statusCode: errSecSuccess)
|
|
||||||
}
|
|
||||||
let key = untypedSafe as! SecKey
|
|
||||||
let verified = SecKeyVerifySignature(key, .ecdsaSignatureMessageX962SHA256, data as CFData, signature as CFData, &verifyError)
|
|
||||||
if !verified, let verifyError {
|
|
||||||
if verifyError.takeUnretainedValue() ~= .verifyError {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
throw SigningError(error: verifyError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return verified
|
|
||||||
}
|
|
||||||
|
|
||||||
public func existingPersistedAuthenticationContext(secret: Secret) async -> PersistedAuthenticationContext? {
|
public func existingPersistedAuthenticationContext(secret: Secret) async -> PersistedAuthenticationContext? {
|
||||||
await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret)
|
await persistentAuthenticationHandler.existingPersistedAuthenticationContext(secret: secret)
|
||||||
}
|
}
|
||||||
@ -262,9 +227,9 @@ extension SecureEnclave.Store {
|
|||||||
|
|
||||||
extension SecureEnclave {
|
extension SecureEnclave {
|
||||||
|
|
||||||
enum Constants {
|
public enum Constants {
|
||||||
static let keyTag = Data("com.maxgoedjen.secretive.secureenclave.key".utf8)
|
public static let keyTag = Data("com.maxgoedjen.secretive.secureenclave.key".utf8)
|
||||||
static let keyType = kSecAttrKeyTypeECSECPrimeRandom as String
|
public static let keyType = kSecAttrKeyTypeECSECPrimeRandom as String
|
||||||
static let unauthenticatedThreshold: TimeInterval = 0.05
|
static let unauthenticatedThreshold: TimeInterval = 0.05
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
|
||||||
import SecretKit
|
import SecretKit
|
||||||
|
|
||||||
extension SmartCard {
|
extension SmartCard {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Observation
|
import Observation
|
||||||
import Security
|
import Security
|
||||||
import CryptoTokenKit
|
@preconcurrency import CryptoTokenKit
|
||||||
import LocalAuthentication
|
import LocalAuthentication
|
||||||
import SecretKit
|
import SecretKit
|
||||||
|
|
||||||
@ -23,6 +23,9 @@ extension SmartCard {
|
|||||||
public var isAvailable: Bool {
|
public var isAvailable: Bool {
|
||||||
state.isAvailable
|
state.isAvailable
|
||||||
}
|
}
|
||||||
|
@MainActor public var smartcardTokenID: String? {
|
||||||
|
state.tokenID
|
||||||
|
}
|
||||||
|
|
||||||
public let id = UUID()
|
public let id = UUID()
|
||||||
@MainActor public var name: String {
|
@MainActor public var name: String {
|
||||||
@ -34,17 +37,18 @@ extension SmartCard {
|
|||||||
|
|
||||||
/// Initializes a Store.
|
/// Initializes a Store.
|
||||||
public init() {
|
public init() {
|
||||||
Task { @MainActor in
|
Task {
|
||||||
if let tokenID = state.tokenID {
|
await MainActor.run {
|
||||||
state.isAvailable = true
|
if let tokenID = smartcardTokenID {
|
||||||
state.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: tokenID)
|
state.isAvailable = true
|
||||||
|
state.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: tokenID)
|
||||||
|
}
|
||||||
|
loadSecrets()
|
||||||
}
|
}
|
||||||
loadSecrets()
|
// Doing this inside a regular mainactor handler casues thread assertions in CryptoTokenKit to blow up when the handler executes.
|
||||||
state.watcher.setInsertionHandler { id in
|
await state.watcher.setInsertionHandler { id in
|
||||||
// Setting insertion handler will cause it to be called immediately.
|
|
||||||
// Make a thread jump so we don't hit a recursive lock attempt.
|
|
||||||
Task {
|
Task {
|
||||||
self.smartcardInserted(for: id)
|
await self.smartcardInserted(for: id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -81,29 +85,6 @@ extension SmartCard {
|
|||||||
return signature as Data
|
return signature as Data
|
||||||
}
|
}
|
||||||
|
|
||||||
public func verify(signature: Data, for data: Data, with secret: Secret) throws -> Bool {
|
|
||||||
let attributes = KeychainDictionary([
|
|
||||||
kSecAttrKeyType: secret.keyType.secAttrKeyType as Any,
|
|
||||||
kSecAttrKeySizeInBits: secret.keyType.size,
|
|
||||||
kSecAttrKeyClass: kSecAttrKeyClassPublic
|
|
||||||
])
|
|
||||||
var verifyError: SecurityError?
|
|
||||||
let untyped: CFTypeRef? = SecKeyCreateWithData(secret.publicKey as CFData, attributes, &verifyError)
|
|
||||||
guard let untypedSafe = untyped else {
|
|
||||||
throw KeychainError(statusCode: errSecSuccess)
|
|
||||||
}
|
|
||||||
let key = untypedSafe as! SecKey
|
|
||||||
let verified = SecKeyVerifySignature(key, signatureAlgorithm(for: secret, allowRSA: true), data as CFData, signature as CFData, &verifyError)
|
|
||||||
if !verified, let verifyError {
|
|
||||||
if verifyError.takeUnretainedValue() ~= .verifyError {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
throw SigningError(error: verifyError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return verified
|
|
||||||
}
|
|
||||||
|
|
||||||
public func existingPersistedAuthenticationContext(secret: Secret) -> PersistedAuthenticationContext? {
|
public func existingPersistedAuthenticationContext(secret: Secret) -> PersistedAuthenticationContext? {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
@ -135,12 +116,13 @@ extension SmartCard.Store {
|
|||||||
/// Resets the token ID and reloads secrets.
|
/// Resets the token ID and reloads secrets.
|
||||||
/// - Parameter tokenID: The ID of the token that was inserted.
|
/// - Parameter tokenID: The ID of the token that was inserted.
|
||||||
@MainActor private func smartcardInserted(for tokenID: String? = nil) {
|
@MainActor private func smartcardInserted(for tokenID: String? = nil) {
|
||||||
guard let string = state.watcher.nonSecureEnclaveTokens.first else { return }
|
guard let string = state.watcher.nonSecureEnclaveTokens.first else { return }
|
||||||
guard state.tokenID == nil else { return }
|
guard state.tokenID == nil else { return }
|
||||||
guard !string.contains("setoken") else { return }
|
guard !string.contains("setoken") else { return }
|
||||||
state.tokenID = string
|
state.tokenID = string
|
||||||
state.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: string)
|
state.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: string)
|
||||||
state.tokenID = string
|
state.tokenID = string
|
||||||
|
reloadSecretsInternal()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resets the token ID and reloads secrets.
|
/// Resets the token ID and reloads secrets.
|
||||||
@ -188,88 +170,6 @@ extension SmartCard.Store {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// MARK: Smart Card specific encryption/decryption/verification
|
|
||||||
extension SmartCard.Store {
|
|
||||||
|
|
||||||
/// Encrypts a payload with a specified key.
|
|
||||||
/// - Parameters:
|
|
||||||
/// - data: The payload to encrypt.
|
|
||||||
/// - secret: The secret to encrypt with.
|
|
||||||
/// - Returns: The encrypted data.
|
|
||||||
/// - Warning: Encryption functions are deliberately only exposed on a library level, and are not exposed in Secretive itself to prevent users from data loss. Any pull requests which expose this functionality in the app will not be merged.
|
|
||||||
public func encrypt(data: Data, with secret: SecretType) throws -> Data {
|
|
||||||
let context = LAContext()
|
|
||||||
context.localizedReason = String(localized: .authContextRequestEncryptDescription(secretName: secret.name))
|
|
||||||
context.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
|
|
||||||
let attributes = KeychainDictionary([
|
|
||||||
kSecAttrKeyType: secret.keyType.secAttrKeyType as Any,
|
|
||||||
kSecAttrKeySizeInBits: secret.keyType.size,
|
|
||||||
kSecAttrKeyClass: kSecAttrKeyClassPublic,
|
|
||||||
kSecUseAuthenticationContext: context
|
|
||||||
])
|
|
||||||
var encryptError: SecurityError?
|
|
||||||
let untyped: CFTypeRef? = SecKeyCreateWithData(secret.publicKey as CFData, attributes, &encryptError)
|
|
||||||
guard let untypedSafe = untyped else {
|
|
||||||
throw KeychainError(statusCode: errSecSuccess)
|
|
||||||
}
|
|
||||||
let key = untypedSafe as! SecKey
|
|
||||||
guard let signature = SecKeyCreateEncryptedData(key, encryptionAlgorithm(for: secret), data as CFData, &encryptError) else {
|
|
||||||
throw SigningError(error: encryptError)
|
|
||||||
}
|
|
||||||
return signature as Data
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decrypts a payload with a specified key.
|
|
||||||
/// - Parameters:
|
|
||||||
/// - data: The payload to decrypt.
|
|
||||||
/// - secret: The secret to decrypt with.
|
|
||||||
/// - Returns: The decrypted data.
|
|
||||||
/// - Warning: Encryption functions are deliberately only exposed on a library level, and are not exposed in Secretive itself to prevent users from data loss. Any pull requests which expose this functionality in the app will not be merged.
|
|
||||||
public func decrypt(data: Data, with secret: SecretType) async throws -> Data {
|
|
||||||
guard let tokenID = await state.tokenID else { fatalError() }
|
|
||||||
let context = LAContext()
|
|
||||||
context.localizedReason = String(localized: .authContextRequestDecryptDescription(secretName: secret.name))
|
|
||||||
context.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
|
|
||||||
let attributes = KeychainDictionary([
|
|
||||||
kSecClass: kSecClassKey,
|
|
||||||
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
|
|
||||||
kSecAttrApplicationLabel: secret.id as CFData,
|
|
||||||
kSecAttrTokenID: tokenID,
|
|
||||||
kSecUseAuthenticationContext: context,
|
|
||||||
kSecReturnRef: true
|
|
||||||
])
|
|
||||||
var untyped: CFTypeRef?
|
|
||||||
let status = SecItemCopyMatching(attributes, &untyped)
|
|
||||||
if status != errSecSuccess {
|
|
||||||
throw KeychainError(statusCode: status)
|
|
||||||
}
|
|
||||||
guard let untypedSafe = untyped else {
|
|
||||||
throw KeychainError(statusCode: errSecSuccess)
|
|
||||||
}
|
|
||||||
let key = untypedSafe as! SecKey
|
|
||||||
var encryptError: SecurityError?
|
|
||||||
guard let signature = SecKeyCreateDecryptedData(key, encryptionAlgorithm(for: secret), data as CFData, &encryptError) else {
|
|
||||||
throw SigningError(error: encryptError)
|
|
||||||
}
|
|
||||||
return signature as Data
|
|
||||||
}
|
|
||||||
|
|
||||||
private func encryptionAlgorithm(for secret: SecretType) -> SecKeyAlgorithm {
|
|
||||||
switch (secret.keyType.algorithm, secret.keyType.size) {
|
|
||||||
case (.ecdsa, 256):
|
|
||||||
return .eciesEncryptionCofactorVariableIVX963SHA256AESGCM
|
|
||||||
case (.ecdsa, 384):
|
|
||||||
return .eciesEncryptionCofactorVariableIVX963SHA384AESGCM
|
|
||||||
case (.rsa, 1024), (.rsa, 2048):
|
|
||||||
return .rsaEncryptionOAEPSHA512AESGCM
|
|
||||||
default:
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension TKTokenWatcher {
|
extension TKTokenWatcher {
|
||||||
|
|
||||||
/// All available tokens, excluding the Secure Enclave.
|
/// All available tokens, excluding the Secure Enclave.
|
||||||
|
@ -60,18 +60,10 @@ import CryptoKit
|
|||||||
}
|
}
|
||||||
var rs = r
|
var rs = r
|
||||||
rs.append(s)
|
rs.append(s)
|
||||||
let signature = try! P256.Signing.ECDSASignature(rawRepresentation: rs)
|
let signature = try P256.Signing.ECDSASignature(rawRepresentation: rs)
|
||||||
let referenceValid = try! P256.Signing.PublicKey(x963Representation: Constants.Secrets.ecdsa256Secret.publicKey).isValidSignature(signature, for: dataToSign)
|
// Correct signature
|
||||||
let store = await list.stores.first!
|
#expect(try P256.Signing.PublicKey(x963Representation: Constants.Secrets.ecdsa256Secret.publicKey)
|
||||||
let derVerifies = try await store.verify(signature: signature.derRepresentation, for: dataToSign, with: AnySecret(Constants.Secrets.ecdsa256Secret))
|
.isValidSignature(signature, for: dataToSign))
|
||||||
let invalidRandomSignature = try await store.verify(signature: "invalid".data(using: .utf8)!, for: dataToSign, with: AnySecret(Constants.Secrets.ecdsa256Secret))
|
|
||||||
let invalidRandomData = try await store.verify(signature: signature.derRepresentation, for: "invalid".data(using: .utf8)!, with: AnySecret(Constants.Secrets.ecdsa256Secret))
|
|
||||||
let invalidWrongKey = try await store.verify(signature: signature.derRepresentation, for: dataToSign, with: AnySecret(Constants.Secrets.ecdsa384Secret))
|
|
||||||
#expect(referenceValid)
|
|
||||||
#expect(derVerifies)
|
|
||||||
#expect(invalidRandomSignature == false)
|
|
||||||
#expect(invalidRandomData == false)
|
|
||||||
#expect(invalidWrongKey == false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Witness protocol
|
// MARK: Witness protocol
|
||||||
|
@ -61,29 +61,6 @@ extension Stub {
|
|||||||
return SecKeyCreateSignature(privateKey, signatureAlgorithm(for: secret), data as CFData, nil)! as Data
|
return SecKeyCreateSignature(privateKey, signatureAlgorithm(for: secret), data as CFData, nil)! as Data
|
||||||
}
|
}
|
||||||
|
|
||||||
public func verify(signature: Data, for data: Data, with secret: Stub.Secret) throws -> Bool {
|
|
||||||
let attributes = KeychainDictionary([
|
|
||||||
kSecAttrKeyType: secret.algorithm.secAttrKeyType,
|
|
||||||
kSecAttrKeySizeInBits: secret.keySize,
|
|
||||||
kSecAttrKeyClass: kSecAttrKeyClassPublic
|
|
||||||
])
|
|
||||||
var verifyError: Unmanaged<CFError>?
|
|
||||||
let untyped: CFTypeRef? = SecKeyCreateWithData(secret.publicKey as CFData, attributes, &verifyError)
|
|
||||||
guard let untypedSafe = untyped else {
|
|
||||||
throw NSError(domain: "test", code: 0, userInfo: nil)
|
|
||||||
}
|
|
||||||
let key = untypedSafe as! SecKey
|
|
||||||
let verified = SecKeyVerifySignature(key, signatureAlgorithm(for: secret), data as CFData, signature as CFData, &verifyError)
|
|
||||||
if let verifyError {
|
|
||||||
if verifyError.takeUnretainedValue() ~= .verifyError {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
throw NSError(domain: "test", code: 0, userInfo: nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return verified
|
|
||||||
}
|
|
||||||
|
|
||||||
public func existingPersistedAuthenticationContext(secret: Stub.Secret) -> PersistedAuthenticationContext? {
|
public func existingPersistedAuthenticationContext(secret: Stub.Secret) -> PersistedAuthenticationContext? {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
import OSLog
|
import OSLog
|
||||||
import Combine
|
|
||||||
import SecretKit
|
import SecretKit
|
||||||
import SecureEnclaveSecretKit
|
import SecureEnclaveSecretKit
|
||||||
import SmartCardSecretKit
|
import SmartCardSecretKit
|
||||||
@ -28,7 +27,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
let path = (NSHomeDirectory() as NSString).appendingPathComponent("socket.ssh") as String
|
let path = (NSHomeDirectory() as NSString).appendingPathComponent("socket.ssh") as String
|
||||||
return SocketController(path: path)
|
return SocketController(path: path)
|
||||||
}()
|
}()
|
||||||
private var updateSink: AnyCancellable?
|
|
||||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "AppDelegate")
|
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "AppDelegate")
|
||||||
|
|
||||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
|
||||||
import AppKit
|
import AppKit
|
||||||
import SecretKit
|
import SecretKit
|
||||||
import Observation
|
import Observation
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
|
||||||
import AppKit
|
import AppKit
|
||||||
|
|
||||||
protocol JustUpdatedCheckerProtocol: Observable {
|
protocol JustUpdatedCheckerProtocol: Observable {
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
|
||||||
|
|
||||||
class PreviewAgentStatusChecker: AgentStatusCheckerProtocol {
|
class PreviewAgentStatusChecker: AgentStatusCheckerProtocol {
|
||||||
|
|
||||||
|
@ -42,10 +42,6 @@ extension Preview {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
func verify(signature data: Data, for signature: Data, with secret: Preview.Secret) throws -> Bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
func existingPersistedAuthenticationContext(secret: Preview.Secret) -> PersistedAuthenticationContext? {
|
func existingPersistedAuthenticationContext(secret: Preview.Secret) -> PersistedAuthenticationContext? {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
@ -85,10 +81,6 @@ extension Preview {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
func verify(signature data: Data, for signature: Data, with secret: Preview.Secret) throws -> Bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
func existingPersistedAuthenticationContext(secret: Preview.Secret) -> PersistedAuthenticationContext? {
|
func existingPersistedAuthenticationContext(secret: Preview.Secret) -> PersistedAuthenticationContext? {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Combine
|
|
||||||
import SecretKit
|
import SecretKit
|
||||||
|
|
||||||
struct StoreListView: View {
|
struct StoreListView: View {
|
||||||
|
Loading…
Reference in New Issue
Block a user