Compare commits

..

4 Commits

Author SHA1 Message Date
13f30f0343 WIP 2022-02-27 16:29:09 -08:00
964ef1e201 WIP 2022-02-27 15:09:59 -08:00
ee850e58d1 WIP 2022-02-27 15:08:25 -08:00
e94346583e Preview 2022-02-27 13:30:18 -08:00
23 changed files with 256 additions and 554 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 * * *" - cron: "0 8 * * *"
jobs: jobs:
build: build:
runs-on: macOS-latest runs-on: macos-11.0
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@ -19,7 +19,7 @@ jobs:
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
run: ./.github/scripts/signing.sh run: ./.github/scripts/signing.sh
- name: Set Environment - 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 - name: Update Build Number
env: env:
RUN_ID: ${{ github.run_id }} 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 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 - name: Document SHAs
run: | run: |
echo "sha-512:"
shasum -a 512 Secretive.zip shasum -a 512 Secretive.zip
shasum -a 512 Archive.zip shasum -a 512 Archive.zip
echo "sha-256:"
shasum -a 256 Secretive.zip
shasum -a 256 Archive.zip
- name: Upload App to Artifacts - name: Upload App to Artifacts
uses: actions/upload-artifact@v1 uses: actions/upload-artifact@v1
with: with:

View File

@ -6,7 +6,7 @@ on:
- '*' - '*'
jobs: jobs:
test: test:
runs-on: macOS-latest runs-on: macos-11.0
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
@ -20,14 +20,14 @@ jobs:
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
run: ./.github/scripts/signing.sh run: ./.github/scripts/signing.sh
- name: Set Environment - 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 - name: Test
run: | run: |
pushd Sources/Packages pushd Sources/Packages
swift test swift test
popd popd
build: build:
runs-on: macOS-latest runs-on: macos-11.0
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@ -41,7 +41,7 @@ jobs:
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
run: ./.github/scripts/signing.sh run: ./.github/scripts/signing.sh
- name: Set Environment - 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 - name: Update Build Number
env: env:
TAG_NAME: ${{ github.ref }} 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 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 - name: Document SHAs
run: | run: |
echo "sha-512:"
shasum -a 512 Secretive.zip shasum -a 512 Secretive.zip
shasum -a 512 Archive.zip shasum -a 512 Archive.zip
echo "sha-256:"
shasum -a 256 Secretive.zip
shasum -a 256 Archive.zip
- name: Create Release - name: Create Release
id: create_release id: create_release
uses: actions/create-release@v1 uses: actions/create-release@v1

View File

@ -3,12 +3,12 @@ name: Test
on: [push, pull_request] on: [push, pull_request]
jobs: jobs:
test: test:
runs-on: macOS-latest runs-on: macos-11.0
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set Environment - 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 - name: Test
run: | run: |
pushd Sources/Packages pushd Sources/Packages

View File

@ -26,15 +26,6 @@ Host *
IdentityAgent /Users/$YOUR_USERNAME/Library/Containers/com.maxgoedjen.Secretive.SecretAgent/Data/socket.ssh 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 ## Cyberduck
Add this to `~/Library/LaunchAgents/com.maxgoedjen.Secretive.SecretAgent.plist` 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. 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 ## GitKraken
Add this to `~/Library/LaunchAgents/com.maxgoedjen.Secretive.SecretAgent.plist` 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. 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 ### 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: 1) Make sure you have enabled "Use your Apple Watch to unlock apps and your Mac" in System Preferences --> Security & Privacy:

View File

