mirror of
https://github.com/maxgoedjen/secretive.git
synced 2026-04-10 11:17:24 +02:00
CryptoKit migration (#628)
* WIP. * WIP * WIP Edit * Key selection. * WIP * WIP * Proxy through * WIP * Remove verify. * Migration. * Comment * Add param * Semi-offering key * Ignore updates if test build. * Fix rsa public key gen * Messily fix RSA * Remove 1024 bit rsa * Cleanup * Cleanup * Clean out MLDSA refs for now * Dump notifier changes * Put back UI tweaks * Fixes.
This commit is contained in:
@@ -22,7 +22,7 @@ SecretKit is a collection of protocols describing secrets and stores.
|
||||
|
||||
### OpenSSH
|
||||
|
||||
- ``OpenSSHKeyWriter``
|
||||
- ``OpenSSHPublicKeyWriter``
|
||||
- ``OpenSSHReader``
|
||||
|
||||
### Signing Process
|
||||
|
||||
@@ -3,14 +3,12 @@ import Foundation
|
||||
/// Type eraser for Secret.
|
||||
public struct AnySecret: Secret, @unchecked Sendable {
|
||||
|
||||
public let base: Any
|
||||
public let base: any Secret
|
||||
private let hashable: AnyHashable
|
||||
private let _id: () -> AnyHashable
|
||||
private let _name: () -> String
|
||||
private let _algorithm: () -> Algorithm
|
||||
private let _keySize: () -> Int
|
||||
private let _requiresAuthentication: () -> Bool
|
||||
private let _publicKey: () -> Data
|
||||
private let _attributes: () -> Attributes
|
||||
|
||||
public init<T>(_ secret: T) where T: Secret {
|
||||
if let secret = secret as? AnySecret {
|
||||
@@ -18,19 +16,15 @@ public struct AnySecret: Secret, @unchecked Sendable {
|
||||
hashable = secret.hashable
|
||||
_id = secret._id
|
||||
_name = secret._name
|
||||
_algorithm = secret._algorithm
|
||||
_keySize = secret._keySize
|
||||
_requiresAuthentication = secret._requiresAuthentication
|
||||
_publicKey = secret._publicKey
|
||||
_attributes = secret._attributes
|
||||
} else {
|
||||
base = secret as Any
|
||||
base = secret
|
||||
self.hashable = secret
|
||||
_id = { secret.id as AnyHashable }
|
||||
_name = { secret.name }
|
||||
_algorithm = { secret.algorithm }
|
||||
_keySize = { secret.keySize }
|
||||
_requiresAuthentication = { secret.requiresAuthentication }
|
||||
_publicKey = { secret.publicKey }
|
||||
_attributes = { secret.attributes }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,21 +36,13 @@ public struct AnySecret: Secret, @unchecked Sendable {
|
||||
_name()
|
||||
}
|
||||
|
||||
public var algorithm: Algorithm {
|
||||
_algorithm()
|
||||
}
|
||||
|
||||
public var keySize: Int {
|
||||
_keySize()
|
||||
}
|
||||
|
||||
public var requiresAuthentication: Bool {
|
||||
_requiresAuthentication()
|
||||
}
|
||||
|
||||
public var publicKey: Data {
|
||||
_publicKey()
|
||||
}
|
||||
|
||||
public var attributes: Attributes {
|
||||
_attributes()
|
||||
}
|
||||
|
||||
public static func == (lhs: AnySecret, rhs: AnySecret) -> Bool {
|
||||
lhs.hashable == rhs.hashable
|
||||
|
||||
@@ -3,7 +3,7 @@ import Foundation
|
||||
/// Type eraser for SecretStore.
|
||||
open class AnySecretStore: SecretStore, @unchecked Sendable {
|
||||
|
||||
let base: any Sendable
|
||||
let base: any SecretStore
|
||||
private let _isAvailable: @MainActor @Sendable () -> Bool
|
||||
private let _id: @Sendable () -> UUID
|
||||
private let _name: @MainActor @Sendable () -> String
|
||||
@@ -61,27 +61,33 @@ open class AnySecretStore: SecretStore, @unchecked Sendable {
|
||||
|
||||
public final class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable, @unchecked Sendable {
|
||||
|
||||
private let _create: @Sendable (String, Bool) async throws -> Void
|
||||
private let _create: @Sendable (String, Attributes) async throws -> Void
|
||||
private let _delete: @Sendable (AnySecret) async throws -> Void
|
||||
private let _update: @Sendable (AnySecret, String) async throws -> Void
|
||||
private let _update: @Sendable (AnySecret, String, Attributes) async throws -> Void
|
||||
private let _supportedKeyTypes: @Sendable () -> [KeyType]
|
||||
|
||||
public init<SecretStoreType>(modifiable secretStore: SecretStoreType) where SecretStoreType: SecretStoreModifiable {
|
||||
_create = { try await secretStore.create(name: $0, requiresAuthentication: $1) }
|
||||
public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStoreModifiable {
|
||||
_create = { try await secretStore.create(name: $0, attributes: $1) }
|
||||
_delete = { try await secretStore.delete(secret: $0.base as! SecretStoreType.SecretType) }
|
||||
_update = { try await secretStore.update(secret: $0.base as! SecretStoreType.SecretType, name: $1) }
|
||||
_update = { try await secretStore.update(secret: $0.base as! SecretStoreType.SecretType, name: $1, attributes: $2) }
|
||||
_supportedKeyTypes = { secretStore.supportedKeyTypes }
|
||||
super.init(secretStore)
|
||||
}
|
||||
|
||||
public func create(name: String, requiresAuthentication: Bool) async throws {
|
||||
try await _create(name, requiresAuthentication)
|
||||
public func create(name: String, attributes: Attributes) async throws {
|
||||
try await _create(name, attributes)
|
||||
}
|
||||
|
||||
public func delete(secret: AnySecret) async throws {
|
||||
try await _delete(secret)
|
||||
}
|
||||
|
||||
public func update(secret: AnySecret, name: String) async throws {
|
||||
try await _update(secret, name)
|
||||
public func update(secret: AnySecret, name: String, attributes: Attributes) async throws {
|
||||
try await _update(secret, name, attributes)
|
||||
}
|
||||
|
||||
public var supportedKeyTypes: [KeyType] {
|
||||
_supportedKeyTypes()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -51,19 +51,17 @@ public extension SecretStore {
|
||||
/// Returns the appropriate keychian signature algorithm to use for a given secret.
|
||||
/// - Parameters:
|
||||
/// - secret: The secret which will be used for signing.
|
||||
/// - allowRSA: Whether or not RSA key types should be permited.
|
||||
/// - Returns: The appropriate algorithm.
|
||||
func signatureAlgorithm(for secret: SecretType, allowRSA: Bool = false) -> SecKeyAlgorithm {
|
||||
switch (secret.algorithm, secret.keySize) {
|
||||
case (.ellipticCurve, 256):
|
||||
return .ecdsaSignatureMessageX962SHA256
|
||||
case (.ellipticCurve, 384):
|
||||
return .ecdsaSignatureMessageX962SHA384
|
||||
case (.rsa, 1024), (.rsa, 2048):
|
||||
guard allowRSA else { fatalError() }
|
||||
return .rsaSignatureMessagePKCS1v15SHA512
|
||||
func signatureAlgorithm(for secret: SecretType) -> SecKeyAlgorithm? {
|
||||
switch (secret.keyType.algorithm, secret.keyType.size) {
|
||||
case (.ecdsa, 256):
|
||||
.ecdsaSignatureMessageX962SHA256
|
||||
case (.ecdsa, 384):
|
||||
.ecdsaSignatureMessageX962SHA384
|
||||
case (.rsa, 2048):
|
||||
.rsaSignatureMessagePKCS1v15SHA512
|
||||
default:
|
||||
fatalError()
|
||||
nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import Foundation
|
||||
|
||||
extension Data {
|
||||
|
||||
/// Creates an OpenSSH protocol style data object, which has a length header, followed by the data payload.
|
||||
/// - Returns: OpenSSH data.
|
||||
package var lengthAndData: Data {
|
||||
let rawLength = UInt32(count)
|
||||
var endian = rawLength.bigEndian
|
||||
return Data(bytes: &endian, count: UInt32.bitWidth/8) + self
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension String {
|
||||
|
||||
/// Creates an OpenSSH protocol style data object, which has a length header, followed by the data payload.
|
||||
/// - Returns: OpenSSH data.
|
||||
package var lengthAndData: Data {
|
||||
Data(utf8).lengthAndData
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,7 +6,7 @@ public actor 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 writer = OpenSSHPublicKeyWriter()
|
||||
private var keyBlobsAndNames: [AnySecret: (Data, Data)] = [:]
|
||||
|
||||
/// Initializes an OpenSSHCertificateHandler.
|
||||
@@ -40,10 +40,10 @@ public actor OpenSSHCertificateHandler: Sendable {
|
||||
let curveIdentifier = reader.readNextChunk()
|
||||
let publicKey = reader.readNextChunk()
|
||||
|
||||
let curveType = certType.replacingOccurrences(of: "-cert-v01@openssh.com", with: "").data(using: .utf8)!
|
||||
return writer.lengthAndData(of: curveType) +
|
||||
writer.lengthAndData(of: curveIdentifier) +
|
||||
writer.lengthAndData(of: publicKey)
|
||||
let openSSHIdentifier = certType.replacingOccurrences(of: "-cert-v01@openssh.com", with: "")
|
||||
return openSSHIdentifier.lengthAndData +
|
||||
curveIdentifier.lengthAndData +
|
||||
publicKey.lengthAndData
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
@@ -78,14 +78,13 @@ public actor OpenSSHCertificateHandler: Sendable {
|
||||
throw OpenSSHCertificateError.parsingFailed
|
||||
}
|
||||
|
||||
if certElements.count >= 3, let certName = certElements[2].data(using: .utf8) {
|
||||
if certElements.count >= 3 {
|
||||
let certName = Data(certElements[2].utf8)
|
||||
return (certDecoded, certName)
|
||||
} else if let certName = secret.name.data(using: .utf8) {
|
||||
logger.info("Certificate for \(secret.name) does not have a name tag, using secret name instead")
|
||||
return (certDecoded, certName)
|
||||
} else {
|
||||
throw OpenSSHCertificateError.parsingFailed
|
||||
}
|
||||
let certName = Data(secret.name.utf8)
|
||||
logger.info("Certificate for \(secret.name) does not have a name tag, using secret name instead")
|
||||
return (certDecoded, certName)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
||||
/// Generates OpenSSH representations of Secrets.
|
||||
public struct OpenSSHKeyWriter: Sendable {
|
||||
|
||||
/// Initializes the writer.
|
||||
public init() {
|
||||
}
|
||||
|
||||
/// Generates an OpenSSH data payload identifying the secret.
|
||||
/// - Returns: OpenSSH data payload identifying the secret.
|
||||
public func data<SecretType: Secret>(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<SecretType: Secret>(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<SecretType: Secret>(secret: SecretType) -> String {
|
||||
// OpenSSL format seems to strip the padding at the end.
|
||||
let base64 = Data(SHA256.hash(data: data(secret: secret))).base64EncodedString()
|
||||
let paddingRange = base64.index(base64.endIndex, offsetBy: -2)..<base64.endIndex
|
||||
let cleaned = base64.replacingOccurrences(of: "=", with: "", range: paddingRange)
|
||||
return "SHA256:\(cleaned)"
|
||||
}
|
||||
|
||||
/// Generates an OpenSSH MD5 fingerprint string.
|
||||
/// - Returns: OpenSSH MD5 fingerprint string.
|
||||
public func openSSHMD5Fingerprint<SecretType: Secret>(secret: SecretType) -> String {
|
||||
Insecure.MD5.hash(data: data(secret: secret))
|
||||
.compactMap { ("0" + String($0, radix: 16, uppercase: false)).suffix(2) }
|
||||
.joined(separator: ":")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/// 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)
|
||||
case .rsa:
|
||||
// All RSA keys use the same 512 bit hash function, per
|
||||
// https://security.stackexchange.com/questions/255074/why-are-rsa-sha2-512-and-rsa-sha2-256-supported-but-not-reported-by-ssh-q-key
|
||||
return "rsa-sha2-512"
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)
|
||||
case .rsa:
|
||||
// All RSA keys use the same 512 bit hash function
|
||||
return "rsa-sha2-512"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
||||
/// Generates OpenSSH representations of the public key sof secrets.
|
||||
public struct OpenSSHPublicKeyWriter: Sendable {
|
||||
|
||||
/// Initializes the writer.
|
||||
public init() {
|
||||
}
|
||||
|
||||
/// Generates an OpenSSH data payload identifying the secret.
|
||||
/// - Returns: OpenSSH data payload identifying the secret.
|
||||
public func data<SecretType: Secret>(secret: SecretType) -> Data {
|
||||
switch secret.keyType.algorithm {
|
||||
case .ecdsa:
|
||||
// https://datatracker.ietf.org/doc/html/rfc5656#section-3.1
|
||||
openSSHIdentifier(for: secret.keyType).lengthAndData +
|
||||
("nistp" + String(describing: secret.keyType.size)).lengthAndData +
|
||||
secret.publicKey.lengthAndData
|
||||
case .rsa:
|
||||
// https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
|
||||
openSSHIdentifier(for: secret.keyType).lengthAndData +
|
||||
rsaPublicKeyBlob(secret: secret)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates an OpenSSH string representation of the secret.
|
||||
/// - Returns: OpenSSH string representation of the secret.
|
||||
public func openSSHString<SecretType: Secret>(secret: SecretType) -> String {
|
||||
let resolvedComment: String
|
||||
if let comment = secret.publicKeyAttribution {
|
||||
resolvedComment = comment
|
||||
} else {
|
||||
let dashedKeyName = secret.name.replacingOccurrences(of: " ", with: "-")
|
||||
let dashedHostName = ["secretive", Host.current().localizedName, "local"]
|
||||
.compactMap { $0 }
|
||||
.joined(separator: ".")
|
||||
.replacingOccurrences(of: " ", with: "-")
|
||||
resolvedComment = "\(dashedKeyName)@\(dashedHostName)"
|
||||
}
|
||||
return [openSSHIdentifier(for: secret.keyType), data(secret: secret).base64EncodedString(), resolvedComment]
|
||||
.compactMap { $0 }
|
||||
.joined(separator: " ")
|
||||
}
|
||||
|
||||
/// Generates an OpenSSH SHA256 fingerprint string.
|
||||
/// - Returns: OpenSSH SHA256 fingerprint string.
|
||||
public func openSSHSHA256Fingerprint<SecretType: Secret>(secret: SecretType) -> String {
|
||||
// OpenSSL format seems to strip the padding at the end.
|
||||
let base64 = Data(SHA256.hash(data: data(secret: secret))).base64EncodedString()
|
||||
let paddingRange = base64.index(base64.endIndex, offsetBy: -2)..<base64.endIndex
|
||||
let cleaned = base64.replacingOccurrences(of: "=", with: "", range: paddingRange)
|
||||
return "SHA256:\(cleaned)"
|
||||
}
|
||||
|
||||
/// Generates an OpenSSH MD5 fingerprint string.
|
||||
/// - Returns: OpenSSH MD5 fingerprint string.
|
||||
public func openSSHMD5Fingerprint<SecretType: Secret>(secret: SecretType) -> String {
|
||||
Insecure.MD5.hash(data: data(secret: secret))
|
||||
.compactMap { ("0" + String($0, radix: 16, uppercase: false)).suffix(2) }
|
||||
.joined(separator: ":")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension OpenSSHPublicKeyWriter {
|
||||
|
||||
/// 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 openSSHIdentifier(for keyType: KeyType) -> String {
|
||||
switch (keyType.algorithm, keyType.size) {
|
||||
case (.ecdsa, 256), (.ecdsa, 384):
|
||||
"ecdsa-sha2-nistp" + String(describing: keyType.size)
|
||||
case (.rsa, _):
|
||||
"ssh-rsa"
|
||||
default:
|
||||
"unknown"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension OpenSSHPublicKeyWriter {
|
||||
|
||||
public func rsaPublicKeyBlob<SecretType: Secret>(secret: SecretType) -> Data {
|
||||
// Cheap way to pull out e and n as defined in https://datatracker.ietf.org/doc/html/rfc4253
|
||||
// Keychain stores it as a thin ASN.1 wrapper with this format:
|
||||
// [4 byte prefix][2 byte prefix][n][2 byte prefix][e]
|
||||
// Rather than parse out the whole ASN.1 blob, we'll cheat and pull values directly since
|
||||
// we only support one key type, and the keychain always gives it in a specific format.
|
||||
let keySize = secret.keyType.size
|
||||
guard secret.keyType.algorithm == .rsa && keySize == 2048 else { fatalError() }
|
||||
let length = secret.keyType.size/8
|
||||
let data = secret.publicKey
|
||||
let n = Data(data[8..<(9+length)])
|
||||
let e = Data(data[(2+9+length)...])
|
||||
return e.lengthAndData + n.lengthAndData
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,9 +17,7 @@ public final class OpenSSHReader {
|
||||
let lengthRange = 0..<(UInt32.bitWidth/8)
|
||||
let lengthChunk = remaining[lengthRange]
|
||||
remaining.removeSubrange(lengthRange)
|
||||
let littleEndianLength = lengthChunk.withUnsafeBytes { pointer in
|
||||
return pointer.load(as: UInt32.self)
|
||||
}
|
||||
let littleEndianLength = lengthChunk.bytes.unsafeLoad(as: UInt32.self)
|
||||
let length = Int(littleEndianLength.bigEndian)
|
||||
let dataRange = 0..<length
|
||||
let ret = Data(remaining[dataRange])
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
||||
/// Generates OpenSSH representations of Secrets.
|
||||
public struct OpenSSHSignatureWriter: Sendable {
|
||||
|
||||
/// Initializes the writer.
|
||||
public init() {
|
||||
}
|
||||
|
||||
/// Generates an OpenSSH data payload identifying the secret.
|
||||
/// - Returns: OpenSSH data payload identifying the secret.
|
||||
public func data<SecretType: Secret>(secret: SecretType, signature: Data) -> Data {
|
||||
switch secret.keyType.algorithm {
|
||||
case .ecdsa:
|
||||
// https://datatracker.ietf.org/doc/html/rfc5656#section-3.1
|
||||
ecdsaSignature(signature, keyType: secret.keyType)
|
||||
case .rsa:
|
||||
// https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
|
||||
rsaSignature(signature)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
extension OpenSSHSignatureWriter {
|
||||
|
||||
func ecdsaSignature(_ rawRepresentation: Data, keyType: KeyType) -> Data {
|
||||
let rawLength = rawRepresentation.count/2
|
||||
// Check if we need to pad with 0x00 to prevent certain
|
||||
// ssh servers from thinking r or s is negative
|
||||
let paddingRange: ClosedRange<UInt8> = 0x80...0xFF
|
||||
var r = Data(rawRepresentation[0..<rawLength])
|
||||
if paddingRange ~= r.first! {
|
||||
r.insert(0x00, at: 0)
|
||||
}
|
||||
var s = Data(rawRepresentation[rawLength...])
|
||||
if paddingRange ~= s.first! {
|
||||
s.insert(0x00, at: 0)
|
||||
}
|
||||
|
||||
var signatureChunk = Data()
|
||||
signatureChunk.append(r.lengthAndData)
|
||||
signatureChunk.append(s.lengthAndData)
|
||||
var mutSignedData = Data()
|
||||
var sub = Data()
|
||||
sub.append(OpenSSHPublicKeyWriter().openSSHIdentifier(for: keyType).lengthAndData)
|
||||
sub.append(signatureChunk.lengthAndData)
|
||||
mutSignedData.append(sub.lengthAndData)
|
||||
return mutSignedData
|
||||
}
|
||||
|
||||
func rsaSignature(_ rawRepresentation: Data) -> Data {
|
||||
var mutSignedData = Data()
|
||||
var sub = Data()
|
||||
sub.append("rsa-sha2-512".lengthAndData)
|
||||
sub.append(rawRepresentation.lengthAndData)
|
||||
mutSignedData.append(sub.lengthAndData)
|
||||
return mutSignedData
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,7 +6,7 @@ public final class PublicKeyFileStoreController: Sendable {
|
||||
|
||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "PublicKeyFileStoreController")
|
||||
private let directory: String
|
||||
private let keyWriter = OpenSSHKeyWriter()
|
||||
private let keyWriter = OpenSSHPublicKeyWriter()
|
||||
|
||||
/// Initializes a PublicKeyFileStoreController.
|
||||
public init(homeDirectory: String) {
|
||||
@@ -32,7 +32,7 @@ public final class PublicKeyFileStoreController: Sendable {
|
||||
try? FileManager.default.createDirectory(at: URL(fileURLWithPath: directory), withIntermediateDirectories: false, attributes: nil)
|
||||
for secret in secrets {
|
||||
let path = publicKeyPath(for: secret)
|
||||
guard let data = keyWriter.openSSHString(secret: secret).data(using: .utf8) else { continue }
|
||||
let data = Data(keyWriter.openSSHString(secret: secret).utf8)
|
||||
FileManager.default.createFile(atPath: path, contents: data, attributes: nil)
|
||||
}
|
||||
logger.log("Finished writing public keys")
|
||||
|
||||
@@ -20,7 +20,7 @@ import Observation
|
||||
|
||||
/// Adds a non-type-erased modifiable SecretStore.
|
||||
public func add<SecretStoreType: SecretStoreModifiable>(store: SecretStoreType) {
|
||||
let modifiable = AnySecretStoreModifiable(modifiable: store)
|
||||
let modifiable = AnySecretStoreModifiable(store)
|
||||
if modifiableStore == nil {
|
||||
modifiableStore = modifiable
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import Foundation
|
||||
|
||||
public struct Attributes: Sendable, Codable, Hashable {
|
||||
|
||||
/// The type of key involved.
|
||||
public let keyType: KeyType
|
||||
|
||||
/// The authentication requirements for the key. This is simply a description of the option recorded at creation – modifying it doers not modify the key's authentication requirements.
|
||||
public let authentication: AuthenticationRequirement
|
||||
|
||||
/// The string appended to the end of the SSH Public Key.
|
||||
/// If nil, a default value will be used.
|
||||
public var publicKeyAttribution: String?
|
||||
|
||||
public init(
|
||||
keyType: KeyType,
|
||||
authentication: AuthenticationRequirement,
|
||||
publicKeyAttribution: String? = nil
|
||||
) {
|
||||
self.keyType = keyType
|
||||
self.authentication = authentication
|
||||
self.publicKeyAttribution = publicKeyAttribution
|
||||
}
|
||||
|
||||
public struct UnsupportedOptionError: Error {
|
||||
package init() {}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// The option specified
|
||||
public enum AuthenticationRequirement: String, Hashable, Sendable, Codable, Identifiable {
|
||||
|
||||
/// Authentication is not required for usage.
|
||||
case notRequired
|
||||
|
||||
/// The user needs to authenticate, using either a biometric option, a connected authorized watch, or password entry..
|
||||
case presenceRequired
|
||||
|
||||
/// ONLY the current set of biometric data, as matching at time of creation, is accepted.
|
||||
/// - Warning: This is a dangerous option prone to data loss. The user should be warned before configuring this key that if they modify their enrolled biometry INCLUDING by simply adding a new entry (ie, adding another fingeprting), the key will no longer be able to be accessed. This cannot be overridden with a password.
|
||||
case biometryCurrent
|
||||
|
||||
/// The authentication requirement was not recorded at creation, and is unknown.
|
||||
case unknown
|
||||
|
||||
/// Whether or not the key is known to require authentication.
|
||||
public var required: Bool {
|
||||
self == .presenceRequired || self == .biometryCurrent
|
||||
}
|
||||
|
||||
public var id: AuthenticationRequirement {
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -5,43 +5,72 @@ public protocol Secret: Identifiable, Hashable, Sendable {
|
||||
|
||||
/// 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 }
|
||||
/// Whether the secret requires authentication before use.
|
||||
var requiresAuthentication: Bool { get }
|
||||
/// The public key data for the secret.
|
||||
var publicKey: Data { get }
|
||||
/// The attributes of the key.
|
||||
var attributes: Attributes { get }
|
||||
|
||||
}
|
||||
|
||||
/// The type of algorithm the Secret uses. Currently, only elliptic curve algorithms are supported.
|
||||
public enum Algorithm: Hashable, Sendable {
|
||||
public extension Secret {
|
||||
|
||||
case ellipticCurve
|
||||
case rsa
|
||||
/// The algorithm and key size this secret uses.
|
||||
var keyType: KeyType {
|
||||
attributes.keyType
|
||||
}
|
||||
|
||||
/// Whether the secret requires authentication before use.
|
||||
var authenticationRequirement: AuthenticationRequirement {
|
||||
attributes.authentication
|
||||
}
|
||||
/// An attribution string to apply to the generated public key.
|
||||
var publicKeyAttribution: String? {
|
||||
attributes.publicKeyAttribution
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// The type of algorithm the Secret uses.
|
||||
public struct KeyType: Hashable, Sendable, Codable, CustomStringConvertible {
|
||||
|
||||
public enum Algorithm: Hashable, Sendable, Codable {
|
||||
case ecdsa
|
||||
case rsa
|
||||
}
|
||||
|
||||
public var algorithm: Algorithm
|
||||
public var size: Int
|
||||
|
||||
public init(algorithm: Algorithm, size: Int) {
|
||||
self.algorithm = algorithm
|
||||
self.size = size
|
||||
}
|
||||
|
||||
/// Initializes the Algorithm with a secAttr representation of an algorithm.
|
||||
/// - Parameter secAttr: the secAttr, represented as an NSNumber.
|
||||
public init(secAttr: NSNumber) {
|
||||
public init?(secAttr: NSNumber, size: Int) {
|
||||
let secAttrString = secAttr.stringValue as CFString
|
||||
switch secAttrString {
|
||||
case kSecAttrKeyTypeEC:
|
||||
self = .ellipticCurve
|
||||
algorithm = .ecdsa
|
||||
case kSecAttrKeyTypeRSA:
|
||||
self = .rsa
|
||||
algorithm = .rsa
|
||||
default:
|
||||
fatalError()
|
||||
return nil
|
||||
}
|
||||
self.size = size
|
||||
}
|
||||
|
||||
public var secAttrKeyType: CFString? {
|
||||
switch algorithm {
|
||||
case .ecdsa:
|
||||
kSecAttrKeyTypeEC
|
||||
case .rsa:
|
||||
kSecAttrKeyTypeRSA
|
||||
}
|
||||
}
|
||||
|
||||
public var secAttrKeyType: CFString {
|
||||
switch self {
|
||||
case .ellipticCurve:
|
||||
return kSecAttrKeyTypeEC
|
||||
case .rsa:
|
||||
return kSecAttrKeyTypeRSA
|
||||
}
|
||||
public var description: String {
|
||||
"\(algorithm)-\(size)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Foundation
|
||||
|
||||
/// Manages access to Secrets, and performs signature operations on data using those Secrets.
|
||||
public protocol SecretStore: Identifiable, Sendable {
|
||||
public protocol SecretStore<SecretType>: Identifiable, Sendable {
|
||||
|
||||
associatedtype SecretType: Secret
|
||||
|
||||
@@ -41,13 +41,13 @@ public protocol SecretStore: Identifiable, Sendable {
|
||||
}
|
||||
|
||||
/// A SecretStore that the Secretive admin app can modify.
|
||||
public protocol SecretStoreModifiable: SecretStore {
|
||||
public protocol SecretStoreModifiable<SecretType>: 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) async throws
|
||||
/// - attributes: A struct describing the options for creating the key.
|
||||
func create(name: String, attributes: Attributes) async throws
|
||||
|
||||
/// Deletes a Secret in the store.
|
||||
/// - Parameters:
|
||||
@@ -58,7 +58,10 @@ public protocol SecretStoreModifiable: SecretStore {
|
||||
/// - Parameters:
|
||||
/// - secret: The ``Secret`` to update.
|
||||
/// - name: The new name for the Secret.
|
||||
func update(secret: SecretType, name: String) async throws
|
||||
/// - attributes: The new attributes for the secret.
|
||||
func update(secret: SecretType, name: String, attributes: Attributes) async throws
|
||||
|
||||
var supportedKeyTypes: [KeyType] { get }
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user