diff --git a/Sources/Packages/Sources/SecretKit/Documentation.docc/Documentation.md b/Sources/Packages/Sources/SecretKit/Documentation.docc/Documentation.md new file mode 100644 index 0000000..6888611 --- /dev/null +++ b/Sources/Packages/Sources/SecretKit/Documentation.docc/Documentation.md @@ -0,0 +1,31 @@ +# ````SecretKit```` + +SecretKit is a collection of protocols describing secrets and stores. + +## Topics + +### Base Protocols + +- ``Secret`` +- ``SecretStore`` +- ``SecretStoreModifiable`` + +### Store List + +- ``SecretStoreList`` + +### Type Erasers + +- ``AnySecret`` +- ``AnySecretStore`` +- ``AnySecretStoreModifiable`` + +### OpenSSH + +- ``OpenSSHKeyWriter`` +- ``OpenSSHReader`` + +### Signing Process + +- ``SignedData`` +- ``SigningRequestProvenance`` diff --git a/Sources/Packages/Sources/SecretKit/Erasers/AnySecret.swift b/Sources/Packages/Sources/SecretKit/Erasers/AnySecret.swift index a3c0415..b5a748d 100644 --- a/Sources/Packages/Sources/SecretKit/Erasers/AnySecret.swift +++ b/Sources/Packages/Sources/SecretKit/Erasers/AnySecret.swift @@ -1,5 +1,6 @@ import Foundation +/// Type eraser for Secret. public struct AnySecret: Secret { let base: Any diff --git a/Sources/Packages/Sources/SecretKit/Erasers/AnySecretStore.swift b/Sources/Packages/Sources/SecretKit/Erasers/AnySecretStore.swift index 7b565fe..305ecd2 100644 --- a/Sources/Packages/Sources/SecretKit/Erasers/AnySecretStore.swift +++ b/Sources/Packages/Sources/SecretKit/Erasers/AnySecretStore.swift @@ -1,6 +1,7 @@ import Foundation import Combine +/// Type eraser for SecretStore. public class AnySecretStore: SecretStore { let base: Any diff --git a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHKeyWriter.swift b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHKeyWriter.swift index a577476..223b935 100644 --- a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHKeyWriter.swift +++ b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHKeyWriter.swift @@ -1,24 +1,31 @@ import Foundation import CryptoKit -// For the moment, only supports ecdsa-sha2-nistp256 and ecdsa-sha2-nistp386 keys +/// Generates OpenSSH representations of Secrets. public struct OpenSSHKeyWriter { + /// Initializes the writer. public init() { } + /// Generates an OpenSSH data payload identifying the secret. + /// - Returns: OpenSSH data payload identifying the secret. public func data(secret: SecretType) -> Data { lengthAndData(of: curveType(for: secret.algorithm, length: secret.keySize).data(using: .utf8)!) + lengthAndData(of: curveIdentifier(for: secret.algorithm, length: secret.keySize).data(using: .utf8)!) + lengthAndData(of: secret.publicKey) } + /// Generates an OpenSSH string representation of the secret. + /// - Returns: OpenSSH string representation of the secret. public func openSSHString(secret: SecretType, comment: String? = nil) -> String { [curveType(for: secret.algorithm, length: secret.keySize), data(secret: secret).base64EncodedString(), comment] .compactMap { $0 } .joined(separator: " ") } + /// Generates an OpenSSH SHA256 fingerprint string. + /// - Returns: OpenSSH SHA256 fingerprint string. public func openSSHSHA256Fingerprint(secret: SecretType) -> String { // OpenSSL format seems to strip the padding at the end. let base64 = Data(SHA256.hash(data: data(secret: secret))).base64EncodedString() @@ -27,6 +34,8 @@ public struct OpenSSHKeyWriter { return "SHA256:\(cleaned)" } + /// Generates an OpenSSH MD5 fingerprint string. + /// - Returns: OpenSSH MD5 fingerprint string. public func openSSHMD5Fingerprint(secret: SecretType) -> String { Insecure.MD5.hash(data: data(secret: secret)) .compactMap { ("0" + String($0, radix: 16, uppercase: false)).suffix(2) } @@ -37,23 +46,37 @@ public struct OpenSSHKeyWriter { extension OpenSSHKeyWriter { + /// Creates an OpenSSH protocol style data object, which has a length header, followed by the data payload. + /// - Parameter data: The data payload. + /// - Returns: OpenSSH data. public func lengthAndData(of data: Data) -> Data { let rawLength = UInt32(data.count) var endian = rawLength.bigEndian return Data(bytes: &endian, count: UInt32.bitWidth/8) + data } - public func curveIdentifier(for algorithm: Algorithm, length: Int) -> String { - switch algorithm { - case .ellipticCurve: - return "nistp" + String(describing: length) - } - } - + /// The fully qualified OpenSSH identifier for the algorithm. + /// - Parameters: + /// - algorithm: The algorithm to identify. + /// - length: The key length of the algorithm. + /// - Returns: The OpenSSH identifier for the algorithm. public func curveType(for algorithm: Algorithm, length: Int) -> String { switch algorithm { case .ellipticCurve: return "ecdsa-sha2-nistp" + String(describing: length) } } + + /// The OpenSSH identifier for an algorithm. + /// - Parameters: + /// - algorithm: The algorithm to identify. + /// - length: The key length of the algorithm. + /// - Returns: The OpenSSH identifier for the algorithm. + private func curveIdentifier(for algorithm: Algorithm, length: Int) -> String { + switch algorithm { + case .ellipticCurve: + return "nistp" + String(describing: length) + } + } + } diff --git a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHReader.swift b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHReader.swift index 5e89055..027b63d 100644 --- a/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHReader.swift +++ b/Sources/Packages/Sources/SecretKit/OpenSSH/OpenSSHReader.swift @@ -1,13 +1,18 @@ import Foundation +/// Reads OpenSSH protocol data. public class OpenSSHReader { var remaining: Data + /// Initialize the reader with an OpenSSH data payload. + /// - Parameter data: The data to read. public init(data: Data) { remaining = Data(data) } + /// Reads the next chunk of data from the playload. + /// - Returns: The next chunk of data. public func readNextChunk() -> Data { let lengthRange = 0..<(UInt32.bitWidth/8) let lengthChunk = remaining[lengthRange] diff --git a/Sources/Packages/Sources/SecretKit/SecretStoreList.swift b/Sources/Packages/Sources/SecretKit/SecretStoreList.swift index 0263a76..57fed98 100644 --- a/Sources/Packages/Sources/SecretKit/SecretStoreList.swift +++ b/Sources/Packages/Sources/SecretKit/SecretStoreList.swift @@ -1,25 +1,32 @@ import Foundation import Combine +/// A "Store Store," which holds a list of type-erased stores. public class SecretStoreList: ObservableObject { + /// The Stores managed by the SecretStoreList. @Published public var stores: [AnySecretStore] = [] + /// A modifiable store, if one is available. @Published public var modifiableStore: AnySecretStoreModifiable? private var sinks: [AnyCancellable] = [] + /// Initializes a SecretStoreList. public init() { } + /// Adds a non-type-erased SecretStore to the list. public func add(store: SecretStoreType) { addInternal(store: AnySecretStore(store)) } + /// Adds a non-type-erased modifiable SecretStore. public func add(store: SecretStoreType) { let modifiable = AnySecretStoreModifiable(modifiable: store) modifiableStore = modifiable addInternal(store: modifiable) } + /// A boolean describing whether there are any Stores available. public var anyAvailable: Bool { stores.reduce(false, { $0 || $1.isAvailable }) } diff --git a/Sources/Packages/Sources/SecretKit/Types/Secret.swift b/Sources/Packages/Sources/SecretKit/Types/Secret.swift index 1df3bf1..154a2d3 100644 --- a/Sources/Packages/Sources/SecretKit/Types/Secret.swift +++ b/Sources/Packages/Sources/SecretKit/Types/Secret.swift @@ -1,16 +1,26 @@ import Foundation +/// The base protocol for describing a Secret public protocol Secret: Identifiable, Hashable { + /// A user-facing string identifying the Secret. var name: String { get } + /// The algorithm this secret uses. var algorithm: Algorithm { get } + /// The key size for the secret. var keySize: Int { get } + /// The public key data for the secret. var publicKey: Data { get } } +/// The type of algorithm the Secret uses. Currently, only elliptic curve algorithms are supported. public enum Algorithm: Hashable { + case ellipticCurve + + /// Initializes the Algorithm with a secAttr representation of an algorithm. + /// - Parameter secAttr: the secAttr, represented as an NSNumber. public init(secAttr: NSNumber) { let secAttrString = secAttr.stringValue as CFString switch secAttrString { diff --git a/Sources/Packages/Sources/SecretKit/Types/SecretStore.swift b/Sources/Packages/Sources/SecretKit/Types/SecretStore.swift index 2f835c8..6edef38 100644 --- a/Sources/Packages/Sources/SecretKit/Types/SecretStore.swift +++ b/Sources/Packages/Sources/SecretKit/Types/SecretStore.swift @@ -1,25 +1,55 @@ import Foundation import Combine +/// Manages access to Secrets, and performs signature operations on data using those Secrets. public protocol SecretStore: ObservableObject, Identifiable { associatedtype SecretType: Secret + /// A boolean indicating whether or not the store is available. var isAvailable: Bool { get } + /// A unique identifier for the store. var id: UUID { get } + /// A user-facing name for the store. var name: String { get } + /// The secrets the store manages. var secrets: [SecretType] { get } + /// Signs a data payload with a specified Secret. + /// - Parameters: + /// - data: The data to sign. + /// - secret: The ``Secret`` to sign with. + /// - provenance: A ``SigningRequestProvenance`` describing where the request came from. + /// - Returns: A ``SignedData`` object, containing the signature and metadata about the signature process. func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> SignedData + /// Persists user authorization for access to a secret. + /// - Parameters: + /// - secret: The ``Secret`` to persist the authorization for. + /// - duration: The duration that the authorization should persist for. + /// - Note: This is used for temporarily unlocking access to a secret which would otherwise require authentication every single use. This is useful for situations where the user anticipates several rapid accesses to a authorization-guarded secret. func persistAuthentication(secret: SecretType, forDuration duration: TimeInterval) throws } +/// A SecretStore that the Secretive admin app can modify. public protocol SecretStoreModifiable: SecretStore { + /// Creates a new ``Secret`` in the store. + /// - Parameters: + /// - name: The user-facing name for the ``Secret``. + /// - requiresAuthentication: A boolean indicating whether or not the user will be required to authenticate before performing signature operations with the secret. func create(name: String, requiresAuthentication: Bool) throws + + /// Deletes a Secret in the store. + /// - Parameters: + /// - secret: The ``Secret`` to delete. func delete(secret: SecretType) throws + + /// Updates the name of a Secret in the store. + /// - Parameters: + /// - secret: The ``Secret`` to update. + /// - name: The new name for the Secret. func update(secret: SecretType, name: String) throws } diff --git a/Sources/Packages/Sources/SecretKit/Types/SignedData.swift b/Sources/Packages/Sources/SecretKit/Types/SignedData.swift index bcaf171..1468867 100644 --- a/Sources/Packages/Sources/SecretKit/Types/SignedData.swift +++ b/Sources/Packages/Sources/SecretKit/Types/SignedData.swift @@ -1,10 +1,17 @@ import Foundation +/// Describes the output of a sign request. public struct SignedData { + /// The signed data. public let data: Data + /// A boolean describing whether authentication was required during the signature process. public let requiredAuthentication: Bool + /// Initializes a new SignedData. + /// - Parameters: + /// - data: The signed data. + /// - requiredAuthentication: A boolean describing whether authentication was required during the signature process. public init(data: Data, requiredAuthentication: Bool) { self.data = data self.requiredAuthentication = requiredAuthentication diff --git a/Sources/Packages/Sources/SecretKit/Types/SigningRequestProvenance.swift b/Sources/Packages/Sources/SecretKit/Types/SigningRequestProvenance.swift index 271c8c0..a1095fd 100644 --- a/Sources/Packages/Sources/SecretKit/Types/SigningRequestProvenance.swift +++ b/Sources/Packages/Sources/SecretKit/Types/SigningRequestProvenance.swift @@ -1,8 +1,11 @@ import Foundation import AppKit +/// Describes the chain of applications that requested a signature operation. public struct SigningRequestProvenance: Equatable { + /// A list of processes involved in the request. + /// - Note: A chain will typically consist of many elements even for a simple request. For example, running `git fetch` in Terminal.app would generate a request chain of `ssh` -> `git` -> `zsh` -> `login` -> `Terminal.app` public var chain: [Process] public init(root: Process) { self.chain = [root] @@ -12,10 +15,12 @@ public struct SigningRequestProvenance: Equatable { extension SigningRequestProvenance { + /// The `Process` which initiated the signing request. public var origin: Process { chain.last! } + /// A boolean describing whether all processes in the request chain had a valid code signature. public var intact: Bool { chain.allSatisfy { $0.validSignature } } @@ -24,16 +29,33 @@ extension SigningRequestProvenance { extension SigningRequestProvenance { + /// Describes a process in a `SigningRequestProvenance` chain. public struct Process: Equatable { + /// The pid of the process. public let pid: Int32 + /// A user-facing name for the process. public let processName: String + /// A user-facing name for the application, if one exists. public let appName: String? + /// An icon representation of the application, if one exists. public let iconURL: URL? + /// The path the process exists at. public let path: String + /// A boolean describing whether or not the process has a valid code signature. public let validSignature: Bool + /// The pid of the process's parent. public let parentPID: Int32? + /// Initializes a Process. + /// - Parameters: + /// - pid: The pid of the process. + /// - processName: A user-facing name for the process. + /// - appName: A user-facing name for the application, if one exists. + /// - iconURL: An icon representation of the application, if one exists. + /// - path: The path the process exists at. + /// - validSignature: A boolean describing whether or not the process has a valid code signature. + /// - parentPID: The pid of the process's parent. public init(pid: Int32, processName: String, appName: String?, iconURL: URL?, path: String, validSignature: Bool, parentPID: Int32?) { self.pid = pid self.processName = processName @@ -44,6 +66,7 @@ extension SigningRequestProvenance { self.parentPID = parentPID } + /// The best user-facing name to display for the process. public var displayName: String { appName ?? processName } diff --git a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift index c50ccf2..9e2fb95 100644 --- a/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift +++ b/Sources/Packages/Sources/SecureEnclaveSecretKit/SecureEnclaveStore.swift @@ -97,7 +97,6 @@ extension SecureEnclave { } reloadSecrets() } - public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> SignedData { let context: LAContext if let existing = persistedAuthenticationContexts[secret], existing.valid { @@ -141,6 +140,10 @@ extension SecureEnclave { return SignedData(data: signature as Data, requiredAuthentication: requiredAuthentication) } + /// <#Description#> + /// - Parameters: + /// - secret: <#secret description#> + /// - duration: <#duration description#> public func persistAuthentication(secret: Secret, forDuration duration: TimeInterval) throws { let newContext = LAContext() newContext.touchIDAuthenticationAllowableReuseDuration = duration diff --git a/Sources/Secretive.xcodeproj/project.pbxproj b/Sources/Secretive.xcodeproj/project.pbxproj index dd4c6d3..d66060b 100644 --- a/Sources/Secretive.xcodeproj/project.pbxproj +++ b/Sources/Secretive.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 2C4A9D2F2636FFD3008CC8E2 /* RenameSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4A9D2E2636FFD3008CC8E2 /* RenameSecretView.swift */; }; 50020BB024064869003D4025 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50020BAF24064869003D4025 /* AppDelegate.swift */; }; + 50033AC327813F1700253856 /* BundleIDs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50033AC227813F1700253856 /* BundleIDs.swift */; }; 5003EF3B278005E800DF2006 /* SecretKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF3A278005E800DF2006 /* SecretKit */; }; 5003EF3D278005F300DF2006 /* Brief in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF3C278005F300DF2006 /* Brief */; }; 5003EF3F278005F300DF2006 /* SecretAgentKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5003EF3E278005F300DF2006 /* SecretAgentKit */; }; @@ -104,6 +105,7 @@ /* Begin PBXFileReference section */ 2C4A9D2E2636FFD3008CC8E2 /* RenameSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenameSecretView.swift; sourceTree = ""; }; 50020BAF24064869003D4025 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 50033AC227813F1700253856 /* BundleIDs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIDs.swift; sourceTree = ""; }; 5003EF39278005C800DF2006 /* Packages */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Packages; sourceTree = ""; }; 50153E1F250AFCB200525160 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = ""; }; 50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = ""; }; @@ -181,6 +183,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 50033AC427813F1C00253856 /* Helpers */ = { + isa = PBXGroup; + children = ( + 50033AC227813F1700253856 /* BundleIDs.swift */, + ); + path = Helpers; + sourceTree = ""; + }; 50617D7623FCE48D0099B055 = { isa = PBXGroup; children = ( @@ -210,6 +220,7 @@ 50617D8223FCE48E0099B055 /* App.swift */, 508A58B0241ED1C40069DC07 /* Views */, 508A58B1241ED1EA0069DC07 /* Controllers */, + 50033AC427813F1C00253856 /* Helpers */, 50617D8623FCE48E0099B055 /* Assets.xcassets */, 50617D8E23FCE48E0099B055 /* Info.plist */, 508BF28D25B4F005009EFB7E /* InternetAccessPolicy.plist */, @@ -469,6 +480,7 @@ 5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */, 50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */, 5066A6F7251829B1004B5A36 /* ShellConfigurationController.swift in Sources */, + 50033AC327813F1700253856 /* BundleIDs.swift in Sources */, 508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */, 50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */, 5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */, diff --git a/Sources/Packages/Sources/SecretKit/BundleIDs.swift b/Sources/Secretive/Helpers/BundleIDs.swift similarity index 100% rename from Sources/Packages/Sources/SecretKit/BundleIDs.swift rename to Sources/Secretive/Helpers/BundleIDs.swift