@ -4,25 +4,6 @@ import OSLog
import SecretKit import SecretKit
import AppKit import AppKit
enum OpenSSHCertificateError: Error {
case unsupportedType
case parsingFailed
case doesNotExist
}
extension OpenSSHCertificateError: CustomStringConvertible {
public var description: 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"
}
}
}
/// The `Agent` is an implementation of an SSH agent. It manages coordination and access between a socket, traces requests, notifies witnesses and passes requests to stores. /// The `Agent` is an implementation of an SSH agent. It manages coordination and access between a socket, traces requests, notifies witnesses and passes requests to stores.
public class Agent { public class Agent {
@ -30,15 +11,13 @@ public class Agent {
private let witness: SigningWitness? private let witness: SigningWitness?
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 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
} }
@ -54,16 +33,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)
@ -71,25 +50,23 @@ 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
@ -106,24 +83,14 @@ extension Agent {
var count = UInt32(secrets.count).bigEndian var count = UInt32(secrets.count).bigEndian
let countData = Data(bytes: &count, count: UInt32.bitWidth/8) let countData = Data(bytes: &count, count: UInt32.bitWidth/8)
var keyData = Data() var keyData = Data()
let writer = OpenSSHKeyWriter()
for secret in secrets { for secret in secrets {
let keyBlob: Data let keyBlob = writer.data(secret: secret)
let curveData: Data
if let (certBlob, certName) = try? checkForCert(secret: secret) {
keyBlob = certBlob
curveData = certName
} else {
keyBlob = writer.data(secret: secret)
curveData = writer.curveType(for: secret.algorithm, length: secret.keySize).data(using: .utf8)!
}
keyData.append(writer.lengthAndData(of: keyBlob)) 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)) keyData.append(writer.lengthAndData(of: curveData))
} }
logger.log("Agent enumerated \(secrets.count) identities") Logger().debug("Agent enumerated \(secrets.count) identities")
return countData + keyData return countData + keyData
} }
@ -134,15 +101,9 @@ extension Agent {
/// - Returns: An OpenSSH formatted Data payload containing the signed data response. /// - Returns: An OpenSSH formatted Data payload containing the signed data response.
func sign(data: Data, provenance: SigningRequestProvenance) throws -> Data { func sign(data: Data, provenance: SigningRequestProvenance) throws -> Data {
let reader = OpenSSHReader(data: data) let reader = OpenSSHReader(data: data)
var hash = reader.readNextChunk() let hash = reader.readNextChunk()
// Check if hash is actually an openssh certificate and reconstruct the public key if it is
if let certPublicKey = try? getPublicKeyFromCert(certBlob: hash) {
hash = certPublicKey
}
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
} }
@ -196,93 +157,15 @@ 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
} }
/// Reconstructs a public key from a ``Data`` object that contains an OpenSSH certificate. 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
func getPublicKeyFromCert(certBlob: Data) throws -> Data {
let reader = OpenSSHReader(data: certBlob)
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()
if 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)
} else {
throw OpenSSHCertificateError.parsingFailed
}
default:
throw OpenSSHCertificateError.unsupportedType
}
}
/// Attempts to find an OpenSSH Certificate that corresponds to a ``Secret``
/// - Parameter secret: The secret to search for a certificate with
/// - Returns: Two ``Data`` objects containing the certificate and certificate name respectively
func checkForCert(secret: AnySecret) throws -> (Data, Data) {
let minimalHex = writer.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "")
let certificatePath = certsPath.appending("/").appending("\(minimalHex)-cert.pub")
if FileManager.default.fileExists(atPath: certificatePath) {
logger.debug("Found certificate for \(secret.name)")
do {
let certContent = try String(contentsOfFile:certificatePath, encoding: .utf8)
let certElements = certContent.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: " ")
if certElements.count >= 2 {
if let certDecoded = Data(base64Encoded: certElements[1] as String) {
if certElements.count >= 3 {
if 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
}
}
} else {
logger.warning("Certificate found for \(secret.name) but failed to decode base64 key")
throw OpenSSHCertificateError.parsingFailed
}
}
} catch {
logger.warning("Certificate found for \(secret.name) but failed to load")
throw OpenSSHCertificateError.parsingFailed
}
}
throw OpenSSHCertificateError.doesNotExist
}
} }
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.

View File

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

View File

