Compare commits

..

1 Commits

Author SHA1 Message Date
64b2cc6f29 Fix 2022-06-18 20:19:11 -07:00
31 changed files with 202 additions and 764 deletions

View File

@ -1,16 +0,0 @@
name: Add bugs to bugs project
on:
issues:
types:
- opened
jobs:
add-to-project:
name: Add issue to project
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@v0.0.3
with:
project-url: https://github.com/users/maxgoedjen/projects/1
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}

View File

@ -5,7 +5,7 @@ on:
- cron: "0 8 * * *"
jobs:
build:
runs-on: macOS-latest
runs-on: macos-11.0
timeout-minutes: 10
steps:
- uses: actions/checkout@v2
@ -19,7 +19,7 @@ jobs:
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
run: ./.github/scripts/signing.sh
- name: Set Environment
run: sudo xcrun xcode-select -s /Applications/Xcode_14.1.app
run: sudo xcrun xcode-select -s /Applications/Xcode_13.2.1.app
- name: Update Build Number
env:
RUN_ID: ${{ github.run_id }}
@ -40,12 +40,8 @@ jobs:
run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip
- name: Document SHAs
run: |
echo "sha-512:"
shasum -a 512 Secretive.zip
shasum -a 512 Archive.zip
echo "sha-256:"
shasum -a 256 Secretive.zip
shasum -a 256 Archive.zip
- name: Upload App to Artifacts
uses: actions/upload-artifact@v1
with:

View File

@ -6,7 +6,7 @@ on:
- '*'
jobs:
test:
runs-on: macOS-latest
runs-on: macos-11.0
timeout-minutes: 10
steps:
- uses: actions/checkout@v1
@ -20,14 +20,14 @@ jobs:
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
run: ./.github/scripts/signing.sh
- name: Set Environment
run: sudo xcrun xcode-select -s /Applications/Xcode_14.1.app
run: sudo xcrun xcode-select -s /Applications/Xcode_13.2.1.app
- name: Test
run: |
pushd Sources/Packages
swift test
popd
build:
runs-on: macOS-latest
runs-on: macos-11.0
timeout-minutes: 10
steps:
- uses: actions/checkout@v2
@ -41,7 +41,7 @@ jobs:
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
run: ./.github/scripts/signing.sh
- name: Set Environment
run: sudo xcrun xcode-select -s /Applications/Xcode_14.1.app
run: sudo xcrun xcode-select -s /Applications/Xcode_13.2.1.app
- name: Update Build Number
env:
TAG_NAME: ${{ github.ref }}
@ -64,12 +64,8 @@ jobs:
run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip
- name: Document SHAs
run: |
echo "sha-512:"
shasum -a 512 Secretive.zip
shasum -a 512 Archive.zip
echo "sha-256:"
shasum -a 256 Secretive.zip
shasum -a 256 Archive.zip
- name: Create Release
id: create_release
uses: actions/create-release@v1

View File

@ -3,12 +3,12 @@ name: Test
on: [push, pull_request]
jobs:
test:
runs-on: macOS-latest
runs-on: macos-11.0
timeout-minutes: 10
steps:
- uses: actions/checkout@v2
- name: Set Environment
run: sudo xcrun xcode-select -s /Applications/Xcode_14.1.app
run: sudo xcrun xcode-select -s /Applications/Xcode_13.2.1.app
- name: Test
run: |
pushd Sources/Packages

View File

