mirror of
https://github.com/maxgoedjen/secretive.git
synced 2025-04-10 17:47:19 +00:00
Merge branch 'main' into sshcertcleanup_2
This commit is contained in:
commit
d3814d27d9
@ -12,7 +12,7 @@ public class Agent {
|
|||||||
private let writer = OpenSSHKeyWriter()
|
private let writer = OpenSSHKeyWriter()
|
||||||
private let requestTracer = SigningRequestTracer()
|
private let requestTracer = SigningRequestTracer()
|
||||||
private let certificateHandler = OpenSSHCertificateHandler()
|
private let certificateHandler = OpenSSHCertificateHandler()
|
||||||
private let logger = Logger()
|
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:
|
||||||
@ -53,6 +53,8 @@ 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 {
|
||||||
@ -106,7 +108,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,6 +204,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,12 +175,15 @@ 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()
|
||||||
NotificationCenter.default.post(name: .secretStoreReloaded, object: self)
|
if secrets != before {
|
||||||
if notifyAgent {
|
NotificationCenter.default.post(name: .secretStoreReloaded, object: self)
|
||||||
DistributedNotificationCenter.default().postNotificationName(.secretStoreUpdated, object: nil, deliverImmediately: true)
|
if notifyAgent {
|
||||||
|
DistributedNotificationCenter.default().postNotificationName(.secretStoreUpdated, object: nil, deliverImmediately: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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() {
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user