@ -12,7 +12,6 @@ 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?
@ -25,7 +24,6 @@ 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()
} }
@ -59,10 +57,6 @@ 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 {

View File

@ -15,21 +15,15 @@ public class PublicKeyFileStoreController {
/// Writes out the keys specified to disk. /// Writes out the keys specified to disk.
/// - Parameter secrets: The Secrets to generate keys for. /// - 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 { public func generatePublicKeys(for secrets: [AnySecret], clear: Bool = false) throws {
logger.log("Writing public keys to disk") logger.log("Writing public keys to disk")
if clear { if clear {
let validPaths = Set(secrets.map { publicKeyPath(for: $0) }).union(Set(secrets.map { sshCertificatePath(for: $0) })) try? FileManager.default.removeItem(at: URL(fileURLWithPath: directory))
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.createDirectory(at: URL(fileURLWithPath: directory), withIntermediateDirectories: false, attributes: nil) try? FileManager.default.createDirectory(at: URL(fileURLWithPath: directory), withIntermediateDirectories: false, attributes: nil)
for secret in secrets { 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 } guard let data = keyWriter.openSSHString(secret: secret).data(using: .utf8) else { continue }
FileManager.default.createFile(atPath: path, contents: data, attributes: nil) FileManager.default.createFile(atPath: path, contents: data, attributes: nil)
} }
@ -40,18 +34,9 @@ public class PublicKeyFileStoreController {
/// - Parameter secret: The Secret to return the path for. /// - Parameter secret: The Secret to return the path for.
/// - Returns: The path to the Secret's public key. /// - 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. /// - 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: "") let minimalHex = keyWriter.openSSHMD5Fingerprint(secret: secret).replacingOccurrences(of: ":", with: "")
return directory.appending("/").appending("\(minimalHex).pub") return directory.appending("/").appending("\(minimalHex).pub")
} }
/// 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

@ -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. /// - 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.

View File

@ -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.reloadSecretsInternal(notifyAgent: false) self.reloadSecrets(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)
reloadSecretsInternal() reloadSecrets()
} }
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)
} }
reloadSecretsInternal() reloadSecrets()
} }
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)
} }
reloadSecretsInternal() reloadSecrets()
} }
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,10 +163,6 @@ extension SecureEnclave {
} }
} }
public func reloadSecrets() {
reloadSecretsInternal(notifyAgent: false)
}
} }
} }
@ -175,17 +171,14 @@ 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 reloadSecretsInternal(notifyAgent: Bool = true) { private func reloadSecrets(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() {

View File

@ -89,19 +89,6 @@ 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)
}
}
}
} }
} }
@ -115,6 +102,15 @@ 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 }

View File

@ -78,9 +78,6 @@ 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() {
}
} }
} }

View File

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

View File

@ -31,7 +31,6 @@
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 */; };
@ -124,7 +123,6 @@
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>"; };
@ -277,7 +275,6 @@
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>";
@ -478,7 +475,6 @@
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 */,

View File

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

View File