@ -26,15 +26,6 @@ Host *
IdentityAgent /Users/$YOUR_USERNAME/Library/Containers/com.maxgoedjen.Secretive.SecretAgent/Data/socket.ssh
```
## nushell
Add this to your `~/.ssh/config` (the path should match the socket path from the setup flow).
```
Host *
IdentityAgent /Users/$YOUR_USERNAME/Library/Containers/com.maxgoedjen.Secretive.SecretAgent/Data/socket.ssh
```
## Cyberduck
Add this to `~/Library/LaunchAgents/com.maxgoedjen.Secretive.SecretAgent.plist`
@ -60,31 +51,6 @@ Add this to `~/Library/LaunchAgents/com.maxgoedjen.Secretive.SecretAgent.plist`
Log out and log in again before launching Cyberduck.
## Mountain Duck
Add this to `~/Library/LaunchAgents/com.maxgoedjen.Secretive.SecretAgent.plist`
```
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>link-ssh-auth-sock</string>
<key>ProgramArguments</key>
<array>
<string>/bin/sh</string>
<string>-c</string>
<string>/bin/ln -sf $HOME/Library/Containers/com.maxgoedjen.Secretive.SecretAgent/Data/socket.ssh $SSH_AUTH_SOCK</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
```
Log out and log in again before launching Mountain Duck.
## GitKraken
Add this to `~/Library/LaunchAgents/com.maxgoedjen.Secretive.SecretAgent.plist`

4
FAQ.md
View File

@ -12,10 +12,6 @@ Secretive relies on the `SSH_AUTH_SOCK` environment variable being respected. Th
Please run `ssh -Tv git@github.com` in your terminal and paste the output in a [new GitHub issue](https://github.com/maxgoedjen/secretive/issues/new) with a description of your issue.
### Secretive was working for me, but now it has stopped
Try running the "Setup Secretive" process by clicking on "Help", then "Setup Secretive." If that doesn't work, follow the process above.
### Secretive prompts me to type my password instead of using my Apple Watch
1) Make sure you have enabled "Use your Apple Watch to unlock apps and your Mac" in System Preferences --> Security & Privacy:

View File

@ -11,18 +11,15 @@ public class Agent {
private let witness: SigningWitness?
private let writer = OpenSSHKeyWriter()
private let requestTracer = SigningRequestTracer()
private let certificateHandler = OpenSSHCertificateHandler()
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent.agent", category: "")
/// Initializes an agent with a store list and a witness.
/// - Parameters:
/// - storeList: The `SecretStoreList` to make available.
/// - witness: A witness to notify of requests.
public init(storeList: SecretStoreList, witness: SigningWitness? = nil) {
logger.debug("Agent is running")
Logger().debug("Agent is running")
self.storeList = storeList
self.witness = witness
certificateHandler.reloadCertificates(for: storeList.allSecrets)
}
}
@ -36,16 +33,16 @@ extension Agent {
/// - Return value:
/// - Boolean if data could be read
@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)
guard data.count > 4 else { return false}
let requestTypeInt = data[4]
guard let requestType = SSHAgent.RequestType(rawValue: requestTypeInt) else {
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
}
logger.debug("Agent handling request of type \(requestType.debugDescription)")
Logger().debug("Agent handling request of type \(requestType.debugDescription)")
let subData = Data(data[5...])
let response = handle(requestType: requestType, data: subData, reader: reader)
writer.write(response)
@ -53,25 +50,23 @@ extension Agent {
}
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()
do {
switch requestType {
case .requestIdentities:
response.append(SSHAgent.ResponseType.agentIdentitiesAnswer.data)
response.append(identities())
logger.debug("Agent returned \(SSHAgent.ResponseType.agentIdentitiesAnswer.debugDescription)")
Logger().debug("Agent returned \(SSHAgent.ResponseType.agentIdentitiesAnswer.debugDescription)")
case .signRequest:
let provenance = requestTracer.provenance(from: reader)
response.append(SSHAgent.ResponseType.agentSignResponse.data)
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 {
response.removeAll()
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)
return full
@ -84,29 +79,18 @@ extension Agent {
/// Lists the identities available for signing operations
/// - Returns: An OpenSSH formatted Data payload listing the identities available for signing operations.
func identities() -> Data {
let secrets = storeList.allSecrets
certificateHandler.reloadCertificates(for: secrets)
let secrets = storeList.stores.flatMap(\.secrets)
var count = UInt32(secrets.count).bigEndian
let countData = Data(bytes: &count, count: UInt32.bitWidth/8)
var keyData = Data()
let writer = OpenSSHKeyWriter()
for secret in secrets {
let keyBlob: Data
let curveData: Data
if let (certificateData, name) = try? certificateHandler.keyBlobAndName(for: secret) {
keyBlob = certificateData
curveData = name
} else {
keyBlob = writer.data(secret: secret)
curveData = writer.curveType(for: secret.algorithm, length: secret.keySize).data(using: .utf8)!
}
let keyBlob = writer.data(secret: secret)
keyData.append(writer.lengthAndData(of: keyBlob))
let curveData = writer.curveType(for: secret.algorithm, length: secret.keySize).data(using: .utf8)!
keyData.append(writer.lengthAndData(of: curveData))
}
logger.log("Agent enumerated \(secrets.count) identities")
Logger().debug("Agent enumerated \(secrets.count) identities")
return countData + keyData
}
@ -117,17 +101,9 @@ extension Agent {
/// - Returns: An OpenSSH formatted Data payload containing the signed data response.
func sign(data: Data, provenance: SigningRequestProvenance) throws -> Data {
let reader = OpenSSHReader(data: data)
let payloadHash = reader.readNextChunk()
let hash: Data
// Check if hash is actually an openssh certificate and reconstruct the public key if it is
if let certificatePublicKey = certificateHandler.publicKeyHash(from: payloadHash) {
hash = certificatePublicKey
} else {
hash = payloadHash
}
let hash = reader.readNextChunk()
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
}
@ -181,7 +157,7 @@ extension Agent {
try witness.witness(accessTo: secret, from: store, by: provenance)
}
logger.debug("Agent signed request")
Logger().debug("Agent signed request")
return signedData
}
@ -190,16 +166,6 @@ 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.
/// - Parameter hash: The hash to match against.
/// - Returns: A ``Secret`` and the ``SecretStore`` containing it, if a match is found.
@ -225,7 +191,6 @@ extension Agent {
case unhandledType
case noMatchingKey
case unsupportedKeyType
case notOpenSSHCertificate
}
}

View File

@ -1,11 +1,11 @@
import Foundation
/// A namespace for the SSH Agent Protocol, as described in https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#section-5.1
/// A namespace for the SSH Agent Protocol, as described in https://tools.ietf.org/id/draft-miller-ssh-agent-01.html
public enum SSHAgent {}
extension SSHAgent {
/// The type of the SSH Agent Request, as described in https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#section-5.1
/// The type of the SSH Agent Request, as described in https://tools.ietf.org/id/draft-miller-ssh-agent-01.html#rfc.section.5.1
public enum RequestType: UInt8, CustomDebugStringConvertible {
case requestIdentities = 11
@ -21,11 +21,10 @@ extension SSHAgent {
}
}
/// The type of the SSH Agent Response, as described in https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#section-5.1
/// The type of the SSH Agent Response, as described in https://tools.ietf.org/id/draft-miller-ssh-agent-01.html#rfc.section.5.1
public enum ResponseType: UInt8, CustomDebugStringConvertible {
case agentFailure = 5
case agentSuccess = 6
case agentIdentitiesAnswer = 12
case agentSignResponse = 14
@ -33,8 +32,6 @@ extension SSHAgent {
switch self {
case .agentFailure:
return "AgentFailure"
case .agentSuccess:
return "AgentSuccess"
case .agentIdentitiesAnswer:
return "AgentIdentitiesAnswer"
case .agentSignResponse:

View File

@ -40,10 +40,7 @@ extension SigningRequestTracer {
func process(from pid: Int32) -> SigningRequestProvenance.Process {
var pidAndNameInfo = self.pidAndNameInfo(from: pid)
let ppid = pidAndNameInfo.kp_eproc.e_ppid != 0 ? pidAndNameInfo.kp_eproc.e_ppid : nil
let procName = withUnsafeMutablePointer(to: &pidAndNameInfo.kp_proc.p_comm.0) { pointer in
String(cString: pointer)
}
let procName = String(cString: &pidAndNameInfo.kp_proc.p_comm.0)
let pathPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: Int(MAXPATHLEN))
_ = proc_pidpath(pid, pathPointer, UInt32(MAXPATHLEN))
let path = String(cString: pathPointer)

View File

@ -45,20 +45,14 @@ public class SocketController {
var addr = sockaddr_un()
addr.sun_family = sa_family_t(AF_UNIX)
var len: Int = 0
let len = MemoryLayout.size(ofValue: addr.sun_path) - 1
withUnsafeMutablePointer(to: &addr.sun_path.0) { pointer in
path.withCString { cstring in
len = strlen(cstring)
strncpy(pointer, cstring, len)
}
}
addr.sun_len = UInt8(len+2)
var data: Data!
withUnsafePointer(to: &addr) { pointer in
data = Data(bytes: pointer, count: MemoryLayout<sockaddr_un>.size)
// The buffer is pre-zeroed, so manual termination is unnecessary.
precondition(memccpy(pointer, path, 0, len) != nil)
}
addr.sun_len = UInt8(len)
let data = withUnsafeBytes(of: &addr, Data.init(_:))
return SocketPort(protocolFamily: AF_UNIX, socketType: SOCK_STREAM, protocol: 0, address: data)!
}

View File

@ -12,7 +12,6 @@ public class AnySecretStore: SecretStore {
private let _sign: (Data, AnySecret, SigningRequestProvenance) throws -> Data
private let _existingPersistedAuthenticationContext: (AnySecret) -> PersistedAuthenticationContext?
private let _persistAuthentication: (AnySecret, TimeInterval) throws -> Void
private let _reloadSecrets: () -> Void
private var sink: AnyCancellable?
@ -25,7 +24,6 @@ public class AnySecretStore: SecretStore {
_sign = { try secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType, for: $2) }
_existingPersistedAuthenticationContext = { secretStore.existingPersistedAuthenticationContext(secret: $0.base as! SecretStoreType.SecretType) }
_persistAuthentication = { try secretStore.persistAuthentication(secret: $0.base as! SecretStoreType.SecretType, forDuration: $1) }
_reloadSecrets = { secretStore.reloadSecrets() }
sink = secretStore.objectWillChange.sink { _ in
self.objectWillChange.send()
}
@ -59,10 +57,6 @@ public class AnySecretStore: SecretStore {
try _persistAuthentication(secret, duration)
}
public func reloadSecrets() {
_reloadSecrets()
}
}
public class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable {

View File

@ -1,120 +0,0 @@
import Foundation
import OSLog
/// Manages storage and lookup for OpenSSH certificates.
public class OpenSSHCertificateHandler {
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: NSHomeDirectory())
private let logger = Logger()
private let writer = OpenSSHKeyWriter()
private var keyBlobsAndNames: [AnySecret: (Data, Data)] = [:]
/// Initializes an OpenSSHCertificateHandler.
public init() {
}
/// Reloads any certificates in the PublicKeys folder.
/// - Parameter secrets: the secrets to look up corresponding certificates for.
public func reloadCertificates(for secrets: [AnySecret]) {
guard publicKeyFileStoreController.hasAnyCertificates else {
logger.log("No certificates, short circuiting")
return
}
keyBlobsAndNames = secrets.reduce(into: [:]) { partialResult, next in
partialResult[next] = try? loadKeyblobAndName(for: next)
}
}
/// Whether or not the certificate handler has a certifiicate associated with a given secret.
/// - Parameter secret: The secret to check for a certificate.
/// - Returns: A boolean describing whether or not the certificate handler has a certifiicate associated with a given secret
public func hasCertificate<SecretType: Secret>(for secret: SecretType) -> Bool {
keyBlobsAndNames[AnySecret(secret)] != nil
}
/// Reconstructs a public key from a ``Data``, if that ``Data`` contains an OpenSSH certificate hash. Currently only ecdsa certificates are supported
/// - Parameter certBlock: The openssh certificate to extract the public key from
/// - Returns: A ``Data`` object containing the public key in OpenSSH wire format if the ``Data`` is an OpenSSH certificate hash, otherwise nil.
public func publicKeyHash(from hash: Data) -> Data? {
let reader = OpenSSHReader(data: hash)
let certType = String(decoding: reader.readNextChunk(), as: UTF8.self)
switch certType {
case "ecdsa-sha2-nistp256-cert-v01@openssh.com",
"ecdsa-sha2-nistp384-cert-v01@openssh.com",
"ecdsa-sha2-nistp521-cert-v01@openssh.com":
_ = reader.readNextChunk() // nonce
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)
default:
return nil
}
}
/// Attempts to find an OpenSSH Certificate that corresponds to a ``Secret``
/// - Parameter secret: The secret to search for a certificate with
/// - Returns: A (``Data``, ``Data``) tuple containing the certificate and certificate name, respectively.
public func keyBlobAndName<SecretType: Secret>(for secret: SecretType) throws -> (Data, Data)? {
keyBlobsAndNames[AnySecret(secret)]
}
/// Attempts to find an OpenSSH Certificate that corresponds to a ``Secret``
/// - Parameter secret: The secret to search for a certificate with
/// - Returns: A (``Data``, ``Data``) tuple containing the certificate and certificate name, respectively.
private func loadKeyblobAndName<SecretType: Secret>(for secret: SecretType) throws -> (Data, Data)? {
let certificatePath = publicKeyFileStoreController.sshCertificatePath(for: secret)
guard FileManager.default.fileExists(atPath: certificatePath) else {
return nil
}
logger.debug("Found certificate for \(secret.name)")
let certContent = try String(contentsOfFile:certificatePath, encoding: .utf8)
let certElements = certContent.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: " ")
guard certElements.count >= 2 else {
logger.warning("Certificate found for \(secret.name) but failed to load")
throw OpenSSHCertificateError.parsingFailed
}
guard let certDecoded = Data(base64Encoded: certElements[1] as String) else {
logger.warning("Certificate found for \(secret.name) but failed to decode base64 key")
throw OpenSSHCertificateError.parsingFailed
}
if certElements.count >= 3, let certName = certElements[2].data(using: .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
}
}
}
extension OpenSSHCertificateHandler {
enum OpenSSHCertificateError: LocalizedError {
case unsupportedType
case parsingFailed
case doesNotExist
public var errorDescription: String? {
switch self {
case .unsupportedType:
return "The key type was unsupported"
case .parsingFailed:
return "Failed to properly parse the SSH certificate"
case .doesNotExist:
return "Certificate does not exist"
}
}
}
}

View File

@ -15,21 +15,15 @@ public class PublicKeyFileStoreController {
/// Writes out the keys specified to disk.
/// - Parameter secrets: The Secrets to generate keys for.
/// - Parameter clear: Whether or not any untracked files in the directory should be removed.
/// - Parameter clear: Whether or not the directory should be erased before writing keys.
public func generatePublicKeys(for secrets: [AnySecret], clear: Bool = false) throws {
logger.log("Writing public keys to disk")
if clear {
let validPaths = Set(secrets.map { publicKeyPath(for: $0) }).union(Set(secrets.map { sshCertificatePath(for: $0) }))
let untracked = Set(try FileManager.default.contentsOfDirectory(atPath: directory)
.map { "\(directory)/\($0)" })
.subtracting(validPaths)
for path in untracked {
try? FileManager.default.removeItem(at: URL(fileURLWithPath: path))
}
try? FileManager.default.removeItem(at: URL(fileURLWithPath: directory))
}
try? FileManager.default.createDirectory(at: URL(fileURLWithPath: directory), withIntermediateDirectories: false, attributes: nil)
for secret in secrets {
let path = publicKeyPath(for: secret)
let path = path(for: secret)
guard let data = keyWriter.openSSHString(secret: secret).data(using: .utf8) else { continue }
FileManager.default.createFile(atPath: path, contents: data, attributes: nil)
}
@ -40,30 +34,9 @@ public class PublicKeyFileStoreController {
/// - Parameter secret: The Secret to return the path for.
/// - Returns: The path to the Secret's public key.
/// - Warning: This method returning a path does not imply that a key has been written to disk already. This method only describes where it will be written to.
public func publicKeyPath<SecretType: Secret>(for secret: SecretType) -> String {
public func path<SecretType: Secret>(for secret: SecretType) -> String {
let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "")
return directory.appending("/").appending("\(minimalHex).pub")
}
/// Short-circuit check to ship enumerating a bunch of paths if there's nothing in the cert directory.
public var hasAnyCertificates: Bool {
do {
return try FileManager.default
.contentsOfDirectory(atPath: directory)
.filter { $0.hasSuffix("-cert.pub") }
.isEmpty == false
} catch {
return false
}
}
/// The path for a Secret's SSH Certificate public key.
/// - Parameter secret: The Secret to return the path for.
/// - Returns: The path to the SSH Certificate public key.
/// - Warning: This method returning a path does not imply that a key has a SSH certificates. This method only describes where it will be.
public func sshCertificatePath<SecretType: Secret>(for secret: SecretType) -> String {
let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "")
return directory.appending("/").appending("\(minimalHex)-cert.pub")
}
}

View File

@ -8,7 +8,7 @@ public class SecretStoreList: ObservableObject {
@Published public var stores: [AnySecretStore] = []
/// A modifiable store, if one is available.
@Published public var modifiableStore: AnySecretStoreModifiable?
private var cancellables: Set<AnyCancellable> = []
private var sinks: [AnyCancellable] = []
/// Initializes a SecretStoreList.
public init() {
@ -31,19 +31,16 @@ public class SecretStoreList: ObservableObject {
stores.reduce(false, { $0 || $1.isAvailable })
}
public var allSecrets: [AnySecret] {
stores.flatMap(\.secrets)
}
}
extension SecretStoreList {
private func addInternal(store: AnySecretStore) {
stores.append(store)
store.objectWillChange.sink {
let sink = store.objectWillChange.sink {
self.objectWillChange.send()
}.store(in: &cancellables)
}
sinks.append(sink)
}
}

View File

@ -36,9 +36,6 @@ 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.
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.

View File

@ -24,7 +24,7 @@ extension SecureEnclave {
/// Initializes a Store.
public init() {
DistributedNotificationCenter.default().addObserver(forName: .secretStoreUpdated, object: nil, queue: .main) { _ in
self.reloadSecretsInternal(notifyAgent: false)
self.reloadSecrets(notifyAgent: false)
}
loadSecrets()
}
@ -68,7 +68,7 @@ extension SecureEnclave {
throw KeychainError(statusCode: nil)
}
try savePublicKey(publicKey, name: name)
reloadSecretsInternal()
reloadSecrets()
}
public func delete(secret: Secret) throws {
@ -80,7 +80,7 @@ extension SecureEnclave {
if status != errSecSuccess {
throw KeychainError(statusCode: status)
}
reloadSecretsInternal()
reloadSecrets()
}
public func update(secret: Secret, name: String) throws {
@ -97,7 +97,7 @@ extension SecureEnclave {
if status != errSecSuccess {
throw KeychainError(statusCode: status)
}
reloadSecretsInternal()
reloadSecrets()
}
public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data {
@ -163,10 +163,6 @@ extension SecureEnclave {
}
}
public func reloadSecrets() {
reloadSecretsInternal(notifyAgent: false)
}
}
}
@ -175,15 +171,12 @@ extension SecureEnclave.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.
private func reloadSecretsInternal(notifyAgent: Bool = true) {
let before = secrets
private func reloadSecrets(notifyAgent: Bool = true) {
secrets.removeAll()
loadSecrets()
if secrets != before {
NotificationCenter.default.post(name: .secretStoreReloaded, object: self)
if notifyAgent {
DistributedNotificationCenter.default().postNotificationName(.secretStoreUpdated, object: nil, deliverImmediately: true)
}
NotificationCenter.default.post(name: .secretStoreReloaded, object: self)
if notifyAgent {
DistributedNotificationCenter.default().postNotificationName(.secretStoreUpdated, object: nil, deliverImmediately: true)
}
}

View File

@ -89,19 +89,6 @@ extension SmartCard {
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)
}
}
}
}
}
@ -115,6 +102,15 @@ extension SmartCard.Store {
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.
private func loadSecrets() {
guard let tokenID = tokenID else { return }

View File

@ -78,9 +78,6 @@ extension Stub {
public func persistAuthentication(secret: Stub.Secret, forDuration duration: TimeInterval) throws {
}
public func reloadSecrets() {
}
}
}

View File

@ -34,9 +34,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
self.socketController.handler = self.agent.handle(reader:writer:)
}
NotificationCenter.default.addObserver(forName: .secretStoreReloaded, object: nil, queue: .main) { [self] _ in
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.stores.flatMap({ $0.secrets }), clear: true)
}
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.stores.flatMap({ $0.secrets }), clear: true)
notifier.prompt()
updateSink = updater.$update.sink { update in
guard let update = update else { return }

View File

@ -154,7 +154,7 @@ class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
case Notifier.Constants.persistAuthenticationCategoryIdentitifier:
handlePersistAuthenticationResponse(response: response)
default:
break
fatalError()
}
completionHandler()

View File

@ -31,7 +31,6 @@
50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8923FCE48E0099B055 /* Preview Assets.xcassets */; };
50617D9923FCE48E0099B055 /* SecretiveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D9823FCE48E0099B055 /* SecretiveTests.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 */; };
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C72516FE6E004B5A36 /* CopyableView.swift */; };
5066A6F7251829B1004B5A36 /* ShellConfigurationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */; };
@ -124,7 +123,6 @@
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>"; };
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>"; };
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>"; };
@ -266,7 +264,6 @@
isa = PBXGroup;
children = (
50617D8423FCE48E0099B055 /* ContentView.swift */,
5065E312295517C500E16645 /* ToolbarButtonStyle.swift */,
5079BA0E250F29BF00EA86F4 /* StoreListView.swift */,
50153E21250DECA300525160 /* SecretListItemView.swift */,
50C385A42407A76D00AF2719 /* SecretDetailView.swift */,
@ -478,7 +475,6 @@
2C4A9D2F2636FFD3008CC8E2 /* RenameSecretView.swift in Sources */,
5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */,
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */,
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */,
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */,
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */,
5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */,

View File

@ -75,7 +75,6 @@
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">

View File

@ -47,9 +47,6 @@ extension Preview {
func persistAuthentication(secret: Preview.Secret, forDuration duration: TimeInterval) throws {
}
func reloadSecrets() {
}
}
class StoreModifiable: Store, SecretStoreModifiable {

View File

@ -9,9 +9,6 @@ struct ContentView<UpdaterType: UpdaterProtocol, AgentStatusCheckerType: AgentSt
@Binding var showingCreation: Bool
@Binding var runningSetup: Bool
@Binding var hasRunSetup: Bool
@State var showingAgentInfo = false
@State var activeSecret: AnySecret.ID?
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject private var storeList: SecretStoreList
@EnvironmentObject private var updater: UpdaterType
@ -23,17 +20,17 @@ struct ContentView<UpdaterType: UpdaterProtocol, AgentStatusCheckerType: AgentSt
var body: some View {
VStack {
if storeList.anyAvailable {
StoreListView(activeSecret: $activeSecret)
StoreListView(showingCreation: $showingCreation)
} else {
NoStoresView()
}
}
.frame(minWidth: 640, minHeight: 320)
.toolbar {
toolbarItem(updateNoticeView, id: "update")
toolbarItem(runningOrRunSetupView, id: "setup")
toolbarItem(appPathNoticeView, id: "appPath")
toolbarItem(newItemView, id: "new")
updateNotice
setupNotice
appPathNotice
newItem
}
.sheet(isPresented: $runningSetup) {
SetupView(visible: $runningSetup, setupComplete: $hasRunSetup)
@ -44,145 +41,121 @@ struct ContentView<UpdaterType: UpdaterProtocol, AgentStatusCheckerType: AgentSt
extension ContentView {
func toolbarItem(_ view: some View, id: String) -> ToolbarItem<String, some View> {
ToolbarItem(id: id) { view }
}
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 updateNotice: ToolbarItem<Void, AnyView> {
guard let update = updater.update else {
return ToolbarItem { AnyView(EmptyView()) }
}
}
var updateNoticeContent: (String, Color)? {
guard let update = updater.update else { return nil }
let color: Color
let text: String
if update.critical {
return ("Critical Security Update Required", .red)
text = "Critical Security Update Required"
color = .red
} else {
if updater.testBuild {
return ("Test Build", .blue)
text = "Test Build"
color = .blue
} else {
return ("Update Available", .orange)
text = "Update Available"
color = .orange
}
}
return ToolbarItem {
AnyView(
Button(action: {
selectedUpdate = update
}, label: {
Text(text)
.font(.headline)
.foregroundColor(.white)
})
.background(color)
.cornerRadius(5)
.popover(item: $selectedUpdate, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) { update in
UpdateDetailView(update: update)
}
)
}
}
@ViewBuilder
var updateNoticeView: some View {
if let update = updater.update, let (text, color) = updateNoticeContent {
Button(action: {
selectedUpdate = update
}, label: {
Text(text)
var newItem: ToolbarItem<Void, AnyView> {
guard storeList.modifiableStore?.isAvailable ?? false else {
return ToolbarItem { AnyView(EmptyView()) }
}
return ToolbarItem {
AnyView(
Button(action: {
showingCreation = true
}, label: {
Image(systemName: "plus")
})
.popover(isPresented: $showingCreation, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) {
if let modifiable = storeList.modifiableStore {
CreateSecretView(store: modifiable, showing: $showingCreation)
}
}
)
}
}
var setupNotice: ToolbarItem<Void, AnyView> {
return ToolbarItem {
AnyView(
Group {
if (runningSetup || !hasRunSetup || !agentStatusChecker.running) && !agentStatusChecker.developmentBuild {
Button(action: {
runningSetup = true
}, label: {
Group {
if hasRunSetup && !agentStatusChecker.running {
Text("Secret Agent Is Not Running")
} else {
Text("Setup Secretive")
}
}
.font(.headline)
.foregroundColor(.white)
})
.background(Color.orange)
.cornerRadius(5)
} else {
EmptyView()
}
}
)
}
}
var appPathNotice: ToolbarItem<Void, AnyView> {
let controller = ApplicationDirectoryController()
guard !controller.isInApplicationsDirectory else {
return ToolbarItem { AnyView(EmptyView()) }
}
return ToolbarItem {
AnyView(
Button(action: {
showingAppPathNotice = true
}, label: {
Group {
Text("Secretive Is Not in Applications Folder")
}
.font(.headline)
.foregroundColor(.white)
})
.buttonStyle(ToolbarButtonStyle(color: color))
.popover(item: $selectedUpdate, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) { update in
UpdateDetailView(update: update)
}
}
}
@ViewBuilder
var newItemView: some View {
if storeList.modifiableStore?.isAvailable ?? false {
Button(action: {
showingCreation = true
}, label: {
Image(systemName: "plus")
})
.sheet(isPresented: $showingCreation) {
if let modifiable = storeList.modifiableStore {
CreateSecretView(store: modifiable, showing: $showingCreation)
.onDisappear {
guard let newest = modifiable.secrets.last?.id else { return }
activeSecret = newest
}
})
.background(Color.orange)
.cornerRadius(5)
.popover(isPresented: $showingAppPathNotice, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) {
VStack {
Image(systemName: "exclamationmark.triangle")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 64)
Text("Secretive needs to be in your Applications folder to work properly. Please move it and relaunch.")
.frame(maxWidth: 300)
}
.padding()
}
}
}
}
@ViewBuilder
var setupNoticeView: some View {
Button(action: {
runningSetup = true
}, label: {
Group {
if hasRunSetup && !agentStatusChecker.running {
Text("Secret Agent Is Not Running")
} else {
Text("Setup Secretive")
}
}
.font(.headline)
.foregroundColor(.white)
})
.buttonStyle(ToolbarButtonStyle(color: .orange))
}
@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()
}
}
@ViewBuilder
var appPathNoticeView: some View {
if !ApplicationDirectoryController().isInApplicationsDirectory {
Button(action: {
showingAppPathNotice = true
}, label: {
Group {
Text("Secretive Is Not in Applications Folder")
}
.font(.headline)
.foregroundColor(.white)
})
.buttonStyle(ToolbarButtonStyle(color: .orange))
.popover(isPresented: $showingAppPathNotice, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) {
VStack {
Image(systemName: "exclamationmark.triangle")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 64)
Text("Secretive needs to be in your Applications folder to work properly. Please move it and relaunch.")
.frame(maxWidth: 300)
}
.padding()
}
)
}
}
@ -225,4 +198,3 @@ struct ContentView_Previews: PreviewProvider {
}
#endif

View File

@ -8,7 +8,6 @@ struct CopyableView: View {
var text: String
@State private var interactionState: InteractionState = .normal
@Environment(\.colorScheme) private var colorScheme
var body: some View {
VStack(alignment: .leading) {
@ -78,32 +77,38 @@ struct CopyableView: View {
}
var backgroundColor: Color {
let color: NSColor
switch interactionState {
case .normal:
return colorScheme == .dark ? Color(white: 0.2) : Color(white: 0.885)
color = .windowBackgroundColor
case .hovering:
return colorScheme == .dark ? Color(white: 0.275) : Color(white: 0.82)
color = .unemphasizedSelectedContentBackgroundColor
case .clicking:
return .accentColor
color = .selectedContentBackgroundColor
}
return Color(color)
}
var primaryTextColor: Color {
let color: NSColor
switch interactionState {
case .normal, .hovering:
return Color(.textColor)
color = .textColor
case .clicking:
return .white
color = .white
}
return Color(color)
}
var secondaryTextColor: Color {
let color: NSColor
switch interactionState {
case .normal, .hovering:
return Color(.secondaryLabelColor)
color = .secondaryLabelColor
case .clicking:
return .white
color = .white
}
return Color(color)
}
func copy() {
@ -123,9 +128,7 @@ struct CopyableView_Previews: PreviewProvider {
static var previews: some View {
Group {
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. ")
.padding()
}
}
}

View File

@ -12,35 +12,28 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
var body: some View {
VStack {
HStack {
Image(nsImage: NSApplication.shared.applicationIconImage)
.resizable()
.frame(width: 64, height: 64)
.padding()
VStack {
HStack {
Text("Create a New Secret")
.font(.largeTitle)
Text("Create a New Secret").bold()
Spacer()
}
HStack {
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 {
VStack(spacing: 20) {
Picker("", selection: $requiresAuthentication) {
Text("Requires Authentication (Biometrics or Password) before each use").tag(true)
Text("Authentication not required when Mac is unlocked").tag(false)
}
.pickerStyle(RadioGroupPickerStyle())
Spacer(minLength: 10)
HStack {
VStack(spacing: 20) {
Picker("", selection: $requiresAuthentication) {
Text("Requires Authentication (Biometrics or Password) before each use").tag(true)
Text("Authentication not required when Mac is unlocked").tag(false)
}
.pickerStyle(RadioGroupPickerStyle())
}
Spacer()
}
}
}
@ -61,208 +54,4 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
try! store.create(name: name, requiresAuthentication: requiresAuthentication)
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

View File

@ -35,6 +35,7 @@ struct DeleteSecretView<StoreType: SecretStoreModifiable>: View {
Spacer()
Button("Delete", action: delete)
.disabled(confirm != secret.name)
.keyboardShortcut(.delete)
Button("Don't Delete") {
dismissalBlock(false)
}

View File

@ -21,7 +21,7 @@ struct SecretDetailView<SecretType: Secret>: View {
CopyableView(title: "Public Key", image: Image(systemName: "key"), text: keyString)
Spacer()
.frame(height: 20)
CopyableView(title: "Public Key Path", image: Image(systemName: "lock.doc"), text: publicKeyFileStoreController.publicKeyPath(for: secret))
CopyableView(title: "Public Key Path", image: Image(systemName: "lock.doc"), text: publicKeyFileStoreController.path(for: secret))
Spacer()
}
}

View File

@ -29,8 +29,7 @@ struct SecretListItemView: View {
} else {
Text(secret.name)
}
}
.contextMenu {
}.contextMenu {
if store is AnySecretStoreModifiable {
Button(action: { isRenaming = true }) {
Text("Rename")

View File

@ -1,10 +1,11 @@
import SwiftUI
import Combine
import SecretKit
struct StoreListView: View {
@Binding var activeSecret: AnySecret.ID?
@Binding var showingCreation: Bool
@State private var activeSecret: AnySecret.ID?
@EnvironmentObject private var storeList: SecretStoreList
@ -13,7 +14,7 @@ struct StoreListView: View {
}
private func secretRenamed(secret: AnySecret) {
activeSecret = secret.id
activeSecret = nextDefaultSecret
}
var body: some View {

View File

@ -1,37 +0,0 @@
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
}
}
}
}