mirror of
https://github.com/maxgoedjen/secretive.git
synced 2026-04-10 03:07:22 +02:00
Compare commits
6 Commits
automatic_
...
viewbuilde
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8597d86ee8 | ||
|
|
efef3b3412 | ||
|
|
d429d090cf | ||
|
|
acdf0baf3a | ||
|
|
1a362ef955 | ||
|
|
3b254d33a5 |
@@ -31,13 +31,14 @@ public class Agent {
|
|||||||
private let writer = OpenSSHKeyWriter()
|
private let writer = OpenSSHKeyWriter()
|
||||||
private let requestTracer = SigningRequestTracer()
|
private let requestTracer = SigningRequestTracer()
|
||||||
private let certsPath = (NSHomeDirectory() as NSString).appendingPathComponent("PublicKeys") as String
|
private let certsPath = (NSHomeDirectory() as NSString).appendingPathComponent("PublicKeys") as String
|
||||||
|
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent.agent", category: "")
|
||||||
|
|
||||||
/// Initializes an agent with a store list and a witness.
|
/// Initializes an agent with a store list and a witness.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - storeList: The `SecretStoreList` to make available.
|
/// - storeList: The `SecretStoreList` to make available.
|
||||||
/// - witness: A witness to notify of requests.
|
/// - witness: A witness to notify of requests.
|
||||||
public init(storeList: SecretStoreList, witness: SigningWitness? = nil) {
|
public init(storeList: SecretStoreList, witness: SigningWitness? = nil) {
|
||||||
Logger().debug("Agent is running")
|
logger.debug("Agent is running")
|
||||||
self.storeList = storeList
|
self.storeList = storeList
|
||||||
self.witness = witness
|
self.witness = witness
|
||||||
}
|
}
|
||||||
@@ -53,16 +54,16 @@ extension Agent {
|
|||||||
/// - Return value:
|
/// - Return value:
|
||||||
/// - Boolean if data could be read
|
/// - Boolean if data could be read
|
||||||
@discardableResult public func handle(reader: FileHandleReader, writer: FileHandleWriter) -> Bool {
|
@discardableResult public func handle(reader: FileHandleReader, writer: FileHandleWriter) -> Bool {
|
||||||
Logger().debug("Agent handling new data")
|
logger.debug("Agent handling new data")
|
||||||
let data = Data(reader.availableData)
|
let data = Data(reader.availableData)
|
||||||
guard data.count > 4 else { return false}
|
guard data.count > 4 else { return false}
|
||||||
let requestTypeInt = data[4]
|
let requestTypeInt = data[4]
|
||||||
guard let requestType = SSHAgent.RequestType(rawValue: requestTypeInt) else {
|
guard let requestType = SSHAgent.RequestType(rawValue: requestTypeInt) else {
|
||||||
writer.write(OpenSSHKeyWriter().lengthAndData(of: SSHAgent.ResponseType.agentFailure.data))
|
writer.write(OpenSSHKeyWriter().lengthAndData(of: SSHAgent.ResponseType.agentFailure.data))
|
||||||
Logger().debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)")
|
logger.debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
Logger().debug("Agent handling request of type \(requestType.debugDescription)")
|
logger.debug("Agent handling request of type \(requestType.debugDescription)")
|
||||||
let subData = Data(data[5...])
|
let subData = Data(data[5...])
|
||||||
let response = handle(requestType: requestType, data: subData, reader: reader)
|
let response = handle(requestType: requestType, data: subData, reader: reader)
|
||||||
writer.write(response)
|
writer.write(response)
|
||||||
@@ -70,23 +71,25 @@ extension Agent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handle(requestType: SSHAgent.RequestType, data: Data, reader: FileHandleReader) -> Data {
|
func handle(requestType: SSHAgent.RequestType, data: Data, reader: FileHandleReader) -> Data {
|
||||||
|
// Depending on the launch context (such as after macOS update), the agent may need to reload secrets before acting
|
||||||
|
reloadSecretsIfNeccessary()
|
||||||
var response = Data()
|
var response = Data()
|
||||||
do {
|
do {
|
||||||
switch requestType {
|
switch requestType {
|
||||||
case .requestIdentities:
|
case .requestIdentities:
|
||||||
response.append(SSHAgent.ResponseType.agentIdentitiesAnswer.data)
|
response.append(SSHAgent.ResponseType.agentIdentitiesAnswer.data)
|
||||||
response.append(identities())
|
response.append(identities())
|
||||||
Logger().debug("Agent returned \(SSHAgent.ResponseType.agentIdentitiesAnswer.debugDescription)")
|
logger.debug("Agent returned \(SSHAgent.ResponseType.agentIdentitiesAnswer.debugDescription)")
|
||||||
case .signRequest:
|
case .signRequest:
|
||||||
let provenance = requestTracer.provenance(from: reader)
|
let provenance = requestTracer.provenance(from: reader)
|
||||||
response.append(SSHAgent.ResponseType.agentSignResponse.data)
|
response.append(SSHAgent.ResponseType.agentSignResponse.data)
|
||||||
response.append(try sign(data: data, provenance: provenance))
|
response.append(try sign(data: data, provenance: provenance))
|
||||||
Logger().debug("Agent returned \(SSHAgent.ResponseType.agentSignResponse.debugDescription)")
|
logger.debug("Agent returned \(SSHAgent.ResponseType.agentSignResponse.debugDescription)")
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
response.removeAll()
|
response.removeAll()
|
||||||
response.append(SSHAgent.ResponseType.agentFailure.data)
|
response.append(SSHAgent.ResponseType.agentFailure.data)
|
||||||
Logger().debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)")
|
logger.debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)")
|
||||||
}
|
}
|
||||||
let full = OpenSSHKeyWriter().lengthAndData(of: response)
|
let full = OpenSSHKeyWriter().lengthAndData(of: response)
|
||||||
return full
|
return full
|
||||||
@@ -120,7 +123,7 @@ extension Agent {
|
|||||||
keyData.append(writer.lengthAndData(of: curveData))
|
keyData.append(writer.lengthAndData(of: curveData))
|
||||||
|
|
||||||
}
|
}
|
||||||
Logger().debug("Agent enumerated \(secrets.count) identities")
|
logger.log("Agent enumerated \(secrets.count) identities")
|
||||||
return countData + keyData
|
return countData + keyData
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +142,7 @@ extension Agent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
guard let (store, secret) = secret(matching: hash) else {
|
guard let (store, secret) = secret(matching: hash) else {
|
||||||
Logger().debug("Agent did not have a key matching \(hash as NSData)")
|
logger.debug("Agent did not have a key matching \(hash as NSData)")
|
||||||
throw AgentError.noMatchingKey
|
throw AgentError.noMatchingKey
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +196,7 @@ extension Agent {
|
|||||||
try witness.witness(accessTo: secret, from: store, by: provenance)
|
try witness.witness(accessTo: secret, from: store, by: provenance)
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger().debug("Agent signed request")
|
logger.debug("Agent signed request")
|
||||||
|
|
||||||
return signedData
|
return signedData
|
||||||
}
|
}
|
||||||
@@ -235,7 +238,7 @@ extension Agent {
|
|||||||
let certificatePath = certsPath.appending("/").appending("\(minimalHex)-cert.pub")
|
let certificatePath = certsPath.appending("/").appending("\(minimalHex)-cert.pub")
|
||||||
|
|
||||||
if FileManager.default.fileExists(atPath: certificatePath) {
|
if FileManager.default.fileExists(atPath: certificatePath) {
|
||||||
Logger().debug("Found certificate for \(secret.name)")
|
logger.debug("Found certificate for \(secret.name)")
|
||||||
do {
|
do {
|
||||||
let certContent = try String(contentsOfFile:certificatePath, encoding: .utf8)
|
let certContent = try String(contentsOfFile:certificatePath, encoding: .utf8)
|
||||||
let certElements = certContent.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: " ")
|
let certElements = certContent.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: " ")
|
||||||
@@ -246,19 +249,19 @@ extension Agent {
|
|||||||
if let certName = certElements[2].data(using: .utf8) {
|
if let certName = certElements[2].data(using: .utf8) {
|
||||||
return (certDecoded, certName)
|
return (certDecoded, certName)
|
||||||
} else if let certName = secret.name.data(using: .utf8) {
|
} 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")
|
logger.info("Certificate for \(secret.name) does not have a name tag, using secret name instead")
|
||||||
return (certDecoded, certName)
|
return (certDecoded, certName)
|
||||||
} else {
|
} else {
|
||||||
throw OpenSSHCertificateError.parsingFailed
|
throw OpenSSHCertificateError.parsingFailed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Logger().warning("Certificate found for \(secret.name) but failed to decode base64 key")
|
logger.warning("Certificate found for \(secret.name) but failed to decode base64 key")
|
||||||
throw OpenSSHCertificateError.parsingFailed
|
throw OpenSSHCertificateError.parsingFailed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
Logger().warning("Certificate found for \(secret.name) but failed to load")
|
logger.warning("Certificate found for \(secret.name) but failed to load")
|
||||||
throw OpenSSHCertificateError.parsingFailed
|
throw OpenSSHCertificateError.parsingFailed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -270,6 +273,16 @@ extension Agent {
|
|||||||
|
|
||||||
extension Agent {
|
extension Agent {
|
||||||
|
|
||||||
|
/// Gives any store with no loaded secrets a chance to reload.
|
||||||
|
func reloadSecretsIfNeccessary() {
|
||||||
|
for store in storeList.stores {
|
||||||
|
if store.secrets.isEmpty {
|
||||||
|
logger.debug("Store \(store.name, privacy: .public) has no loaded secrets. Reloading.")
|
||||||
|
store.reloadSecrets()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Finds a ``Secret`` matching a specified hash whos signature was requested.
|
/// Finds a ``Secret`` matching a specified hash whos signature was requested.
|
||||||
/// - Parameter hash: The hash to match against.
|
/// - Parameter hash: The hash to match against.
|
||||||
/// - Returns: A ``Secret`` and the ``SecretStore`` containing it, if a match is found.
|
/// - Returns: A ``Secret`` and the ``SecretStore`` containing it, if a match is found.
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ public class AnySecretStore: SecretStore {
|
|||||||
private let _sign: (Data, AnySecret, SigningRequestProvenance) throws -> Data
|
private let _sign: (Data, AnySecret, SigningRequestProvenance) throws -> Data
|
||||||
private let _existingPersistedAuthenticationContext: (AnySecret) -> PersistedAuthenticationContext?
|
private let _existingPersistedAuthenticationContext: (AnySecret) -> PersistedAuthenticationContext?
|
||||||
private let _persistAuthentication: (AnySecret, TimeInterval) throws -> Void
|
private let _persistAuthentication: (AnySecret, TimeInterval) throws -> Void
|
||||||
|
private let _reloadSecrets: () -> Void
|
||||||
|
|
||||||
private var sink: AnyCancellable?
|
private var sink: AnyCancellable?
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ public class AnySecretStore: SecretStore {
|
|||||||
_sign = { try secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType, for: $2) }
|
_sign = { try secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType, for: $2) }
|
||||||
_existingPersistedAuthenticationContext = { secretStore.existingPersistedAuthenticationContext(secret: $0.base as! SecretStoreType.SecretType) }
|
_existingPersistedAuthenticationContext = { secretStore.existingPersistedAuthenticationContext(secret: $0.base as! SecretStoreType.SecretType) }
|
||||||
_persistAuthentication = { try secretStore.persistAuthentication(secret: $0.base as! SecretStoreType.SecretType, forDuration: $1) }
|
_persistAuthentication = { try secretStore.persistAuthentication(secret: $0.base as! SecretStoreType.SecretType, forDuration: $1) }
|
||||||
|
_reloadSecrets = { secretStore.reloadSecrets() }
|
||||||
sink = secretStore.objectWillChange.sink { _ in
|
sink = secretStore.objectWillChange.sink { _ in
|
||||||
self.objectWillChange.send()
|
self.objectWillChange.send()
|
||||||
}
|
}
|
||||||
@@ -57,6 +59,10 @@ public class AnySecretStore: SecretStore {
|
|||||||
try _persistAuthentication(secret, duration)
|
try _persistAuthentication(secret, duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func reloadSecrets() {
|
||||||
|
_reloadSecrets()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable {
|
public class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable {
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ public protocol SecretStore: ObservableObject, Identifiable {
|
|||||||
/// - 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.
|
/// - 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
|
func persistAuthentication(secret: SecretType, forDuration duration: TimeInterval) throws
|
||||||
|
|
||||||
|
/// Requests that the store reload secrets from any backing store, if neccessary.
|
||||||
|
func reloadSecrets()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A SecretStore that the Secretive admin app can modify.
|
/// A SecretStore that the Secretive admin app can modify.
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ extension SecureEnclave {
|
|||||||
/// Initializes a Store.
|
/// Initializes a Store.
|
||||||
public init() {
|
public init() {
|
||||||
DistributedNotificationCenter.default().addObserver(forName: .secretStoreUpdated, object: nil, queue: .main) { _ in
|
DistributedNotificationCenter.default().addObserver(forName: .secretStoreUpdated, object: nil, queue: .main) { _ in
|
||||||
self.reloadSecrets(notifyAgent: false)
|
self.reloadSecretsInternal(notifyAgent: false)
|
||||||
}
|
}
|
||||||
loadSecrets()
|
loadSecrets()
|
||||||
}
|
}
|
||||||
@@ -68,7 +68,7 @@ extension SecureEnclave {
|
|||||||
throw KeychainError(statusCode: nil)
|
throw KeychainError(statusCode: nil)
|
||||||
}
|
}
|
||||||
try savePublicKey(publicKey, name: name)
|
try savePublicKey(publicKey, name: name)
|
||||||
reloadSecrets()
|
reloadSecretsInternal()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func delete(secret: Secret) throws {
|
public func delete(secret: Secret) throws {
|
||||||
@@ -80,7 +80,7 @@ extension SecureEnclave {
|
|||||||
if status != errSecSuccess {
|
if status != errSecSuccess {
|
||||||
throw KeychainError(statusCode: status)
|
throw KeychainError(statusCode: status)
|
||||||
}
|
}
|
||||||
reloadSecrets()
|
reloadSecretsInternal()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func update(secret: Secret, name: String) throws {
|
public func update(secret: Secret, name: String) throws {
|
||||||
@@ -97,7 +97,7 @@ extension SecureEnclave {
|
|||||||
if status != errSecSuccess {
|
if status != errSecSuccess {
|
||||||
throw KeychainError(statusCode: status)
|
throw KeychainError(statusCode: status)
|
||||||
}
|
}
|
||||||
reloadSecrets()
|
reloadSecretsInternal()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data {
|
public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data {
|
||||||
@@ -163,6 +163,10 @@ extension SecureEnclave {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func reloadSecrets() {
|
||||||
|
reloadSecretsInternal(notifyAgent: false)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -171,14 +175,17 @@ extension SecureEnclave.Store {
|
|||||||
|
|
||||||
/// Reloads all secrets from the store.
|
/// Reloads all secrets from the store.
|
||||||
/// - Parameter notifyAgent: A boolean indicating whether a distributed notification should be posted, notifying other processes (ie, the SecretAgent) to reload their stores as well.
|
/// - Parameter notifyAgent: A boolean indicating whether a distributed notification should be posted, notifying other processes (ie, the SecretAgent) to reload their stores as well.
|
||||||
private func reloadSecrets(notifyAgent: Bool = true) {
|
private func reloadSecretsInternal(notifyAgent: Bool = true) {
|
||||||
|
let before = secrets
|
||||||
secrets.removeAll()
|
secrets.removeAll()
|
||||||
loadSecrets()
|
loadSecrets()
|
||||||
|
if secrets != before {
|
||||||
NotificationCenter.default.post(name: .secretStoreReloaded, object: self)
|
NotificationCenter.default.post(name: .secretStoreReloaded, object: self)
|
||||||
if notifyAgent {
|
if notifyAgent {
|
||||||
DistributedNotificationCenter.default().postNotificationName(.secretStoreUpdated, object: nil, deliverImmediately: true)
|
DistributedNotificationCenter.default().postNotificationName(.secretStoreUpdated, object: nil, deliverImmediately: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Loads all secrets from the store.
|
/// Loads all secrets from the store.
|
||||||
private func loadSecrets() {
|
private func loadSecrets() {
|
||||||
|
|||||||
@@ -89,6 +89,19 @@ extension SmartCard {
|
|||||||
public func persistAuthentication(secret: SmartCard.Secret, forDuration: TimeInterval) throws {
|
public func persistAuthentication(secret: SmartCard.Secret, forDuration: TimeInterval) throws {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reloads all secrets from the store.
|
||||||
|
public func reloadSecrets() {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.isAvailable = self.tokenID != nil
|
||||||
|
let before = self.secrets
|
||||||
|
self.secrets.removeAll()
|
||||||
|
self.loadSecrets()
|
||||||
|
if self.secrets != before {
|
||||||
|
NotificationCenter.default.post(name: .secretStoreReloaded, object: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -102,15 +115,6 @@ extension SmartCard.Store {
|
|||||||
reloadSecrets()
|
reloadSecrets()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reloads all secrets from the store.
|
|
||||||
private func reloadSecrets() {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.isAvailable = self.tokenID != nil
|
|
||||||
self.secrets.removeAll()
|
|
||||||
self.loadSecrets()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loads all secrets from the store.
|
/// Loads all secrets from the store.
|
||||||
private func loadSecrets() {
|
private func loadSecrets() {
|
||||||
guard let tokenID = tokenID else { return }
|
guard let tokenID = tokenID else { return }
|
||||||
|
|||||||
@@ -78,6 +78,9 @@ extension Stub {
|
|||||||
public func persistAuthentication(secret: Stub.Secret, forDuration duration: TimeInterval) throws {
|
public func persistAuthentication(secret: Stub.Secret, forDuration duration: TimeInterval) throws {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func reloadSecrets() {
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8923FCE48E0099B055 /* Preview Assets.xcassets */; };
|
50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8923FCE48E0099B055 /* Preview Assets.xcassets */; };
|
||||||
50617D9923FCE48E0099B055 /* SecretiveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D9823FCE48E0099B055 /* SecretiveTests.swift */; };
|
50617D9923FCE48E0099B055 /* SecretiveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D9823FCE48E0099B055 /* SecretiveTests.swift */; };
|
||||||
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DD123FCEFA90099B055 /* PreviewStore.swift */; };
|
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DD123FCEFA90099B055 /* PreviewStore.swift */; };
|
||||||
|
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */; };
|
||||||
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C12516F303004B5A36 /* SetupView.swift */; };
|
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C12516F303004B5A36 /* SetupView.swift */; };
|
||||||
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C72516FE6E004B5A36 /* CopyableView.swift */; };
|
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C72516FE6E004B5A36 /* CopyableView.swift */; };
|
||||||
5066A6F7251829B1004B5A36 /* ShellConfigurationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */; };
|
5066A6F7251829B1004B5A36 /* ShellConfigurationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */; };
|
||||||
@@ -123,6 +124,7 @@
|
|||||||
50617D9823FCE48E0099B055 /* SecretiveTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretiveTests.swift; sourceTree = "<group>"; };
|
50617D9823FCE48E0099B055 /* SecretiveTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretiveTests.swift; sourceTree = "<group>"; };
|
||||||
50617D9A23FCE48E0099B055 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
50617D9A23FCE48E0099B055 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
50617DD123FCEFA90099B055 /* PreviewStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewStore.swift; sourceTree = "<group>"; };
|
50617DD123FCEFA90099B055 /* PreviewStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewStore.swift; sourceTree = "<group>"; };
|
||||||
|
5065E312295517C500E16645 /* ToolbarButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolbarButtonStyle.swift; sourceTree = "<group>"; };
|
||||||
5066A6C12516F303004B5A36 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = "<group>"; };
|
5066A6C12516F303004B5A36 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = "<group>"; };
|
||||||
5066A6C72516FE6E004B5A36 /* CopyableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableView.swift; sourceTree = "<group>"; };
|
5066A6C72516FE6E004B5A36 /* CopyableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableView.swift; sourceTree = "<group>"; };
|
||||||
5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellConfigurationController.swift; sourceTree = "<group>"; };
|
5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellConfigurationController.swift; sourceTree = "<group>"; };
|
||||||
@@ -275,6 +277,7 @@
|
|||||||
50153E1F250AFCB200525160 /* UpdateView.swift */,
|
50153E1F250AFCB200525160 /* UpdateView.swift */,
|
||||||
5066A6C12516F303004B5A36 /* SetupView.swift */,
|
5066A6C12516F303004B5A36 /* SetupView.swift */,
|
||||||
5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
|
5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
|
||||||
|
5065E312295517C500E16645 /* ToolbarButtonStyle.swift */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -475,6 +478,7 @@
|
|||||||
2C4A9D2F2636FFD3008CC8E2 /* RenameSecretView.swift in Sources */,
|
2C4A9D2F2636FFD3008CC8E2 /* RenameSecretView.swift in Sources */,
|
||||||
5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */,
|
5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */,
|
||||||
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */,
|
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */,
|
||||||
|
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */,
|
||||||
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */,
|
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */,
|
||||||
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */,
|
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */,
|
||||||
5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */,
|
5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */,
|
||||||
|
|||||||
@@ -75,6 +75,7 @@
|
|||||||
ignoresPersistentStateOnLaunch = "NO"
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
debugDocumentVersioning = "YES"
|
debugDocumentVersioning = "YES"
|
||||||
debugServiceExtension = "internal"
|
debugServiceExtension = "internal"
|
||||||
|
enableGPUValidationMode = "1"
|
||||||
allowLocationSimulation = "YES">
|
allowLocationSimulation = "YES">
|
||||||
<BuildableProductRunnable
|
<BuildableProductRunnable
|
||||||
runnableDebuggingMode = "0">
|
runnableDebuggingMode = "0">
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ extension Preview {
|
|||||||
func persistAuthentication(secret: Preview.Secret, forDuration duration: TimeInterval) throws {
|
func persistAuthentication(secret: Preview.Secret, forDuration duration: TimeInterval) throws {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func reloadSecrets() {
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class StoreModifiable: Store, SecretStoreModifiable {
|
class StoreModifiable: Store, SecretStoreModifiable {
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ struct ContentView<UpdaterType: UpdaterProtocol, AgentStatusCheckerType: AgentSt
|
|||||||
@Binding var showingCreation: Bool
|
@Binding var showingCreation: Bool
|
||||||
@Binding var runningSetup: Bool
|
@Binding var runningSetup: Bool
|
||||||
@Binding var hasRunSetup: Bool
|
@Binding var hasRunSetup: Bool
|
||||||
|
@State var showingAgentInfo = false
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
@EnvironmentObject private var storeList: SecretStoreList
|
@EnvironmentObject private var storeList: SecretStoreList
|
||||||
@EnvironmentObject private var updater: UpdaterType
|
@EnvironmentObject private var updater: UpdaterType
|
||||||
@@ -27,10 +29,10 @@ struct ContentView<UpdaterType: UpdaterProtocol, AgentStatusCheckerType: AgentSt
|
|||||||
}
|
}
|
||||||
.frame(minWidth: 640, minHeight: 320)
|
.frame(minWidth: 640, minHeight: 320)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
updateNotice
|
toolbarItem(updateNoticeView, id: "update")
|
||||||
setupNotice
|
toolbarItem(runningOrRunSetupView, id: "setup")
|
||||||
appPathNotice
|
toolbarItem(appPathNoticeView, id: "appPath")
|
||||||
newItem
|
toolbarItem(newItemView, id: "new")
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $runningSetup) {
|
.sheet(isPresented: $runningSetup) {
|
||||||
SetupView(visible: $runningSetup, setupComplete: $hasRunSetup)
|
SetupView(visible: $runningSetup, setupComplete: $hasRunSetup)
|
||||||
@@ -41,26 +43,42 @@ struct ContentView<UpdaterType: UpdaterProtocol, AgentStatusCheckerType: AgentSt
|
|||||||
|
|
||||||
extension ContentView {
|
extension ContentView {
|
||||||
|
|
||||||
var updateNotice: ToolbarItem<Void, AnyView> {
|
|
||||||
guard let update = updater.update else {
|
func toolbarItem(_ view: some View, id: String) -> ToolbarItem<String, some View> {
|
||||||
return ToolbarItem { AnyView(EmptyView()) }
|
ToolbarItem(id: id) { view }
|
||||||
}
|
}
|
||||||
let color: Color
|
|
||||||
let text: String
|
var needsSetup: Bool {
|
||||||
|
(runningSetup || !hasRunSetup || !agentStatusChecker.running) && !agentStatusChecker.developmentBuild
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Item either showing a "everything's good, here's more info" or "something's wrong, re-run setup" message
|
||||||
|
/// These two are mutually exclusive
|
||||||
|
@ViewBuilder
|
||||||
|
var runningOrRunSetupView: some View {
|
||||||
|
if needsSetup {
|
||||||
|
setupNoticeView
|
||||||
|
} else {
|
||||||
|
runningNoticeView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateNoticeContent: (String, Color)? {
|
||||||
|
guard let update = updater.update else { return nil }
|
||||||
if update.critical {
|
if update.critical {
|
||||||
text = "Critical Security Update Required"
|
return ("Critical Security Update Required", .red)
|
||||||
color = .red
|
|
||||||
} else {
|
} else {
|
||||||
if updater.testBuild {
|
if updater.testBuild {
|
||||||
text = "Test Build"
|
return ("Test Build", .blue)
|
||||||
color = .blue
|
|
||||||
} else {
|
} else {
|
||||||
text = "Update Available"
|
return ("Update Available", .orange)
|
||||||
color = .orange
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ToolbarItem {
|
}
|
||||||
AnyView(
|
|
||||||
|
@ViewBuilder
|
||||||
|
var updateNoticeView: some View {
|
||||||
|
if let update = updater.update, let (text, color) = updateNoticeContent {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
selectedUpdate = update
|
selectedUpdate = update
|
||||||
}, label: {
|
}, label: {
|
||||||
@@ -68,41 +86,31 @@ extension ContentView {
|
|||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
})
|
})
|
||||||
.background(color)
|
.buttonStyle(ToolbarButtonStyle(color: color))
|
||||||
.cornerRadius(5)
|
|
||||||
.popover(item: $selectedUpdate, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) { update in
|
.popover(item: $selectedUpdate, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) { update in
|
||||||
UpdateDetailView(update: update)
|
UpdateDetailView(update: update)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var newItem: ToolbarItem<Void, AnyView> {
|
@ViewBuilder
|
||||||
guard storeList.modifiableStore?.isAvailable ?? false else {
|
var newItemView: some View {
|
||||||
return ToolbarItem { AnyView(EmptyView()) }
|
if storeList.modifiableStore?.isAvailable ?? false {
|
||||||
}
|
|
||||||
return ToolbarItem {
|
|
||||||
AnyView(
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
showingCreation = true
|
showingCreation = true
|
||||||
}, label: {
|
}, label: {
|
||||||
Image(systemName: "plus")
|
Image(systemName: "plus")
|
||||||
})
|
})
|
||||||
.popover(isPresented: $showingCreation, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) {
|
.sheet(isPresented: $showingCreation) {
|
||||||
if let modifiable = storeList.modifiableStore {
|
if let modifiable = storeList.modifiableStore {
|
||||||
CreateSecretView(store: modifiable, showing: $showingCreation)
|
CreateSecretView(store: modifiable, showing: $showingCreation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var setupNotice: ToolbarItem<Void, AnyView> {
|
@ViewBuilder
|
||||||
return ToolbarItem {
|
var setupNoticeView: some View {
|
||||||
AnyView(
|
|
||||||
Group {
|
|
||||||
if (runningSetup || !hasRunSetup || !agentStatusChecker.running) && !agentStatusChecker.developmentBuild {
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
runningSetup = true
|
runningSetup = true
|
||||||
}, label: {
|
}, label: {
|
||||||
@@ -116,23 +124,39 @@ extension ContentView {
|
|||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
})
|
})
|
||||||
.background(Color.orange)
|
.buttonStyle(ToolbarButtonStyle(color: .orange))
|
||||||
.cornerRadius(5)
|
|
||||||
} else {
|
|
||||||
EmptyView()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
var runningNoticeView: some View {
|
||||||
|
Button(action: {
|
||||||
|
showingAgentInfo = true
|
||||||
|
}, label: {
|
||||||
|
HStack {
|
||||||
|
Text("Agent is Running")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white)
|
||||||
|
Circle()
|
||||||
|
.frame(width: 10, height: 10)
|
||||||
|
.foregroundColor(Color.green)
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
|
.buttonStyle(ToolbarButtonStyle(lightColor: .black.opacity(0.05), darkColor: .white.opacity(0.05)))
|
||||||
|
.popover(isPresented: $showingAgentInfo, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) {
|
||||||
|
VStack {
|
||||||
|
Text("SecretAgent is Running")
|
||||||
|
.font(.title)
|
||||||
|
.padding(5)
|
||||||
|
Text("SecretAgent is a process that runs in the background to sign requests, so you don't need to keep Secretive open all the time.\n\n**You can close Secretive, and everything will still keep working.**")
|
||||||
|
.frame(width: 300)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var appPathNotice: ToolbarItem<Void, AnyView> {
|
@ViewBuilder
|
||||||
let controller = ApplicationDirectoryController()
|
var appPathNoticeView: some View {
|
||||||
guard !controller.isInApplicationsDirectory else {
|
if !ApplicationDirectoryController().isInApplicationsDirectory {
|
||||||
return ToolbarItem { AnyView(EmptyView()) }
|
|
||||||
}
|
|
||||||
return ToolbarItem {
|
|
||||||
AnyView(
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
showingAppPathNotice = true
|
showingAppPathNotice = true
|
||||||
}, label: {
|
}, label: {
|
||||||
@@ -142,8 +166,7 @@ extension ContentView {
|
|||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
})
|
})
|
||||||
.background(Color.orange)
|
.buttonStyle(ToolbarButtonStyle(color: .orange))
|
||||||
.cornerRadius(5)
|
|
||||||
.popover(isPresented: $showingAppPathNotice, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) {
|
.popover(isPresented: $showingAppPathNotice, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) {
|
||||||
VStack {
|
VStack {
|
||||||
Image(systemName: "exclamationmark.triangle")
|
Image(systemName: "exclamationmark.triangle")
|
||||||
@@ -155,7 +178,6 @@ extension ContentView {
|
|||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,3 +220,4 @@ struct ContentView_Previews: PreviewProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ struct CopyableView: View {
|
|||||||
var text: String
|
var text: String
|
||||||
|
|
||||||
@State private var interactionState: InteractionState = .normal
|
@State private var interactionState: InteractionState = .normal
|
||||||
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
@@ -77,38 +78,32 @@ struct CopyableView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var backgroundColor: Color {
|
var backgroundColor: Color {
|
||||||
let color: NSColor
|
|
||||||
switch interactionState {
|
switch interactionState {
|
||||||
case .normal:
|
case .normal:
|
||||||
color = .windowBackgroundColor
|
return colorScheme == .dark ? Color(white: 0.2) : Color(white: 0.885)
|
||||||
case .hovering:
|
case .hovering:
|
||||||
color = .unemphasizedSelectedContentBackgroundColor
|
return colorScheme == .dark ? Color(white: 0.275) : Color(white: 0.82)
|
||||||
case .clicking:
|
case .clicking:
|
||||||
color = .selectedContentBackgroundColor
|
return .accentColor
|
||||||
}
|
}
|
||||||
return Color(color)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var primaryTextColor: Color {
|
var primaryTextColor: Color {
|
||||||
let color: NSColor
|
|
||||||
switch interactionState {
|
switch interactionState {
|
||||||
case .normal, .hovering:
|
case .normal, .hovering:
|
||||||
color = .textColor
|
return Color(.textColor)
|
||||||
case .clicking:
|
case .clicking:
|
||||||
color = .white
|
return .white
|
||||||
}
|
}
|
||||||
return Color(color)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var secondaryTextColor: Color {
|
var secondaryTextColor: Color {
|
||||||
let color: NSColor
|
|
||||||
switch interactionState {
|
switch interactionState {
|
||||||
case .normal, .hovering:
|
case .normal, .hovering:
|
||||||
color = .secondaryLabelColor
|
return Color(.secondaryLabelColor)
|
||||||
case .clicking:
|
case .clicking:
|
||||||
color = .white
|
return .white
|
||||||
}
|
}
|
||||||
return Color(color)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func copy() {
|
func copy() {
|
||||||
@@ -128,7 +123,9 @@ struct CopyableView_Previews: PreviewProvider {
|
|||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
Group {
|
Group {
|
||||||
CopyableView(title: "Title", image: Image(systemName: "figure.wave"), text: "Hello world.")
|
CopyableView(title: "Title", image: Image(systemName: "figure.wave"), text: "Hello world.")
|
||||||
|
.padding()
|
||||||
CopyableView(title: "Title", image: Image(systemName: "figure.wave"), text: "Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. ")
|
CopyableView(title: "Title", image: Image(systemName: "figure.wave"), text: "Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. ")
|
||||||
|
.padding()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,19 +12,25 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
HStack {
|
HStack {
|
||||||
Image(nsImage: NSApplication.shared.applicationIconImage)
|
|
||||||
.resizable()
|
|
||||||
.frame(width: 64, height: 64)
|
|
||||||
.padding()
|
|
||||||
VStack {
|
VStack {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Create a New Secret").bold()
|
Text("Create a New Secret")
|
||||||
|
.font(.largeTitle)
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
HStack {
|
HStack {
|
||||||
Text("Name:")
|
Text("Name:")
|
||||||
TextField("Shhhhh", text: $name).focusable()
|
TextField("Shhhhh", text: $name)
|
||||||
|
.focusable()
|
||||||
}
|
}
|
||||||
|
if #available(macOS 12.0, *) {
|
||||||
|
ThumbnailPickerView(items: [
|
||||||
|
ThumbnailPickerView.Item(value: true, name: "Require Authentication", description: "You will be required to authenticate using Touch ID, Apple Watch, or password before each use.", thumbnail: AuthenticationView()),
|
||||||
|
ThumbnailPickerView.Item(value: false, name: "Notify",
|
||||||
|
description: "No authentication is required while your Mac is unlocked, but you will be notified when a secret is used.",
|
||||||
|
thumbnail: NotificationView())
|
||||||
|
], selection: $requiresAuthentication)
|
||||||
|
} else {
|
||||||
HStack {
|
HStack {
|
||||||
VStack(spacing: 20) {
|
VStack(spacing: 20) {
|
||||||
Picker("", selection: $requiresAuthentication) {
|
Picker("", selection: $requiresAuthentication) {
|
||||||
@@ -32,8 +38,9 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
|
|||||||
Text("Authentication not required when Mac is unlocked").tag(false)
|
Text("Authentication not required when Mac is unlocked").tag(false)
|
||||||
}
|
}
|
||||||
.pickerStyle(RadioGroupPickerStyle())
|
.pickerStyle(RadioGroupPickerStyle())
|
||||||
|
Spacer(minLength: 10)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,4 +61,208 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
|
|||||||
try! store.create(name: name, requiresAuthentication: requiresAuthentication)
|
try! store.create(name: name, requiresAuthentication: requiresAuthentication)
|
||||||
showing = false
|
showing = false
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ThumbnailPickerView<ValueType: Hashable>: View {
|
||||||
|
|
||||||
|
private let items: [Item<ValueType>]
|
||||||
|
@Binding var selection: ValueType
|
||||||
|
|
||||||
|
init(items: [ThumbnailPickerView<ValueType>.Item<ValueType>], selection: Binding<ValueType>) {
|
||||||
|
self.items = items
|
||||||
|
_selection = selection
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(alignment: .top) {
|
||||||
|
ForEach(items) { item in
|
||||||
|
VStack(alignment: .leading, spacing: 15) {
|
||||||
|
item.thumbnail
|
||||||
|
.frame(height: 200)
|
||||||
|
.overlay(RoundedRectangle(cornerRadius: 10)
|
||||||
|
.stroke(lineWidth: item.value == selection ? 15 : 0))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
|
Text(item.name)
|
||||||
|
.bold()
|
||||||
|
Text(item.description)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: 250)
|
||||||
|
.onTapGesture {
|
||||||
|
withAnimation(.spring()) {
|
||||||
|
selection = item.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ThumbnailPickerView {
|
||||||
|
|
||||||
|
struct Item<ValueType: Hashable>: Identifiable {
|
||||||
|
let id = UUID()
|
||||||
|
let value: ValueType
|
||||||
|
let name: String
|
||||||
|
let description: String
|
||||||
|
let thumbnail: AnyView
|
||||||
|
|
||||||
|
init<ViewType: View>(value: ValueType, name: String, description: String, thumbnail: ViewType) {
|
||||||
|
self.value = value
|
||||||
|
self.name = name
|
||||||
|
self.description = description
|
||||||
|
self.thumbnail = AnyView(thumbnail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor class SystemBackground: ObservableObject {
|
||||||
|
|
||||||
|
static let shared = SystemBackground()
|
||||||
|
@Published var image: NSImage?
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
if let mainScreen = NSScreen.main, let imageURL = NSWorkspace.shared.desktopImageURL(for: mainScreen) {
|
||||||
|
image = NSImage(contentsOf: imageURL)
|
||||||
|
} else {
|
||||||
|
image = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(macOS 12.0, *)
|
||||||
|
struct SystemBackgroundView: View {
|
||||||
|
|
||||||
|
let anchor: UnitPoint
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if let image = SystemBackground.shared.image {
|
||||||
|
Image(nsImage: image)
|
||||||
|
.resizable()
|
||||||
|
.scaleEffect(3, anchor: anchor)
|
||||||
|
.clipped()
|
||||||
|
.allowsHitTesting(false)
|
||||||
|
} else {
|
||||||
|
Rectangle()
|
||||||
|
.foregroundColor(Color(.systemPurple))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(macOS 12.0, *)
|
||||||
|
struct AuthenticationView: View {
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
SystemBackgroundView(anchor: .center)
|
||||||
|
GeometryReader { geometry in
|
||||||
|
VStack {
|
||||||
|
Image(systemName: "touchid")
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.foregroundColor(Color(.systemRed))
|
||||||
|
Text("Touch ID Prompt")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.redacted(reason: .placeholder)
|
||||||
|
VStack {
|
||||||
|
Text("Touch ID Detail prompt.Detail two.")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
Text("Touch ID Detail prompt.Detail two.")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
}
|
||||||
|
.redacted(reason: .placeholder)
|
||||||
|
RoundedRectangle(cornerRadius: 5)
|
||||||
|
.frame(width: geometry.size.width, height: 20, alignment: .center)
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
RoundedRectangle(cornerRadius: 5)
|
||||||
|
.frame(width: geometry.size.width, height: 20, alignment: .center)
|
||||||
|
.foregroundColor(Color(.unemphasizedSelectedContentBackgroundColor))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.frame(width: 150)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 15)
|
||||||
|
.foregroundStyle(.ultraThickMaterial)
|
||||||
|
)
|
||||||
|
.padding()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(macOS 12.0, *)
|
||||||
|
struct NotificationView: View {
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
SystemBackgroundView(anchor: .topTrailing)
|
||||||
|
VStack {
|
||||||
|
Rectangle()
|
||||||
|
.background(Color.clear)
|
||||||
|
.foregroundStyle(.thinMaterial)
|
||||||
|
.frame(height: 35)
|
||||||
|
VStack {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
HStack {
|
||||||
|
Image(nsImage: NSApplication.shared.applicationIconImage)
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 64, height: 64)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("Secretive")
|
||||||
|
.font(.title)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
Text("Secretive wants to sign")
|
||||||
|
.font(.body)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
}
|
||||||
|
}.padding()
|
||||||
|
.redacted(reason: .placeholder)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 15)
|
||||||
|
.foregroundStyle(.ultraThickMaterial)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
|
||||||
|
struct CreateSecretView_Previews: PreviewProvider {
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
Group {
|
||||||
|
CreateSecretView(store: Preview.StoreModifiable(), showing: .constant(true))
|
||||||
|
if #available(macOS 12.0, *) {
|
||||||
|
AuthenticationView().environment(\.colorScheme, .dark)
|
||||||
|
AuthenticationView().environment(\.colorScheme, .light)
|
||||||
|
NotificationView().environment(\.colorScheme, .dark)
|
||||||
|
NotificationView().environment(\.colorScheme, .light)
|
||||||
|
} else {
|
||||||
|
// Fallback on earlier versions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|||||||
37
Sources/Secretive/Views/ToolbarButtonStyle.swift
Normal file
37
Sources/Secretive/Views/ToolbarButtonStyle.swift
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ToolbarButtonStyle: ButtonStyle {
|
||||||
|
|
||||||
|
private let lightColor: Color
|
||||||
|
private let darkColor: Color
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
@State var hovering = false
|
||||||
|
|
||||||
|
init(color: Color) {
|
||||||
|
self.lightColor = color
|
||||||
|
self.darkColor = color
|
||||||
|
}
|
||||||
|
|
||||||
|
init(lightColor: Color, darkColor: Color) {
|
||||||
|
self.lightColor = lightColor
|
||||||
|
self.darkColor = darkColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeBody(configuration: Configuration) -> some View {
|
||||||
|
configuration.label
|
||||||
|
.padding(EdgeInsets(top: 6, leading: 8, bottom: 6, trailing: 8))
|
||||||
|
.background(colorScheme == .light ? lightColor : darkColor)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 5))
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 5)
|
||||||
|
.stroke(colorScheme == .light ? .black.opacity(0.15) : .white.opacity(0.15), lineWidth: 1)
|
||||||
|
.background(hovering ? (colorScheme == .light ? .black.opacity(0.1) : .white.opacity(0.05)) : Color.clear)
|
||||||
|
)
|
||||||
|
.onHover { hovering in
|
||||||
|
withAnimation {
|
||||||
|
self.hovering = hovering
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user