mirror of
https://github.com/maxgoedjen/secretive.git
synced 2026-09-19 14:48:01 +02:00
WIP
This commit is contained in:
@@ -5372,6 +5372,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"auth_context_request_multiple" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "sign multiple requests from “%1$(appName)@” using secret “%2$(secretName)@”"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"auth_context_request_signature_description" : {
|
||||
"comment" : "When the user performs a signature action using a secret, they are shown a prompt to approve the action. This is the description, showing which secret will be used, and where the request is coming from. The first placeholder is the name of the app requesting the operation. The second placeholder is the name of the secret.",
|
||||
"extractionState" : "manual",
|
||||
@@ -18843,6 +18854,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Multiple authenticated requests are pending. You can approve them batches, or request they all proceed individually." : {
|
||||
|
||||
},
|
||||
"no_secure_storage_description" : {
|
||||
"extractionState" : "manual",
|
||||
|
||||
+61
-47
@@ -1,57 +1,71 @@
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
struct MultilineInfoView: View {
|
||||
public struct MultilineInfoView<TitleView: View, ItemView: View>: View {
|
||||
|
||||
struct Item {
|
||||
let text: String
|
||||
let action: (Image, () -> Void)?
|
||||
// public struct Item {
|
||||
// public let text: String
|
||||
// public let action: (Image, () -> Void)?
|
||||
//
|
||||
// public init(text: String, action: (Image, () -> Void)?) {
|
||||
// self.text = text
|
||||
// self.action = action
|
||||
// }
|
||||
//
|
||||
// }
|
||||
|
||||
var titleView: TitleView
|
||||
var items: [ItemView]
|
||||
|
||||
public init(titleView: () -> TitleView, items: () -> [ItemView]) {
|
||||
self.titleView = titleView()
|
||||
self.items = items()
|
||||
}
|
||||
|
||||
var title: LocalizedStringResource
|
||||
var image: Image
|
||||
var items: [Item]
|
||||
|
||||
init(title: LocalizedStringResource, image: Image, items: [Item]) {
|
||||
self.title = title
|
||||
self.image = image
|
||||
self.items = items
|
||||
}
|
||||
|
||||
init(title: LocalizedStringResource, image: Image, items: [String]) {
|
||||
self.title = title
|
||||
self.image = image
|
||||
self.items = items.map({ Item(text: $0, action: nil) })
|
||||
}
|
||||
|
||||
@State private var interactionState: InteractionState = .normal
|
||||
@State private var interactionStateIndex: Int?
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
public init(title: LocalizedStringResource, subtitle: LocalizedStringResource, image: Image, items: [String]) where TitleView == HStack<TupleView<(Image, Text, Spacer)>> , ItemView == Text {
|
||||
self.init {
|
||||
HStack {
|
||||
image
|
||||
.renderingMode(.template)
|
||||
.imageScale(.large)
|
||||
// .imageScale(.large)
|
||||
.foregroundColor(primaryTextColor)
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
.foregroundColor(primaryTextColor)
|
||||
Spacer()
|
||||
}
|
||||
} items: {
|
||||
[Text("Hello")]
|
||||
}
|
||||
|
||||
// self.init {
|
||||
// } items: {
|
||||
// ForEach(items) { item in
|
||||
// return HStack {
|
||||
// Text(item)
|
||||
// Spacer()
|
||||
// // if let (image, _) = $0.1 {
|
||||
// // image
|
||||
// // .foregroundStyle(.secondary)
|
||||
// // }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
@State private var interactionState: InteractionState = .normal
|
||||
@State private var interactionStateIndex: Int?
|
||||
|
||||
public var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
titleView
|
||||
.safeAreaPadding(20)
|
||||
ForEach(Array(items.enumerated()), id: \.offset) { item in
|
||||
Divider()
|
||||
.ignoresSafeArea()
|
||||
.opacity(item.offset == 0 ? 1 : 0.75)
|
||||
HStack {
|
||||
Text(item.element.text)
|
||||
Spacer()
|
||||
if let (image, _) = item.element.action {
|
||||
image
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
items.element
|
||||
.safeAreaPadding(20)
|
||||
.onHover { hovering in
|
||||
withAnimation {
|
||||
@@ -155,16 +169,16 @@ fileprivate struct BackgroundViewModifier: ViewModifier {
|
||||
|
||||
}
|
||||
|
||||
#Preview {
|
||||
MultilineInfoView(title: "Multiple", image: Image(systemName: "figure.wave"), items: [
|
||||
MultilineInfoView.Item(text: "hello", action: (Image(systemName: "chevron.forward"), {})),
|
||||
MultilineInfoView.Item(text: "World", action: (Image(systemName: "chevron.forward"), {})),
|
||||
])
|
||||
.padding()
|
||||
}
|
||||
|
||||
|
||||
#Preview {
|
||||
MultilineInfoView(title: "One", image: Image(systemName: "figure.wave"), items: ["Hello world."])
|
||||
.padding()
|
||||
}
|
||||
//#Preview {
|
||||
// MultilineInfoView(title: "Multiple", image: Image(systemName: "figure.wave"), items: [
|
||||
// MultilineInfoView.Item(text: "hello", action: (Image(systemName: "chevron.forward"), {})),
|
||||
// MultilineInfoView.Item(text: "World", action: (Image(systemName: "chevron.forward"), {})),
|
||||
// ])
|
||||
// .padding()
|
||||
//}
|
||||
//
|
||||
//
|
||||
//#Preview {
|
||||
// MultilineInfoView(title: "One", image: Image(systemName: "figure.wave"), items: ["Hello world."])
|
||||
// .padding()
|
||||
//}
|
||||
@@ -133,20 +133,6 @@ extension Agent {
|
||||
}
|
||||
|
||||
func signWithRequiredAuthentication(data: Data, store: AnySecretStore, secret: AnySecret, provenance: SigningRequestProvenance) async throws -> Data {
|
||||
// let context: any AuthenticationContextProtocol
|
||||
// let offerPersistence: Bool
|
||||
// if let existing = await authenticationHandler.existingAuthenticationContextProtocol(for: SignatureRequest(secret: secret, provenance: provenance)) {
|
||||
// context = existing
|
||||
// offerPersistence = false
|
||||
// logger.debug("Using existing auth context")
|
||||
// } else {
|
||||
// context = authenticationHandler.createAuthenticationContext(for: SignatureRequest(secret: secret, provenance: provenance))
|
||||
// offerPersistence = secret.authenticationRequirement.required
|
||||
// logger.debug("Creating fresh auth context")
|
||||
// }
|
||||
|
||||
|
||||
|
||||
let context = try await authenticationHandler.waitForAuthentication(for: SignatureRequest(secret: secret, provenance: provenance))
|
||||
let result = try await store.sign(data: data, with: secret, for: provenance, context: context.laContext)
|
||||
let signedData = signatureWriter.data(secret: secret, signature: result)
|
||||
|
||||
@@ -57,20 +57,28 @@ public final class AuthenticationContext: AuthenticationContextProtocol {
|
||||
|
||||
}
|
||||
|
||||
public actor AuthenticationHandler {
|
||||
@MainActor public protocol AuthenticationHandlerProtocol: Observable {
|
||||
func setBatchAuthHandler(_ handler: @escaping () async throws -> Void)
|
||||
func waitForAuthentication(for request: SignatureRequest) async throws -> any AuthenticationContextProtocol
|
||||
var batchableRequests: [[SignatureRequest]] { get }
|
||||
func persistAuthentication<SecretType: Secret>(secret: SecretType, forDuration duration: TimeInterval) async throws
|
||||
func requestAuthentication(for requests: Set<SignatureRequest>) async throws
|
||||
}
|
||||
|
||||
@Observable @MainActor public class AuthenticationHandler: AuthenticationHandlerProtocol {
|
||||
|
||||
private var persistedContexts: [AnySecret: AuthenticationContext] = [:]
|
||||
private var holdingRequests: Set<SignatureRequest> = []
|
||||
private var activeTask: Task<Void, any Error>?
|
||||
|
||||
private var lastBatchAuthPresentation: Set<SignatureRequest>?
|
||||
private var presentBatchAuth: (([[SignatureRequest]], @escaping @Sendable (Set<SignatureRequest>) async throws -> Void) async throws -> Void)?
|
||||
private var presentBatchAuth: (() async throws -> Void)?
|
||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "Agent")
|
||||
|
||||
public init() {
|
||||
}
|
||||
|
||||
public func setBatchAuthHandler(_ handler: @escaping (@Sendable ([[SignatureRequest]], @escaping @Sendable (Set<SignatureRequest>) async throws -> Void) async throws -> Void)) {
|
||||
public func setBatchAuthHandler(_ handler: @escaping () async throws -> Void) {
|
||||
self.presentBatchAuth = handler
|
||||
}
|
||||
|
||||
@@ -92,7 +100,7 @@ public actor AuthenticationHandler {
|
||||
activeTask?.cancel()
|
||||
lastBatchAuthPresentation = holdingRequests
|
||||
logger.log("Requesting batch auth presentation")
|
||||
try await presentBatchAuth?(batchableRequests, persistAuthentication(for:))
|
||||
try await presentBatchAuth?()
|
||||
logger.log("Requested batch auth presentation")
|
||||
}
|
||||
if let preauthorized = existingAuthenticationContext(for: request) {
|
||||
@@ -110,11 +118,10 @@ public actor AuthenticationHandler {
|
||||
|
||||
activeTask = Task {
|
||||
logger.log("Beginning individual auth prompt")
|
||||
try await Task.sleep(for: .seconds(1000))
|
||||
// _ = try? await laContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: laContext.localizedReason)
|
||||
_ = try? await laContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: laContext.localizedReason)
|
||||
logger.log("Ended individual auth prompt")
|
||||
}
|
||||
_ = try await activeTask?.value
|
||||
_ = try? await activeTask?.value
|
||||
// TODO: Check something beyond cancellation? id?
|
||||
// Is this okay? Do we always assume that a cancelled task will be the proceeded on?
|
||||
if activeTask?.isCancelled ?? false {
|
||||
@@ -131,7 +138,7 @@ public actor AuthenticationHandler {
|
||||
return context
|
||||
}
|
||||
|
||||
private var batchableRequests: [[SignatureRequest]] {
|
||||
public var batchableRequests: [[SignatureRequest]] {
|
||||
holdingRequests.reduce(into: [:]) { partialResult, next in
|
||||
partialResult[next.batchID, default: []].append(next)
|
||||
}
|
||||
@@ -167,14 +174,19 @@ public actor AuthenticationHandler {
|
||||
persistedContexts[AnySecret(secret)] = context
|
||||
}
|
||||
|
||||
private func persistAuthentication(for requests: Set<SignatureRequest>) async throws {
|
||||
public func requestAuthentication(for requests: Set<SignatureRequest>) async throws {
|
||||
activeTask?.cancel()
|
||||
guard let first = requests.first else { return }
|
||||
let newContext = LAContext()
|
||||
newContext.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
|
||||
|
||||
newContext.localizedReason = String("Multiple")
|
||||
// newContext.localizedReason = String(localized: .authContextPersistForDuration(secretName: secret.name, duration: durationString))
|
||||
let appNames = Set(requests.map(\.provenance.origin.displayName)).joined(separator: ", ")
|
||||
let secretNames = Set(requests.map(\.secret.name)).joined(separator: ", ")
|
||||
if requests.count > 1 {
|
||||
newContext.localizedReason = String(localized: .authContextRequestMultiple(appName: appNames, secretName: secretNames))
|
||||
} else {
|
||||
newContext.localizedReason = String(localized: .authContextRequestSignatureDescription(appName: appNames, secretName: secretNames))
|
||||
}
|
||||
let success = try await newContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: newContext.localizedReason)
|
||||
guard success else { return }
|
||||
let context = AuthenticationContext(secret: first.secret, context: newContext, requestIDs: Set(requests.map(\.id)))
|
||||
|
||||
@@ -44,6 +44,13 @@ extension SigningRequestTracer {
|
||||
|
||||
let pathPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: Int(MAXPATHLEN))
|
||||
_ = unsafe proc_pidpath(pid, pathPointer, UInt32(MAXPATHLEN))
|
||||
|
||||
// let bufferLength = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, nil, 0)
|
||||
let x = UnsafeMutablePointer<UInt64>.allocate(capacity: Int(MAXPATHLEN))
|
||||
_ = unsafe proc_pidinfo(pid, PROC_PIDLISTFDS, 0, x, Int32(MAXPATHLEN))
|
||||
let buffer = unsafe UnsafeBufferPointer(start: x, count: Int(MAXPATHLEN)/64)
|
||||
unsafe print(buffer)
|
||||
|
||||
let path = unsafe String(cString: pathPointer)
|
||||
var secCode: Unmanaged<SecCode>!
|
||||
let flags: SecCSFlags = [.considerExpiration, .enforceRevocationChecks]
|
||||
@@ -80,10 +87,6 @@ extension SigningRequestTracer {
|
||||
|
||||
}
|
||||
|
||||
// from libproc.h
|
||||
@_silgen_name("proc_pidpath")
|
||||
@discardableResult func proc_pidpath(_ pid: Int32, _ buffer: UnsafeMutableRawPointer!, _ buffersize: UInt32) -> Int32
|
||||
|
||||
//// from SecTask.h
|
||||
@_silgen_name("SecCodeCreateWithPID")
|
||||
@discardableResult func SecCodeCreateWithPID(_: Int32, _: SecCSFlags, _: UnsafeMutablePointer<Unmanaged<SecCode>?>!) -> OSStatus
|
||||
|
||||
@@ -29,7 +29,6 @@ struct SecretAgent: App {
|
||||
private let authenticationHandler = AuthenticationHandler()
|
||||
private let publicKeyFileStoreController = PublicKeyFileStoreController(publicKeysURL: URL.publicKeyDirectory, certificatesURL: URL.certificatesDirectory)
|
||||
|
||||
@State var pending: ([[SignatureRequest]], (Set<SignatureRequest>) async throws -> Void)?
|
||||
@Environment(\.openWindow) var openWindow
|
||||
|
||||
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "App")
|
||||
@@ -100,8 +99,7 @@ struct SecretAgent: App {
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await authenticationHandler.setBatchAuthHandler { @MainActor pending, authorize in
|
||||
self.pending = (pending, authorize)
|
||||
authenticationHandler.setBatchAuthHandler { @MainActor in
|
||||
openWindow(id: String(describing: BatchedRequestsView.self))
|
||||
}
|
||||
|
||||
@@ -129,10 +127,10 @@ struct SecretAgent: App {
|
||||
|
||||
@ViewBuilder
|
||||
var pendingView: some View {
|
||||
if let (requests, authorize) = pending {
|
||||
BatchedRequestsView(pending: requests, review: authorize)
|
||||
if !authenticationHandler.batchableRequests.isEmpty {
|
||||
BatchedRequestsView(authenticationHandler: authenticationHandler)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -2,23 +2,21 @@ import SwiftUI
|
||||
import SecretKit
|
||||
import SecretAgentKit
|
||||
import SmartCardSecretKit
|
||||
import Common
|
||||
|
||||
struct BatchedRequestsView: View {
|
||||
|
||||
let pending: [[SignatureRequest]]
|
||||
let review: (Set<SignatureRequest>) async throws -> Void
|
||||
private let authenticationHandler: any AuthenticationHandlerProtocol
|
||||
|
||||
init(pending: [[SignatureRequest]], review: @escaping (Set<SignatureRequest>) async throws -> Void) {
|
||||
self.pending = pending
|
||||
self.review = review
|
||||
init(authenticationHandler: some AuthenticationHandlerProtocol) {
|
||||
self.authenticationHandler = authenticationHandler
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
// .padding()
|
||||
Form {
|
||||
// Text("Multiple authenticated requests are pending. You can approve them batches, or request they all proceed individually.")
|
||||
ForEach(Array(pending.enumerated()), id: \.offset) { group in
|
||||
Text("Multiple authenticated requests are pending. You can approve them batches, or request they all proceed individually.")
|
||||
ForEach(Array(authenticationHandler.batchableRequests.enumerated()), id: \.offset) { group in
|
||||
Section {
|
||||
ForEach(Array(group.element.enumerated()), id: \.offset) { pending in
|
||||
HStack {
|
||||
@@ -31,7 +29,7 @@ struct BatchedRequestsView: View {
|
||||
Spacer()
|
||||
Button("Review") {
|
||||
Task {
|
||||
try? await review([pending.element])
|
||||
try? await authenticationHandler.requestAuthentication(for: [pending.element])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,7 +40,7 @@ struct BatchedRequestsView: View {
|
||||
Spacer()
|
||||
Button("Review All") {
|
||||
Task {
|
||||
try? await review(Set(group.element))
|
||||
try? await authenticationHandler.requestAuthentication(for: Set(group.element))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -56,3 +54,65 @@ struct BatchedRequestsView: View {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private struct TestHandler: AuthenticationHandlerProtocol {
|
||||
|
||||
var batchableRequests: [[SignatureRequest]] = []
|
||||
|
||||
func requestAuthentication(for requests: Set<SignatureRequest>) async throws {
|
||||
|
||||
}
|
||||
|
||||
func persistAuthentication<SecretType>(secret: SecretType, forDuration duration: TimeInterval) async throws where SecretType : Secret {
|
||||
|
||||
}
|
||||
|
||||
func setBatchAuthHandler(_ handler: @escaping () async throws -> Void) {
|
||||
|
||||
}
|
||||
|
||||
func waitForAuthentication(for request: SignatureRequest) async throws -> any AuthenticationContextProtocol {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
}
|
||||
//
|
||||
//#Preview {
|
||||
// ScrollView {
|
||||
// MultilineInfoView(title: "GitHub", subtitle: "Ghostty", image: Image(systemName: "lock"), items: [
|
||||
// "
|
||||
// ])
|
||||
//// Section {
|
||||
//// ForEach(0..<2) { _ in
|
||||
//// VStack(alignment: .leading) {
|
||||
//// Text("Ghostty")
|
||||
//// .font(.headline)
|
||||
//// Text("zsh git zsh")
|
||||
//// .font(.footnote)
|
||||
//// Text("4:05 PM")
|
||||
//// }
|
||||
//// }
|
||||
//// } header: {
|
||||
//// Text("GitHub")
|
||||
//// }
|
||||
//// Section {
|
||||
//// ForEach(0..<2) { _ in
|
||||
//// VStack(alignment: .leading) {
|
||||
//// Text("Ghostty")
|
||||
//// .font(.headline)
|
||||
//// Text("zsh git")
|
||||
//// .font(.footnote.monospaced())
|
||||
//// Text("Git Signature")
|
||||
//// .font(.footnote)
|
||||
//// Text("4:05 PM")
|
||||
//// .font(.caption)
|
||||
//// }
|
||||
//// }
|
||||
//// } header: {
|
||||
//// Text("GitHub Signing Key")
|
||||
//// }
|
||||
// }
|
||||
// .padding()
|
||||
// .formStyle(.grouped)
|
||||
// .frame(minHeight: 700)
|
||||
//}
|
||||
|
||||
@@ -75,7 +75,6 @@
|
||||
50E0145E2EDB9CE400B121F1 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 50E0145D2EDB9CE400B121F1 /* Common */; };
|
||||
50E204E92FA9D12700402380 /* CertificateDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E204E82FA9D12700402380 /* CertificateDetailView.swift */; };
|
||||
50E204ED2FAA997F00402380 /* CertificateListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E204EC2FAA997F00402380 /* CertificateListItemView.swift */; };
|
||||
50E204EF2FAA9C1400402380 /* MultilineInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E204EE2FAA9C1400402380 /* MultilineInfoView.swift */; };
|
||||
50E2051D2FAAB81C00402380 /* SecretiveCertificateParser.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 50E205142FAAB81C00402380 /* SecretiveCertificateParser.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
50E205282FAAB82700402380 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E205242FAAB82700402380 /* main.swift */; };
|
||||
50E2052C2FAAB85000402380 /* SecretiveCertificateParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E2052B2FAAB85000402380 /* SecretiveCertificateParser.swift */; };
|
||||
@@ -276,7 +275,6 @@
|
||||
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtonStyle.swift; sourceTree = "<group>"; };
|
||||
50E204E82FA9D12700402380 /* CertificateDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateDetailView.swift; sourceTree = "<group>"; };
|
||||
50E204EC2FAA997F00402380 /* CertificateListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateListItemView.swift; sourceTree = "<group>"; };
|
||||
50E204EE2FAA9C1400402380 /* MultilineInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultilineInfoView.swift; sourceTree = "<group>"; };
|
||||
50E205142FAAB81C00402380 /* SecretiveCertificateParser.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = SecretiveCertificateParser.xpc; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
50E205232FAAB82700402380 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
50E205242FAAB82700402380 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
|
||||
@@ -404,7 +402,6 @@
|
||||
50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */,
|
||||
50617D8423FCE48E0099B055 /* ContentView.swift */,
|
||||
5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
|
||||
50E204EE2FAA9C1400402380 /* MultilineInfoView.swift */,
|
||||
50153E1F250AFCB200525160 /* UpdateView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
@@ -814,7 +811,6 @@
|
||||
50E204E92FA9D12700402380 /* CertificateDetailView.swift in Sources */,
|
||||
5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */,
|
||||
504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */,
|
||||
50E204EF2FAA9C1400402380 /* MultilineInfoView.swift in Sources */,
|
||||
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */,
|
||||
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */,
|
||||
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */,
|
||||
@@ -868,7 +864,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
50E205802FAB291E00402380 /* CertificateMigrator.swift in Sources */,
|
||||
50020BB024064869003D4025 /* AppDelegate.swift in Sources */,
|
||||
50020BB024064869003D4025 /* App.swift in Sources */,
|
||||
5018F54F24064786002EB505 /* Notifier.swift in Sources */,
|
||||
503647482F870B7800977A23 /* BatchedRequestsView.swift in Sources */,
|
||||
501578132E6C0479004A37D0 /* XPCInputParser.swift in Sources */,
|
||||
|
||||
@@ -87,6 +87,9 @@
|
||||
ReferencedContainer = "container:Secretive.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<MetalAPIValidationSettings
|
||||
isEnabled = "No">
|
||||
</MetalAPIValidationSettings>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Debug"
|
||||
|
||||
@@ -28,6 +28,13 @@ struct SecretDetailView<SecretType: Secret>: View {
|
||||
image: Image(systemName: "touchid"),
|
||||
text: keyWriter.openSSHMD5Fingerprint(secret: secret)
|
||||
)
|
||||
Spacer()
|
||||
.frame(height: 20)
|
||||
CopyableView(
|
||||
title: .secretDetailPublicKeyLabel,
|
||||
image: Image(systemName: "key"),
|
||||
text: keyWriter.openSSHString(secret: secret)
|
||||
)
|
||||
Spacer()
|
||||
.frame(height: 20)
|
||||
CopyableView(
|
||||
|
||||
Reference in New Issue
Block a user