@ -47,9 +47,6 @@ 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 {

View File

@ -9,8 +9,6 @@ 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
@ -29,10 +27,10 @@ struct ContentView<UpdaterType: UpdaterProtocol, AgentStatusCheckerType: AgentSt
} }
.frame(minWidth: 640, minHeight: 320) .frame(minWidth: 640, minHeight: 320)
.toolbar { .toolbar {
toolbarItem(updateNoticeView, id: "update") updateNotice
toolbarItem(runningOrRunSetupView, id: "setup") setupNotice
toolbarItem(appPathNoticeView, id: "appPath") appPathNotice
toolbarItem(newItemView, id: "new") newItem
} }
.sheet(isPresented: $runningSetup) { .sheet(isPresented: $runningSetup) {
SetupView(visible: $runningSetup, setupComplete: $hasRunSetup) SetupView(visible: $runningSetup, setupComplete: $hasRunSetup)
@ -43,42 +41,26 @@ struct ContentView<UpdaterType: UpdaterProtocol, AgentStatusCheckerType: AgentSt
extension ContentView { extension ContentView {
var updateNotice: ToolbarItem<Void, AnyView> {
func toolbarItem(_ view: some View, id: String) -> ToolbarItem<String, some View> { guard let update = updater.update else {
ToolbarItem(id: id) { view } return ToolbarItem { AnyView(EmptyView()) }
} }
let color: Color
var needsSetup: Bool { let text: String
(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 {
return ("Critical Security Update Required", .red) text = "Critical Security Update Required"
color = .red
} else { } else {
if updater.testBuild { if updater.testBuild {
return ("Test Build", .blue) text = "Test Build"
color = .blue
} else { } else {
return ("Update Available", .orange) text = "Update Available"
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: {
@ -86,16 +68,21 @@ extension ContentView {
.font(.headline) .font(.headline)
.foregroundColor(.white) .foregroundColor(.white)
}) })
.buttonStyle(ToolbarButtonStyle(color: color)) .background(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)
} }
)
} }
} }
@ViewBuilder var newItem: ToolbarItem<Void, AnyView> {
var newItemView: some View { guard storeList.modifiableStore?.isAvailable ?? false else {
if storeList.modifiableStore?.isAvailable ?? false { return ToolbarItem { AnyView(EmptyView()) }
}
return ToolbarItem {
AnyView(
Button(action: { Button(action: {
showingCreation = true showingCreation = true
}, label: { }, label: {
@ -106,11 +93,16 @@ extension ContentView {
CreateSecretView(store: modifiable, showing: $showingCreation) CreateSecretView(store: modifiable, showing: $showingCreation)
} }
} }
)
} }
} }
@ViewBuilder var setupNotice: ToolbarItem<Void, AnyView> {
var setupNoticeView: some View { return ToolbarItem {
AnyView(
Group {
if (runningSetup || !hasRunSetup || !agentStatusChecker.running) && !agentStatusChecker.developmentBuild {
Button(action: { Button(action: {
runningSetup = true runningSetup = true
}, label: { }, label: {
@ -124,39 +116,23 @@ extension ContentView {
.font(.headline) .font(.headline)
.foregroundColor(.white) .foregroundColor(.white)
}) })
.buttonStyle(ToolbarButtonStyle(color: .orange)) .background(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()
} }
} }
@ViewBuilder var appPathNotice: ToolbarItem<Void, AnyView> {
var appPathNoticeView: some View { let controller = ApplicationDirectoryController()
if !ApplicationDirectoryController().isInApplicationsDirectory { guard !controller.isInApplicationsDirectory else {
return ToolbarItem { AnyView(EmptyView()) }
}
return ToolbarItem {
AnyView(
Button(action: { Button(action: {
showingAppPathNotice = true showingAppPathNotice = true
}, label: { }, label: {
@ -166,7 +142,8 @@ extension ContentView {
.font(.headline) .font(.headline)
.foregroundColor(.white) .foregroundColor(.white)
}) })
.buttonStyle(ToolbarButtonStyle(color: .orange)) .background(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")
@ -178,6 +155,7 @@ extension ContentView {
} }
.padding() .padding()
} }
)
} }
} }
@ -220,4 +198,3 @@ struct ContentView_Previews: PreviewProvider {
} }
#endif #endif

View File

@ -8,7 +8,6 @@ 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) {
@ -78,32 +77,38 @@ struct CopyableView: View {
} }
var backgroundColor: Color { var backgroundColor: Color {
let color: NSColor
switch interactionState { switch interactionState {
case .normal: case .normal:
return colorScheme == .dark ? Color(white: 0.2) : Color(white: 0.885) color = .windowBackgroundColor
case .hovering: case .hovering:
return colorScheme == .dark ? Color(white: 0.275) : Color(white: 0.82) color = .unemphasizedSelectedContentBackgroundColor
case .clicking: case .clicking:
return .accentColor color = .selectedContentBackgroundColor
} }
return Color(color)
} }
var primaryTextColor: Color { var primaryTextColor: Color {
let color: NSColor
switch interactionState { switch interactionState {
case .normal, .hovering: case .normal, .hovering:
return Color(.textColor) color = .textColor
case .clicking: case .clicking:
return .white color = .white
} }
return Color(color)
} }
var secondaryTextColor: Color { var secondaryTextColor: Color {
let color: NSColor
switch interactionState { switch interactionState {
case .normal, .hovering: case .normal, .hovering:
return Color(.secondaryLabelColor) color = .secondaryLabelColor
case .clicking: case .clicking:
return .white color = .white
} }
return Color(color)
} }
func copy() { func copy() {
@ -123,9 +128,7 @@ 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()
} }
} }
} }

View File

@ -8,6 +8,7 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
@State private var name = "" @State private var name = ""
@State private var requiresAuthentication = true @State private var requiresAuthentication = true
@State private var test: ThumbnailPickerView.Item = ThumbnailPickerView.Item(name: "Test", description: "Hello", thumbnail: Text("Hello"))
var body: some View { var body: some View {
VStack { VStack {
@ -25,22 +26,22 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
} }
if #available(macOS 12.0, *) { if #available(macOS 12.0, *) {
ThumbnailPickerView(items: [ 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(name: "Requires Authentication Before Use", 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", ThumbnailPickerView.Item(name: "Notify on Use",
description: "No authentication is required while your Mac is unlocked, but you will be notified when a secret is used.", description: "No authentication is required while your Mac is unlocked.",
thumbnail: NotificationView()) thumbnail: NotificationView())
], selection: $requiresAuthentication) ], selection: $test)
} else { } else {
HStack { // HStack {
VStack(spacing: 20) { // VStack(spacing: 20) {
Picker("", selection: $requiresAuthentication) { // Picker("", selection: $requiresAuthentication) {
Text("Requires Authentication (Biometrics or Password) before each use").tag(true) // Text("Requires Authentication (Biometrics or Password) before each use").tag(true)
Text("Authentication not required when Mac is unlocked").tag(false) // Text("Authentication not required when Mac is unlocked").tag(false)
} // }
.pickerStyle(RadioGroupPickerStyle()) // .pickerStyle(SegmentedPickerStyle())
Spacer(minLength: 10) // }
} // Spacer()
} // }
} }
} }
} }
@ -61,44 +62,31 @@ 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 { struct ThumbnailPickerView: View {
private let items: [Item<ValueType>] let items: [Item]
@Binding var selection: ValueType @Binding var selection: Item
init(items: [ThumbnailPickerView<ValueType>.Item<ValueType>], selection: Binding<ValueType>) {
self.items = items
_selection = selection
}
var body: some View { var body: some View {
HStack(alignment: .top) { HStack {
ForEach(items) { item in ForEach(items) { item in
VStack(alignment: .leading, spacing: 15) { VStack {
item.thumbnail item.thumbnail
.frame(height: 200) .clipShape(RoundedRectangle(cornerRadius: 10))
.overlay(RoundedRectangle(cornerRadius: 10) .overlay(RoundedRectangle(cornerRadius: 10)
.stroke(lineWidth: item.value == selection ? 15 : 0)) .stroke(lineWidth: item.id == selection.id ? 5 : 0))
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
VStack(alignment: .leading, spacing: 5) {
Text(item.name) Text(item.name)
.bold() .bold()
Text(item.description) Text(item.description)
.fixedSize(horizontal: false, vertical: true) }.onTapGesture {
selection = item
} }
} }
.frame(width: 250) }.onAppear {
.onTapGesture { selection = items.first!
withAnimation(.spring()) {
selection = item.value
}
}
}
.padding(5)
} }
} }
@ -106,15 +94,13 @@ struct ThumbnailPickerView<ValueType: Hashable>: View {
extension ThumbnailPickerView { extension ThumbnailPickerView {
struct Item<ValueType: Hashable>: Identifiable { struct Item: Identifiable {
let id = UUID() let id = UUID()
let value: ValueType
let name: String let name: String
let description: String let description: String
let thumbnail: AnyView let thumbnail: AnyView
init<ViewType: View>(value: ValueType, name: String, description: String, thumbnail: ViewType) { init<ViewType: View>(name: String, description: String, thumbnail: ViewType) {
self.value = value
self.name = name self.name = name
self.description = description self.description = description
self.thumbnail = AnyView(thumbnail) self.thumbnail = AnyView(thumbnail)
@ -123,33 +109,28 @@ extension ThumbnailPickerView {
} }
@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, *) @available(macOS 12.0, *)
struct SystemBackgroundView: View { struct SystemBackgroundView: View {
let anchor: UnitPoint let anchor: UnitPoint
var body: some View { var body: some View {
if let image = SystemBackground.shared.image { if let mainScreen = NSScreen.main, let imageURL = NSWorkspace.shared.desktopImageURL(for: mainScreen) {
Image(nsImage: image) AsyncImage(url: imageURL) { phase in
switch phase {
case .empty, .failure:
Rectangle()
.foregroundColor(Color(.systemPurple))
case .success(let image):
image
.resizable() .resizable()
.scaleEffect(3, anchor: anchor) .scaleEffect(3, anchor: anchor)
.clipped() .clipped()
.allowsHitTesting(false) @unknown default:
Rectangle()
.foregroundColor(Color(.systemPurple))
}
}
} else { } else {
Rectangle() Rectangle()
.foregroundColor(Color(.systemPurple)) .foregroundColor(Color(.systemPurple))
@ -163,41 +144,42 @@ struct AuthenticationView: View {
var body: some View { var body: some View {
ZStack { ZStack {
SystemBackgroundView(anchor: .center) SystemBackgroundView(anchor: .center)
GeometryReader { geometry in
VStack { VStack {
Spacer()
Image(systemName: "touchid") Image(systemName: "touchid")
.resizable() .resizable()
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 100)
.foregroundColor(Color(.systemRed)) .foregroundColor(Color(.systemRed))
Spacer()
Text("Touch ID Prompt") Text("Touch ID Prompt")
.font(.headline) .font(.largeTitle)
.foregroundColor(.primary) .foregroundColor(.primary)
.redacted(reason: .placeholder) .redacted(reason: .placeholder)
Spacer()
VStack { VStack {
Text("Touch ID Detail prompt.Detail two.") Text("Touch ID Detail prompt.Detail two.")
.font(.caption2) .font(.title3)
.foregroundColor(.primary) .foregroundColor(.primary)
Text("Touch ID Detail prompt.Detail two.") Text("Touch ID Detail prompt.Detail two.")
.font(.caption2) .font(.title3)
.foregroundColor(.primary) .foregroundColor(.primary)
} }
.redacted(reason: .placeholder) .redacted(reason: .placeholder)
RoundedRectangle(cornerRadius: 5) Spacer()
.frame(width: geometry.size.width, height: 20, alignment: .center) RoundedRectangle(cornerRadius: 10)
.frame(width: 275, height: 40, alignment: .center)
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
RoundedRectangle(cornerRadius: 5) RoundedRectangle(cornerRadius: 10)
.frame(width: geometry.size.width, height: 20, alignment: .center) .frame(width: 275, height: 40, alignment: .center)
.foregroundColor(Color(.unemphasizedSelectedContentBackgroundColor)) .foregroundColor(Color(.unemphasizedSelectedContentBackgroundColor))
} }
}
.padding() .padding()
.frame(width: 150)
.background( .background(
RoundedRectangle(cornerRadius: 15) RoundedRectangle(cornerRadius: 15)
.foregroundStyle(.ultraThickMaterial) .foregroundStyle(.ultraThickMaterial)
) )
.padding() .padding()
} }
} }
@ -222,15 +204,20 @@ struct NotificationView: View {
.resizable() .resizable()
.frame(width: 64, height: 64) .frame(width: 64, height: 64)
.foregroundColor(.primary) .foregroundColor(.primary)
.padding()
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Secretive") Text("Secretive")
.font(.title) .font(.largeTitle)
.foregroundColor(.primary) .foregroundColor(.primary)
Text("Secretive wants to sign") Text("Secretive wants to sign some request")
.font(.body) .font(.title3)
.foregroundColor(.primary)
Text("Secretive wants to sign some request")
.font(.title3)
.foregroundColor(.primary) .foregroundColor(.primary)
} }
}.padding() .padding()
}
.redacted(reason: .placeholder) .redacted(reason: .placeholder)
.background( .background(
RoundedRectangle(cornerRadius: 15) RoundedRectangle(cornerRadius: 15)

View File

@ -21,7 +21,7 @@ struct SecretDetailView<SecretType: Secret>: View {
CopyableView(title: "Public Key", image: Image(systemName: "key"), text: keyString) CopyableView(title: "Public Key", image: Image(systemName: "key"), text: keyString)
Spacer() Spacer()
.frame(height: 20) .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() Spacer()
} }
} }

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
}
}
}
}