Compare commits

..

52 Commits

Author SHA1 Message Date
9d6bbd2a04 Fix focus resignation (#227) 2021-08-08 22:57:01 +00:00
35aea0729d Switch to Xcode 12.5.1 (#228)
* Update release.yml

* Update test.yml
2021-08-08 22:55:23 +00:00
c5bd4c2189 . (#221) 2021-06-01 00:21:58 -07:00
8114acf50a Add option to rename keys/secrets (#216)
* Add option to rename secrets

* Address PR comments

Co-authored-by: Max Goedjen <max.goedjen@gmail.com>
2021-05-31 23:20:38 -07:00
cd965b9ec6 Fix background issue (#217) 2021-05-10 03:05:45 +00:00
f30d1f802f Configure ssh directly instead of setting environment variable (#208) 2021-03-06 15:21:33 -08:00
af479408a4 Switch GitHub Actions releaser to use API key instead of app specific password (#212)
* Update release.yml

* Update signing.sh

* Update release.yml

* Fix weird spacing

* Update signing.sh

* Update release.yml

* Update signing.sh
2021-02-26 06:28:02 +00:00
42b034270a Changed checkbox to radio button for clearer UI. (#210) 2021-01-30 23:01:17 -08:00
dca340ad40 Change hard-coded bundleID to automatically retrieved one (#202) 2021-01-18 20:08:52 -08:00
8dfabd35aa Update readme assets for design changes (#204)
* Delete app.png

* Add files via upload

* Delete app.png

* Add files via upload

* Add template to sketch
2021-01-18 18:12:19 -08:00
9c28efab5c Improve release template (#206)
* Improve release template

* Update release.yml

* Update test.yml
2021-01-18 15:14:23 -08:00
698a69a034 Improve updater (#203) 2021-01-18 00:49:26 -08:00
4105c1d6f6 Fix arrow position (#200)
* Fix arrow position

* Restore that
2021-01-17 19:35:22 -08:00
4de805dd37 Add support for SHA256 fingerprints (#198)
* Add SHA256

* Update tests

* Fix padding
2021-01-17 16:16:38 -08:00
0544287141 Add internet access policy (#199) 2021-01-17 15:59:35 -08:00
8ec19d1fb7 brew cask install is deprecated (#192)
Use `brew install` instead of `brew cask install`
2021-01-17 13:37:30 -08:00
6c3748b6bf Comment (#181) 2020-11-29 13:15:14 -08:00
dc91f61dba Update ApplicationDirectoryController.swift (#178) 2020-11-25 18:13:46 -08:00
702388fdf8 Update FAQ.md (#176) 2020-11-25 16:00:04 -08:00
a368acea62 . (#174) 2020-11-13 00:02:01 -08:00
76514d410b Fix issues around upgrading (#173)
* Re-check after

* Completion

* Relaunch
2020-11-12 22:55:52 -08:00
08b5c7b13c Fix #170 (#171) 2020-11-12 19:07:47 -08:00
f801853878 Release 11 (#168)
* Major version test

* .
2020-11-11 17:16:36 -08:00
3c9659bd06 Major version test (#167) 2020-11-11 17:13:21 -08:00
4d4b20c38d Fix readme link (#166) 2020-11-11 17:08:02 -08:00
1f0100ff2d Relaunch agent (#165)
* Check on foreground

* Better processing.

* Update icons for agent too

* Add force launch.

* Force launch.
2020-11-11 17:00:15 -08:00
59ec40c611 Switch stuff to popovers to behave better in Big Sur (#163)
* Moving create to a popover.

* Switch to popover here too
2020-11-11 16:45:39 -08:00
edf01cc6d4 Restore changes for Big Sur images. (#160)
* Switch test to Big Sur

* Switch to new logger api

* Fix versions

* .
2020-11-11 15:32:28 -08:00
de21e1040e Break out app faq (#162)
* Break out app faq

* .
2020-11-11 15:24:32 -08:00
d47c5424ad Add Fork to FAQs (#159)
* Add Fork to FAQ's

* Update FAQ.md

Co-authored-by: Max Goedjen <max.goedjen@gmail.com>
2020-11-11 15:11:27 -08:00
b85acebae1 Update icon for big sur and add sketch (#158)
* Update icon for big sur and add sketch

* Set xcode

* Set xcode
2020-11-07 23:31:28 -08:00
4d4619bdd8 Update to checkout v2 (#153)
* Update release.yml

* Update test.yml
2020-10-12 20:48:51 -07:00
50adf4cd48 Update Credits.rtf (#150) 2020-09-27 23:20:29 -07:00
dc5c21c568 Move app name + iconurl to tracer. (#149) 2020-09-27 23:09:23 -07:00
3328ada3bf Update touch id prompt image. (#147) 2020-09-23 22:20:54 -07:00
3774352dfd Show request details in Secure prompts (#146) 2020-09-23 21:51:48 -07:00
85e096f8cc Fix broken updater check (#145) 2020-09-22 00:17:22 -07:00
331e4ed0d6 Update for Big Sur & SwiftUI 2 (#128) 2020-09-21 23:12:50 -07:00
4cc312d4fa Update FAQ.md (#132) 2020-09-18 13:22:15 -07:00
f3d70a57c5 Create SECURITY.md (#123) 2020-07-12 15:14:40 -07:00
55b57e0cbf Remove secret requirement for tests, open to PRs (#122)
* Update test.yml

* Test

* Test

* Test

* Test

* Update test.yml
2020-07-12 15:12:18 -07:00
82dcb76ee6 Update test.yml 2020-07-12 14:53:41 -07:00
bd0e27060c Update test.yml 2020-07-12 14:43:32 -07:00
393f2797ab (docs): fixed small error (#100)
Co-authored-by: Max Goedjen <max.goedjen@gmail.com>
2020-07-12 14:06:08 -07:00
f1bf863a9d Add Homebrew install intructions (#106) 2020-07-12 14:05:48 -07:00
c09d1e4020 Add FAQ entry on how to get WatchOS auth working (#118)
* Add FAQ entry on how to get WatchOS auth working

Fixes #113

* FAQ (Apple Watch) grammar/phrasing suggestions

Co-authored-by: Max Goedjen <max.goedjen@gmail.com>

Co-authored-by: Max Goedjen <max.goedjen@gmail.com>
2020-07-12 14:05:30 -07:00
52225ab5e3 Fix typo (#120) 2020-07-07 19:12:32 -07:00
8b7e3f6b87 Fixes #115 (#116) 2020-07-04 16:25:14 -07:00
bc25576c1f Add installation instructions (#102)
Fixes #101
2020-06-27 18:45:22 -07:00
b234100aa5 Fileprivate -> private (#91) 2020-05-15 23:19:00 -07:00
bc0a45366a Fix typo (#89) 2020-04-06 21:32:22 -07:00
6dbb3b7fb3 Add back Xcode 11.4 changes (#87) 2020-04-05 16:05:45 -07:00
77 changed files with 2008 additions and 915 deletions

BIN
.github/readme/app.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 348 KiB

After

Width:  |  Height:  |  Size: 456 KiB

BIN
.github/readme/apple_watch_auth_mac.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 190 KiB

View File

@ -10,10 +10,13 @@ security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k ci ci.keyc
# Import Profiles # Import Profiles
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
echo $HOST_PROFILE_DATA | base64 -d -o Host.provisionprofile echo $HOST_PROFILE_DATA | base64 -d -o Host.provisionprofile
HOST_UUID=`grep UUID -A1 -a Host.provisionprofile | grep -io "[-A-F0-9]\{36\}"` HOST_UUID=`grep UUID -A1 -a Host.provisionprofile | grep -io "[-A-F0-9]\{36\}"`
cp Host.provisionprofile ~/Library/MobileDevice/Provisioning\ Profiles/$HOST_UUID.provisionprofile cp Host.provisionprofile ~/Library/MobileDevice/Provisioning\ Profiles/$HOST_UUID.provisionprofile
echo $AGENT_PROFILE_DATA | base64 -d -o Agent.provisionprofile echo $AGENT_PROFILE_DATA | base64 -d -o Agent.provisionprofile
AGENT_UUID=`grep UUID -A1 -a Agent.provisionprofile | grep -io "[-A-F0-9]\{36\}"` AGENT_UUID=`grep UUID -A1 -a Agent.provisionprofile | grep -io "[-A-F0-9]\{36\}"`
cp Agent.provisionprofile ~/Library/MobileDevice/Provisioning\ Profiles/$AGENT_UUID.provisionprofile cp Agent.provisionprofile ~/Library/MobileDevice/Provisioning\ Profiles/$AGENT_UUID.provisionprofile
# Create directories for ASC key
mkdir ~/.private_keys
echo -n "$APPLE_API_KEY_DATA" > ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8

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
@ -16,32 +16,29 @@ jobs:
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
HOST_PROFILE_DATA: ${{ secrets.HOST_PROFILE_DATA }} HOST_PROFILE_DATA: ${{ secrets.HOST_PROFILE_DATA }}
AGENT_PROFILE_DATA: ${{ secrets.AGENT_PROFILE_DATA }} AGENT_PROFILE_DATA: ${{ secrets.AGENT_PROFILE_DATA }}
APPLE_API_KEY_DATA: ${{ secrets.APPLE_API_KEY_DATA }}
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
run: ./.github/scripts/signing.sh run: ./.github/scripts/signing.sh
- name: Set Environment
run: sudo xcrun xcode-select -s /Applications/Xcode_12.5.1.app
- name: Test - name: Test
run: xcrun xcodebuild test -project Secretive.xcodeproj -scheme Secretive run: xcrun xcodebuild test -project Secretive.xcodeproj -scheme Secretive
build: build:
runs-on: macOS-latest runs-on: macos-11.0
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v2
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
body: "Build: https://github.com/maxgoedjen/secretive/actions/runs/${{ github.run_id }}"
draft: true
prerelease: false
- name: Setup Signing - name: Setup Signing
env: env:
SIGNING_DATA: ${{ secrets.SIGNING_DATA }} SIGNING_DATA: ${{ secrets.SIGNING_DATA }}
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
HOST_PROFILE_DATA: ${{ secrets.HOST_PROFILE_DATA }} HOST_PROFILE_DATA: ${{ secrets.HOST_PROFILE_DATA }}
AGENT_PROFILE_DATA: ${{ secrets.AGENT_PROFILE_DATA }} AGENT_PROFILE_DATA: ${{ secrets.AGENT_PROFILE_DATA }}
APPLE_API_KEY_DATA: ${{ secrets.APPLE_API_KEY_DATA }}
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
run: ./.github/scripts/signing.sh run: ./.github/scripts/signing.sh
- name: Set Environment
run: sudo xcrun xcode-select -s /Applications/Xcode_12.5.1.app
- name: Update Build Number - name: Update Build Number
env: env:
TAG_NAME: ${{ github.ref }} TAG_NAME: ${{ github.ref }}
@ -59,13 +56,37 @@ jobs:
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive ./Archive.zip ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive ./Archive.zip
- name: Notarize - name: Notarize
env: env:
APPLE_USERNAME: ${{ secrets.APPLE_USERNAME }} APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
run: xcrun altool --notarize-app --primary-bundle-id "com.maxgoedjen.secretive.host" --username $APPLE_USERNAME --password $APPLE_PASSWORD --file Secretive.zip run: xcrun altool --notarize-app --primary-bundle-id "com.maxgoedjen.secretive.host" --apiKey $APPLE_API_KEY_ID --apiIssuer $APPLE_API_ISSUER --file Secretive.zip
- name: Document SHAs - name: Document SHAs
run: | run: |
shasum -a 512 Secretive.zip shasum -a 512 Secretive.zip
shasum -a 512 Archive.zip shasum -a 512 Archive.zip
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
body: |
Update description
## Features
## Fixes
## Minimum macOS Version
## Build
https://github.com/maxgoedjen/secretive/actions/runs/${{ github.run_id }}
draft: true
prerelease: false
- name: Upload App to Release - name: Upload App to Release
id: upload-release-asset id: upload-release-asset
uses: actions/upload-release-asset@v1.0.1 uses: actions/upload-release-asset@v1.0.1

View File

@ -1,18 +1,13 @@
name: Test name: Test
on: push 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@v1 - uses: actions/checkout@v2
- name: Setup Signing - name: Set Environment
env: run: sudo xcrun xcode-select -s /Applications/Xcode_12.5.1.app
SIGNING_DATA: ${{ secrets.SIGNING_DATA }}
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
HOST_PROFILE_DATA: ${{ secrets.HOST_PROFILE_DATA }}
AGENT_PROFILE_DATA: ${{ secrets.AGENT_PROFILE_DATA }}
run: ./.github/scripts/signing.sh
- name: Test - name: Test
run: xcrun xcodebuild test -project Secretive.xcodeproj -scheme Secretive run: xcrun xcodebuild test -project Secretive.xcodeproj -scheme Secretive

1
.gitignore vendored
View File

@ -91,3 +91,4 @@ iOSInjectionProject/
# Build script products # Build script products
Archive.xcarchive Archive.xcarchive
.DS_Store

33
APP_CONFIG.md Normal file
View File

@ -0,0 +1,33 @@
# Setting up Third Party Apps FAQ
## Tower
Tower provides [instructions](https://www.git-tower.com/help/mac/integration/environment).
## GitHub Desktop
Should just work, no configuration needed
## Fork
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
```
## VS Code
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
```
# The app I use isn't listed here!
If you know how to get it set up, please open a PR for this page and add it! Contributions are very welcome.
If you're not able to get it working, please file a [GitHub issue](https://github.com/maxgoedjen/secretive/issues/new) for it. No guarantees we'll be able to get it working, but chances are someone else in the community might be able to.

View File

@ -11,8 +11,14 @@ public class Updater: ObservableObject, UpdaterProtocol {
@Published public var update: Release? @Published public var update: Release?
public init() { private let osVersion: SemVer
checkForUpdates()
public init(checkOnLaunch: Bool, osVersion: SemVer = SemVer(ProcessInfo.processInfo.operatingSystemVersion)) {
self.osVersion = osVersion
if checkOnLaunch {
// Don't do a launch check if the user hasn't seen the setup prompt explaining updater yet.
checkForUpdates()
}
let timer = Timer.scheduledTimer(withTimeInterval: 60*60*24, repeats: true) { _ in let timer = Timer.scheduledTimer(withTimeInterval: 60*60*24, repeats: true) { _ in
self.checkForUpdates() self.checkForUpdates()
} }
@ -22,8 +28,8 @@ public class Updater: ObservableObject, UpdaterProtocol {
public func checkForUpdates() { public func checkForUpdates() {
URLSession.shared.dataTask(with: Constants.updateURL) { data, _, _ in URLSession.shared.dataTask(with: Constants.updateURL) { data, _, _ in
guard let data = data else { return } guard let data = data else { return }
guard let release = try? JSONDecoder().decode(Release.self, from: data) else { return } guard let releases = try? JSONDecoder().decode([Release].self, from: data) else { return }
self.evaluate(release: release) self.evaluate(releases: releases)
}.resume() }.resume()
} }
@ -39,28 +45,23 @@ public class Updater: ObservableObject, UpdaterProtocol {
extension Updater { extension Updater {
func evaluate(release: Release) { func evaluate(releases: [Release]) {
guard let release = releases
.sorted()
.reversed()
.filter({ !$0.prerelease })
.first(where: { $0.minimumOSVersion <= osVersion }) else { return }
guard !userIgnored(release: release) else { return } guard !userIgnored(release: release) else { return }
let latestVersion = semVer(from: release.name) guard !release.prerelease else { return }
let currentVersion = semVer(from: Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String) let latestVersion = SemVer(release.name)
for (latest, current) in zip(latestVersion, currentVersion) { let currentVersion = SemVer(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0")
if latest > current { if latestVersion > currentVersion {
DispatchQueue.main.async { DispatchQueue.main.async {
self.update = release self.update = release
}
return
} }
} }
} }
func semVer(from stringVersion: String) -> [Int] {
var split = stringVersion.split(separator: ".").compactMap { Int($0) }
while split.count < 3 {
split.append(0)
}
return split
}
func userIgnored(release: Release) -> Bool { func userIgnored(release: Release) -> Bool {
guard !release.critical else { return false } guard !release.critical else { return false }
return defaults.bool(forKey: release.name) return defaults.bool(forKey: release.name)
@ -71,10 +72,46 @@ extension Updater {
} }
} }
public struct SemVer {
let versionNumbers: [Int]
public init(_ version: String) {
// Betas have the format 1.2.3_beta1
let strippedBeta = version.split(separator: "_").first!
var split = strippedBeta.split(separator: ".").compactMap { Int($0) }
while split.count < 3 {
split.append(0)
}
versionNumbers = split
}
public init(_ version: OperatingSystemVersion) {
versionNumbers = [version.majorVersion, version.minorVersion, version.patchVersion]
}
}
extension SemVer: Comparable {
public static func < (lhs: SemVer, rhs: SemVer) -> Bool {
for (latest, current) in zip(lhs.versionNumbers, rhs.versionNumbers) {
if latest < current {
return true
} else if latest > current {
return false
}
}
return false
}
}
extension Updater { extension Updater {
enum Constants { enum Constants {
static let updateURL = URL(string: "https://api.github.com/repos/maxgoedjen/secretive/releases/latest")! static let updateURL = URL(string: "https://api.github.com/repos/maxgoedjen/secretive/releases")!
} }
} }
@ -82,22 +119,51 @@ extension Updater {
public struct Release: Codable { public struct Release: Codable {
public let name: String public let name: String
public let prerelease: Bool
public let html_url: URL public let html_url: URL
public let body: String public let body: String
public init(name: String, html_url: URL, body: String) { public init(name: String, prerelease: Bool, html_url: URL, body: String) {
self.name = name self.name = name
self.prerelease = prerelease
self.html_url = html_url self.html_url = html_url
self.body = body self.body = body
} }
} }
extension Release: Identifiable {
public var id: String {
html_url.absoluteString
}
}
extension Release: Comparable {
public static func < (lhs: Release, rhs: Release) -> Bool {
lhs.version < rhs.version
}
}
extension Release { extension Release {
public var critical: Bool { public var critical: Bool {
return body.contains(Constants.securityContent) body.contains(Constants.securityContent)
}
public var version: SemVer {
SemVer(name)
}
public var minimumOSVersion: SemVer {
guard let range = body.range(of: "Minimum macOS Version"),
let numberStart = body.rangeOfCharacter(from: CharacterSet.decimalDigits, options: [], range: range.upperBound..<body.endIndex) else { return SemVer("11.0.0") }
let numbersEnd = body.rangeOfCharacter(from: CharacterSet.whitespacesAndNewlines, options: [], range: numberStart.upperBound..<body.endIndex)?.lowerBound ?? body.endIndex
let version = numberStart.lowerBound..<numbersEnd
return SemVer(String(body[version]))
} }
} }

22
BriefTests/Info.plist Normal file
View File

@ -0,0 +1,22 @@
<?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>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>

View File

@ -0,0 +1,104 @@
import XCTest
@testable import Brief
class ReleaseParsingTests: XCTestCase {
func testNonCritical() {
let release = Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Initial release")
XCTAssert(release.critical == false)
}
func testCritical() {
let release = Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update")
XCTAssert(release.critical == true)
}
func testOSMissing() {
let release = Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update")
XCTAssert(release.minimumOSVersion == SemVer("11.0.0"))
}
func testOSPresentWithContentBelow() {
let release = Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update ##Minimum macOS Version\n1.2.3\nBuild info")
XCTAssert(release.minimumOSVersion == SemVer("1.2.3"))
}
func testOSPresentAtEnd() {
let release = Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update Minimum macOS Version: 1.2.3")
XCTAssert(release.minimumOSVersion == SemVer("1.2.3"))
}
func testOSWithMacOSPrefix() {
let release = Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update Minimum macOS Version: macOS 1.2.3")
XCTAssert(release.minimumOSVersion == SemVer("1.2.3"))
}
func testOSGreaterThanMinimum() {
let release = Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update Minimum macOS Version: 1.2.3")
XCTAssert(release.minimumOSVersion < SemVer("11.0.0"))
}
func testOSEqualToMinimum() {
let release = Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update Minimum macOS Version: 11.2.3")
XCTAssert(release.minimumOSVersion <= SemVer("11.2.3"))
}
func testOSLessThanMinimum() {
let release = Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update Minimum macOS Version: 1.2.3")
XCTAssert(release.minimumOSVersion > SemVer("1.0.0"))
}
func testGreatestSelectedIfOldPatchIsPublishedLater() {
// If 2.x.x series has been published, and a patch for 1.x.x is issued
// 2.x.x should still be selected if user can run it.
let updater = Updater(checkOnLaunch: false, osVersion: SemVer("2.2.3"))
let two = Release(name: "2.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "2.0 available! Minimum macOS Version: 2.2.3")
let releases = [
Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Initial release Minimum macOS Version: 1.2.3"),
Release(name: "1.0.1", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Bug fixes Minimum macOS Version: 1.2.3"),
two,
Release(name: "1.0.2", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Emergency patch! Minimum macOS Version: 1.2.3"),
]
let expectation = XCTestExpectation()
updater.evaluate(releases: releases)
DispatchQueue.main.async {
XCTAssert(updater.update == two)
expectation.fulfill()
}
wait(for: [expectation], timeout: 1)
}
func testLatestVersionIsRunnable() {
// If the 2.x.x series has been published but the user can't run it
// the last version the user can run should be selected.
let updater = Updater(checkOnLaunch: false, osVersion: SemVer("1.2.3"))
let oneOhTwo = Release(name: "1.0.2", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Emergency patch! Minimum macOS Version: 1.2.3")
let releases = [
Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Initial release Minimum macOS Version: 1.2.3"),
Release(name: "1.0.1", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Bug fixes Minimum macOS Version: 1.2.3"),
Release(name: "2.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "2.0 available! Minimum macOS Version: 2.2.3"),
Release(name: "1.0.2", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Emergency patch! Minimum macOS Version: 1.2.3"),
]
let expectation = XCTestExpectation()
updater.evaluate(releases: releases)
DispatchQueue.main.async {
XCTAssert(updater.update == oneOhTwo)
expectation.fulfill()
}
wait(for: [expectation], timeout: 1)
}
func testSorting() {
let two = Release(name: "2.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "2.0 available!")
let releases = [
Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Initial release"),
Release(name: "1.0.1", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Bug fixes"),
two,
Release(name: "1.0.2", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Emergency patch!"),
]
let sorted = releases.sorted().reversed().first
XCTAssert(sorted == two)
}
}

View File

@ -0,0 +1,51 @@
import XCTest
@testable import Brief
class SemVerTests: XCTestCase {
func testEqual() {
let current = SemVer("1.0.2")
let old = SemVer("1.0.2")
XCTAssert(!(current > old))
}
func testPatchGreaterButMinorLess() {
let current = SemVer("1.1.0")
let old = SemVer("1.0.2")
XCTAssert(current > old)
}
func testMajorSameMinorGreater() {
let current = SemVer("1.0.2")
let new = SemVer("1.0.3")
XCTAssert(current < new)
}
func testMajorGreaterMinorLesser() {
let current = SemVer("1.0.2")
let new = SemVer("2.0.0")
XCTAssert(current < new)
}
func testRegularParsing() {
let current = SemVer("1.0.2")
XCTAssert(current.versionNumbers == [1, 0, 2])
}
func testNoPatch() {
let current = SemVer("1.1")
XCTAssert(current.versionNumbers == [1, 1, 0])
}
func testGarbage() {
let current = SemVer("Test")
XCTAssert(current.versionNumbers == [0, 0, 0])
}
func testBeta() {
let current = SemVer("1.0.2")
let new = SemVer("1.1.0_beta1")
XCTAssert(current < new)
}
}

View File

@ -8,12 +8,20 @@ Security is obviously paramount for a project like Secretive. As such, any contr
### Dependencies ### Dependencies
Secretive is desigend to be easily auditable by people who are considering using it. In keeping with this, Secretive has no third party dependencies, and any contributions which bring in new dependencies will be rejected. Secretive is designed to be easily auditable by people who are considering using it. In keeping with this, Secretive has no third party dependencies, and any contributions which bring in new dependencies will be rejected.
## Code of Conduct ## Code of Conduct
All contributors must abide by the [Code of Conduct](CODE_OF_CONDUCT.md) All contributors must abide by the [Code of Conduct](CODE_OF_CONDUCT.md)
## Credits
If you make a material contribution to the app, please add yourself to the end of the [credits](https://github.com/maxgoedjen/secretive/blob/main/Secretive/Credits.rtf).
## Collaborator Status
I will not grant collaborator access to any contributors for this repository. This is basically just because collaborators [can accesss the secrets Secretive uses for the signing credentials stored in the repository](https://docs.github.com/en/actions/reference/encrypted-secrets#accessing-your-secrets).
## Secretive is Opinionated ## Secretive is Opinionated
I'm releasing Secretive as open source so that other people can use it and audit it, feeling comfortable in knowing that the source is available so they can see what it's doing. I have a pretty strong idea of what I'd like this project to look like, and I may respectfully decline contributions that don't line up with that vision. If you'd like to propose a change before implementing, please feel free to [Open an Issue with the proposed tag](https://github.com/maxgoedjen/secretive/issues/new?labels=proposed). I'm releasing Secretive as open source so that other people can use it and audit it, feeling comfortable in knowing that the source is available so they can see what it's doing. I have a pretty strong idea of what I'd like this project to look like, and I may respectfully decline contributions that don't line up with that vision. If you'd like to propose a change before implementing, please feel free to [Open an Issue with the proposed tag](https://github.com/maxgoedjen/secretive/issues/new?labels=proposed).

View File

@ -29,12 +29,21 @@
} }
}, },
{ {
"enabled" : false,
"parallelizable" : true, "parallelizable" : true,
"target" : { "target" : {
"containerPath" : "container:Secretive.xcodeproj", "containerPath" : "container:Secretive.xcodeproj",
"identifier" : "50617D9323FCE48E0099B055", "identifier" : "50617D9323FCE48E0099B055",
"name" : "SecretiveTests" "name" : "SecretiveTests"
} }
},
{
"parallelizable" : true,
"target" : {
"containerPath" : "container:Secretive.xcodeproj",
"identifier" : "5091D31E2519D56D0049FD9B",
"name" : "BriefTests"
}
} }
], ],
"version" : 1 "version" : 1

28
FAQ.md
View File

@ -4,18 +4,30 @@
The secure enclave doesn't allow import or export of private keys. For any new computer, you should just create a new set of keys. If you're using a smart card, you _might_ be able to export your private key from the vendor's software. The secure enclave doesn't allow import or export of private keys. For any new computer, you should just create a new set of keys. If you're using a smart card, you _might_ be able to export your private key from the vendor's software.
### Secretive doesn't work with my git client ### Secretive doesn't work with my git client/app
Secretive relies on the `SSH_AUTH_SOCK` environment variable being respected. The `git` and `ssh` command line tools natively respect this, but third party apps may require some configuration to work. A non-exhaustive list of clients is provided here: Secretive relies on the `SSH_AUTH_SOCK` environment variable being respected. The `git` and `ssh` command line tools natively respect this, but third party apps may require some configuration to work. A non-exhaustive list of setup steps is provided in the [App Config FAQ](APP_CONFIG.md).
Tower - [Instructions](https://www.git-tower.com/help/mac/integration/environment)
GitHub Desktop: Should just work, no configuration needed
### Secretive isn't working for me ### Secretive isn't working for me
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 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:
![System Preferences Setting](.github/readme/apple_watch_system_prefs.png)
2) Ensure that unlocking your Mac with Apple Watch is working (lock and unlock at least once)
3) Now you should get prompted on the watch when your key is accessed. Double click the side button to approve:
![Apple Watch Prompt](.github/readme/apple_watch_auth_mac.png)
![Apple Watch Prompt](.github/readme/apple_watch_auth_watch.png)
### How do I tell SSH to use a specific key?
You can create a `mykey.pub` (where `mykey` is the name of your key) in your `~/.ssh/` directory with the contents of your public key, and specify that you want to use that key in your `~/.ssh/config`. [This ServerFault answer](https://serverfault.com/a/295771) has more details on setting that up
### Why should I trust you? ### Why should I trust you?
You shouldn't, for a piece of software like this. Secretive, by design, has an auditable build process. Each build has a fully auditable build log, showing the source it was built from and a SHA of the build product. You can check the SHA of the zip you download against the SHA output in the build log (which is linked in the About window). You shouldn't, for a piece of software like this. Secretive, by design, has an auditable build process. Each build has a fully auditable build log, showing the source it was built from and a SHA of the build product. You can check the SHA of the zip you download against the SHA output in the build log (which is linked in the About window).
@ -24,6 +36,10 @@ You shouldn't, for a piece of software like this. Secretive, by design, has an a
Awesome! Just bear in mind that because an app only has access to the keychain items that it created, if you have secrets that you created with the prebuilt version of Secretive, you'll be unable to access them using your own custom build (since you'll have changed the bundled ID). Awesome! Just bear in mind that because an app only has access to the keychain items that it created, if you have secrets that you created with the prebuilt version of Secretive, you'll be unable to access them using your own custom build (since you'll have changed the bundled ID).
### What's this network request to GitHub?
Secretive checks in with GitHub's releases API to check if there's a new version of Secretive available. You can audit the source code for this feature [here](https://github.com/maxgoedjen/secretive/blob/main/Brief/Updater.swift).
### I have a security issue ### I have a security issue
Please contact [max.goedjen@gmail.com](mailto:max.goedjen@gmail.com) with a subject containing "SECRETIVE SECURITY" immediately with details, and I'll address the issue and credit you ASAP. Please contact [max.goedjen@gmail.com](mailto:max.goedjen@gmail.com) with a subject containing "SECRETIVE SECURITY" immediately with details, and I'll address the issue and credit you ASAP.

BIN
Icon.sketch Normal file

Binary file not shown.

View File

@ -20,7 +20,7 @@ If your Mac has a Secure Enclave, it also has support for strong access controls
### Notifications ### Notifications
Secretive also notifies you whenever your keys are acceessed, so you're never caught off guard. Secretive also notifies you whenever your keys are accessed, so you're never caught off guard.
<img src="/.github/readme/notification.png" alt="Screenshot of Secretive notifying the user"> <img src="/.github/readme/notification.png" alt="Screenshot of Secretive notifying the user">
@ -30,6 +30,16 @@ For Macs without Secure Enclaves, you can configure a Smart Card (such as a Yubi
## Getting Started ## Getting Started
### Installation
#### Direct Download
You can download the latest release over on the [Releases Page](https://github.com/maxgoedjen/secretive/releases)
#### Using Homebrew
brew install secretive
### FAQ ### FAQ
There's a [FAQ here](FAQ.md). There's a [FAQ here](FAQ.md).
@ -44,7 +54,7 @@ While Secretive uses the Secure Enclave for key storage, it still relies on Keyc
### Backups and Transfers to New Machines ### Backups and Transfers to New Machines
Beacuse secrets in the Secure Enclave are not exportable, they are not able to be backed up, and you will not be able to transfer them to a new machine. If you get a new Mac, just create a new set of secrets specific to that Mac. Because secrets in the Secure Enclave are not exportable, they are not able to be backed up, and you will not be able to transfer them to a new machine. If you get a new Mac, just create a new set of secrets specific to that Mac.
## Security ## Security

9
SECURITY.md Normal file
View File

@ -0,0 +1,9 @@
# Security Policy
## Supported Versions
The latest version on the [Releases page](https://github.com/maxgoedjen/secretive/releases) is the only currently supported version.
## Reporting a Vulnerability
If you discover any vulnerabilities in this project, please notify max.goedjen@gmail.com with the subject containing "SECRETIVE SECURITY."

View File

@ -8,25 +8,25 @@ import Brief
@NSApplicationMain @NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate { class AppDelegate: NSObject, NSApplicationDelegate {
let storeList: SecretStoreList = { private let storeList: SecretStoreList = {
let list = SecretStoreList() let list = SecretStoreList()
list.add(store: SecureEnclave.Store()) list.add(store: SecureEnclave.Store())
list.add(store: SmartCard.Store()) list.add(store: SmartCard.Store())
return list return list
}() }()
let updater = Updater() private let updater = Updater(checkOnLaunch: false)
let notifier = Notifier() private let notifier = Notifier()
lazy var agent: Agent = { private lazy var agent: Agent = {
Agent(storeList: storeList, witness: notifier) Agent(storeList: storeList, witness: notifier)
}() }()
lazy var socketController: SocketController = { private lazy var socketController: SocketController = {
let path = (NSHomeDirectory() as NSString).appendingPathComponent("socket.ssh") as String let path = (NSHomeDirectory() as NSString).appendingPathComponent("socket.ssh") as String
return SocketController(path: path) return SocketController(path: path)
}() }()
fileprivate var updateSink: AnyCancellable? private var updateSink: AnyCancellable?
func applicationDidFinishLaunching(_ aNotification: Notification) { func applicationDidFinishLaunching(_ aNotification: Notification) {
os_log(.debug, "SecretAgent finished launching") Logger().debug("SecretAgent finished launching")
DispatchQueue.main.async { DispatchQueue.main.async {
self.socketController.handler = self.agent.handle(reader:writer:) self.socketController.handler = self.agent.handle(reader:writer:)
} }

View File

@ -31,13 +31,13 @@
"size" : "128x128" "size" : "128x128"
}, },
{ {
"filename" : "Icon 2@1x.png", "filename" : "Mac Icon.png",
"idiom" : "mac", "idiom" : "mac",
"scale" : "1x", "scale" : "1x",
"size" : "256x256" "size" : "256x256"
}, },
{ {
"filename" : "Icon 2@2x.png", "filename" : "Mac Icon@0.25x.png",
"idiom" : "mac", "idiom" : "mac",
"scale" : "2x", "scale" : "2x",
"size" : "256x256" "size" : "256x256"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -0,0 +1,31 @@
<?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>ApplicationDescription</key>
<string>Secretive is an app for storing and managing SSH keys in the Secure Enclave. SecretAgent is a helper process that runs in the background to sign requests, so that you don&apos;t always have to keep the main Secretive app open.</string>
<key>DeveloperName</key>
<string>Max Goedjen</string>
<key>Website</key>
<string>https://github.com/maxgoedjen/secretive</string>
<key>Connections</key>
<array>
<dict>
<key>IsIncoming</key>
<false/>
<key>Host</key>
<string>api.github.com</string>
<key>NetworkProtocol</key>
<string>TCP</string>
<key>Port</key>
<string>443</string>
<key>Purpose</key>
<string>Secretive checks GitHub for new versions and security updates.</string>
<key>DenyConsequences</key>
<string>If you deny these connections, you will not be notified about new versions and critical security updates.</string>
</dict>
</array>
<key>Services</key>
<array/>
</dict>
</plist>

View File

@ -7,29 +7,28 @@ import Brief
class Notifier { class Notifier {
fileprivate let notificationDelegate = NotificationDelegate() private let notificationDelegate = NotificationDelegate()
init() { init() {
let updateAction = UNNotificationAction(identifier: Constants.updateActionIdentitifier, title: "Update", options: []) let updateAction = UNNotificationAction(identifier: Constants.updateActionIdentitifier, title: "Update", options: [])
let ignoreAction = UNNotificationAction(identifier: Constants.ignoreActionIdentitifier, title: "Ignore", options: []) let ignoreAction = UNNotificationAction(identifier: Constants.ignoreActionIdentitifier, title: "Ignore", options: [])
let updateCategory = UNNotificationCategory(identifier: Constants.updateCategoryIdentitifier, actions: [updateAction, ignoreAction], intentIdentifiers: [], options: []) let updateCategory = UNNotificationCategory(identifier: Constants.updateCategoryIdentitifier, actions: [updateAction, ignoreAction], intentIdentifiers: [], options: [])
let criticalUpdateCategory = UNNotificationCategory(identifier: Constants.updateCategoryIdentitifier, actions: [updateAction], intentIdentifiers: [], options: []) let criticalUpdateCategory = UNNotificationCategory(identifier: Constants.criticalUpdateCategoryIdentitifier, actions: [updateAction], intentIdentifiers: [], options: [])
UNUserNotificationCenter.current().setNotificationCategories([updateCategory, criticalUpdateCategory]) UNUserNotificationCenter.current().setNotificationCategories([updateCategory, criticalUpdateCategory])
UNUserNotificationCenter.current().delegate = notificationDelegate UNUserNotificationCenter.current().delegate = notificationDelegate
} }
func prompt() { func prompt() {
let notificationCenter = UNUserNotificationCenter.current() let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.requestAuthorization(options: .alert) { _, _ in notificationCenter.requestAuthorization(options: .alert) { _, _ in }
}
} }
func notify(accessTo secret: AnySecret, by provenance: SigningRequestProvenance) { func notify(accessTo secret: AnySecret, by provenance: SigningRequestProvenance) {
let notificationCenter = UNUserNotificationCenter.current() let notificationCenter = UNUserNotificationCenter.current()
let notificationContent = UNMutableNotificationContent() let notificationContent = UNMutableNotificationContent()
notificationContent.title = "Signed Request from \(provenance.origin.name)" notificationContent.title = "Signed Request from \(provenance.origin.displayName)"
notificationContent.subtitle = "Using secret \"\(secret.name)\"" notificationContent.subtitle = "Using secret \"\(secret.name)\""
if let iconURL = iconURL(for: provenance), let attachment = try? UNNotificationAttachment(identifier: "icon", url: iconURL, options: nil) { if let iconURL = provenance.origin.iconURL, let attachment = try? UNNotificationAttachment(identifier: "icon", url: iconURL, options: nil) {
notificationContent.attachments = [attachment] notificationContent.attachments = [attachment]
} }
let request = UNNotificationRequest(identifier: UUID().uuidString, content: notificationContent, trigger: nil) let request = UNNotificationRequest(identifier: UUID().uuidString, content: notificationContent, trigger: nil)
@ -55,23 +54,6 @@ class Notifier {
} }
extension Notifier {
func iconURL(for provenance: SigningRequestProvenance) -> URL? {
do {
if let app = NSRunningApplication(processIdentifier: provenance.origin.pid), let icon = app.icon?.tiffRepresentation {
let temporaryURL = URL(fileURLWithPath: (NSTemporaryDirectory() as NSString).appendingPathComponent("\(UUID().uuidString).png"))
let bitmap = NSBitmapImageRep(data: icon)
try bitmap?.representation(using: .png, properties: [:])?.write(to: temporaryURL)
return temporaryURL
}
} catch {
}
return nil
}
}
extension Notifier: SigningWitness { extension Notifier: SigningWitness {
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, by provenance: SigningRequestProvenance) throws { func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, by provenance: SigningRequestProvenance) throws {
@ -117,7 +99,7 @@ class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
} }
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler(.alert) completionHandler([.list, .banner])
} }
} }

View File

@ -6,13 +6,13 @@ import AppKit
public class Agent { public class Agent {
fileprivate let storeList: SecretStoreList private let storeList: SecretStoreList
fileprivate let witness: SigningWitness? private let witness: SigningWitness?
fileprivate let writer = OpenSSHKeyWriter() private let writer = OpenSSHKeyWriter()
fileprivate let requestTracer = SigningRequestTracer() private let requestTracer = SigningRequestTracer()
public init(storeList: SecretStoreList, witness: SigningWitness? = nil) { public init(storeList: SecretStoreList, witness: SigningWitness? = nil) {
os_log(.debug, "Agent is running") Logger().debug("Agent is running")
self.storeList = storeList self.storeList = storeList
self.witness = witness self.witness = witness
} }
@ -22,16 +22,16 @@ public class Agent {
extension Agent { extension Agent {
public func handle(reader: FileHandleReader, writer: FileHandleWriter) { public func handle(reader: FileHandleReader, writer: FileHandleWriter) {
os_log(.debug, "Agent handling new data") Logger().debug("Agent handling new data")
let data = reader.availableData let data = reader.availableData
guard !data.isEmpty else { return } guard !data.isEmpty else { return }
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))
os_log(.debug, "Agent returned %@", SSHAgent.ResponseType.agentFailure.debugDescription) Logger().debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)")
return return
} }
os_log(.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)
@ -44,17 +44,17 @@ extension Agent {
case .requestIdentities: case .requestIdentities:
response.append(SSHAgent.ResponseType.agentIdentitiesAnswer.data) response.append(SSHAgent.ResponseType.agentIdentitiesAnswer.data)
response.append(identities()) response.append(identities())
os_log(.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))
os_log(.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)
os_log(.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
@ -65,9 +65,7 @@ extension Agent {
extension Agent { extension Agent {
func identities() -> Data { func identities() -> Data {
// TODO: RESTORE ONCE XCODE 11.4 IS GM let secrets = storeList.stores.flatMap(\.secrets)
let secrets = storeList.stores.flatMap { $0.secrets }
// let secrets = storeList.stores.flatMap(\.secrets)
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()
@ -78,7 +76,7 @@ extension Agent {
let curveData = writer.curveType(for: secret.algorithm, length: secret.keySize).data(using: .utf8)! 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))
} }
os_log(.debug, "Agent enumerated %@ identities", secrets.count as NSNumber) Logger().debug("Agent enumerated \(secrets.count) identities")
return countData + keyData return countData + keyData
} }
@ -86,7 +84,7 @@ extension Agent {
let reader = OpenSSHReader(data: data) let reader = OpenSSHReader(data: data)
let hash = reader.readNextChunk() let hash = reader.readNextChunk()
guard let (store, secret) = secret(matching: hash) else { guard let (store, secret) = secret(matching: hash) else {
os_log(.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
} }
@ -95,7 +93,7 @@ extension Agent {
} }
let dataToSign = reader.readNextChunk() let dataToSign = reader.readNextChunk()
let derSignature = try store.sign(data: dataToSign, with: secret) let derSignature = try store.sign(data: dataToSign, with: secret, for: provenance)
let curveData = writer.curveType(for: secret.algorithm, length: secret.keySize).data(using: .utf8)! let curveData = writer.curveType(for: secret.algorithm, length: secret.keySize).data(using: .utf8)!
@ -139,7 +137,7 @@ extension Agent {
try witness.witness(accessTo: secret, by: provenance) try witness.witness(accessTo: secret, by: provenance)
} }
os_log(.debug, "Agent signed request") Logger().debug("Agent signed request")
return signedData return signedData
} }

View File

@ -1,8 +1,12 @@
import Foundation import Foundation
import AppKit import AppKit
import Security import Security
import SecretKit
struct SigningRequestTracer { struct SigningRequestTracer {
}
extension SigningRequestTracer {
func provenance(from fileHandleReader: FileHandleReader) -> SigningRequestProvenance { func provenance(from fileHandleReader: FileHandleReader) -> SigningRequestProvenance {
let firstInfo = process(from: fileHandleReader.pidOfConnectedProcess) let firstInfo = process(from: fileHandleReader.pidOfConnectedProcess)
@ -33,7 +37,24 @@ struct SigningRequestTracer {
let flags: SecCSFlags = [.considerExpiration, .enforceRevocationChecks] let flags: SecCSFlags = [.considerExpiration, .enforceRevocationChecks]
SecCodeCreateWithPID(pid, SecCSFlags(), &secCode) SecCodeCreateWithPID(pid, SecCSFlags(), &secCode)
let valid = SecCodeCheckValidity(secCode.takeRetainedValue(), flags, nil) == errSecSuccess let valid = SecCodeCheckValidity(secCode.takeRetainedValue(), flags, nil) == errSecSuccess
return SigningRequestProvenance.Process(pid: pid, name: procName, path: path, validSignature: valid, parentPID: ppid) return SigningRequestProvenance.Process(pid: pid, processName: procName, appName: appName(for: pid), iconURL: iconURL(for: pid), path: path, validSignature: valid, parentPID: ppid)
}
func iconURL(for pid: Int32) -> URL? {
do {
if let app = NSRunningApplication(processIdentifier: pid), let icon = app.icon?.tiffRepresentation {
let temporaryURL = URL(fileURLWithPath: (NSTemporaryDirectory() as NSString).appendingPathComponent("\(UUID().uuidString).png"))
let bitmap = NSBitmapImageRep(data: icon)
try bitmap?.representation(using: .png, properties: [:])?.write(to: temporaryURL)
return temporaryURL
}
} catch {
}
return nil
}
func appName(for pid: Int32) -> String? {
NSRunningApplication(processIdentifier: pid)?.localizedName
} }
} }

View File

@ -3,21 +3,21 @@ import OSLog
public class SocketController { public class SocketController {
fileprivate var fileHandle: FileHandle? private var fileHandle: FileHandle?
fileprivate var port: SocketPort? private var port: SocketPort?
public var handler: ((FileHandleReader, FileHandleWriter) -> Void)? public var handler: ((FileHandleReader, FileHandleWriter) -> Void)?
public init(path: String) { public init(path: String) {
os_log(.debug, "Socket controller setting up at %@", path) Logger().debug("Socket controller setting up at \(path)")
if let _ = try? FileManager.default.removeItem(atPath: path) { if let _ = try? FileManager.default.removeItem(atPath: path) {
os_log(.debug, "Socket controller removed existing socket") Logger().debug("Socket controller removed existing socket")
} }
let exists = FileManager.default.fileExists(atPath: path) let exists = FileManager.default.fileExists(atPath: path)
assert(!exists) assert(!exists)
os_log(.debug, "Socket controller path is clear") Logger().debug("Socket controller path is clear")
port = socketPort(at: path) port = socketPort(at: path)
configureSocket(at: path) configureSocket(at: path)
os_log(.debug, "Socket listening at %@", path) Logger().debug("Socket listening at \(path)")
} }
func configureSocket(at path: String) { func configureSocket(at path: String) {
@ -33,7 +33,7 @@ public class SocketController {
addr.sun_family = sa_family_t(AF_UNIX) addr.sun_family = sa_family_t(AF_UNIX)
var len: Int = 0 var len: Int = 0
_ = withUnsafeMutablePointer(to: &addr.sun_path.0) { pointer in withUnsafeMutablePointer(to: &addr.sun_path.0) { pointer in
path.withCString { cstring in path.withCString { cstring in
len = strlen(cstring) len = strlen(cstring)
strncpy(pointer, cstring, len) strncpy(pointer, cstring, len)
@ -42,7 +42,7 @@ public class SocketController {
addr.sun_len = UInt8(len+2) addr.sun_len = UInt8(len+2)
var data: Data! var data: Data!
_ = withUnsafePointer(to: &addr) { pointer in withUnsafePointer(to: &addr) { pointer in
data = Data(bytes: pointer, count: MemoryLayout<sockaddr_un>.size) data = Data(bytes: pointer, count: MemoryLayout<sockaddr_un>.size)
} }
@ -50,7 +50,7 @@ public class SocketController {
} }
@objc func handleConnectionAccept(notification: Notification) { @objc func handleConnectionAccept(notification: Notification) {
os_log(.debug, "Socket controller accepted connection") Logger().debug("Socket controller accepted connection")
guard let new = notification.userInfo?[NSFileHandleNotificationFileHandleItem] as? FileHandle else { return } guard let new = notification.userInfo?[NSFileHandleNotificationFileHandleItem] as? FileHandle else { return }
handler?(new, new) handler?(new, new)
new.waitForDataInBackgroundAndNotify() new.waitForDataInBackgroundAndNotify()
@ -58,9 +58,9 @@ public class SocketController {
} }
@objc func handleConnectionDataAvailable(notification: Notification) { @objc func handleConnectionDataAvailable(notification: Notification) {
os_log(.debug, "Socket controller has new data available") Logger().debug("Socket controller has new data available")
guard let new = notification.object as? FileHandle else { return } guard let new = notification.object as? FileHandle else { return }
os_log(.debug, "Socket controller received new file handle") Logger().debug("Socket controller received new file handle")
handler?(new, new) handler?(new, new)
} }

View File

@ -106,7 +106,7 @@ class AgentTests: XCTestCase {
let agent = Agent(storeList: list, witness: witness) let agent = Agent(storeList: list, witness: witness)
agent.handle(reader: stubReader, writer: stubWriter) agent.handle(reader: stubReader, writer: stubWriter)
XCTAssertEqual(witnessTrace, speakNowTrace) XCTAssertEqual(witnessTrace, speakNowTrace)
XCTAssertEqual(witnessTrace.origin.name, "Finder") XCTAssertEqual(witnessTrace.origin.displayName, "Finder")
XCTAssertEqual(witnessTrace.origin.validSignature, true) XCTAssertEqual(witnessTrace.origin.validSignature, true)
XCTAssertEqual(witnessTrace.origin.parentPID, 1) XCTAssertEqual(witnessTrace.origin.parentPID, 1)
} }

View File

@ -48,7 +48,7 @@ extension Stub {
print("Public Key OpenSSH: \(OpenSSHKeyWriter().openSSHString(secret: secret))") print("Public Key OpenSSH: \(OpenSSHKeyWriter().openSSHString(secret: secret))")
} }
public func sign(data: Data, with secret: Secret) throws -> Data { public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) throws -> Data {
guard !shouldThrow else { guard !shouldThrow else {
throw NSError() throw NSError()
} }

View File

@ -0,0 +1,7 @@
import Foundation
extension Bundle {
public var agentBundleID: String {(self.bundleIdentifier?.replacingOccurrences(of: "Host", with: "SecretAgent"))!}
public var hostBundleID: String {(self.bundleIdentifier?.replacingOccurrences(of: "SecretAgent", with: "Host"))!}
}

View File

@ -3,12 +3,12 @@ import Foundation
public struct AnySecret: Secret { public struct AnySecret: Secret {
let base: Any let base: Any
fileprivate let hashable: AnyHashable private let hashable: AnyHashable
fileprivate let _id: () -> AnyHashable private let _id: () -> AnyHashable
fileprivate let _name: () -> String private let _name: () -> String
fileprivate let _algorithm: () -> Algorithm private let _algorithm: () -> Algorithm
fileprivate let _keySize: () -> Int private let _keySize: () -> Int
fileprivate let _publicKey: () -> Data private let _publicKey: () -> Data
public init<T>(_ secret: T) where T: Secret { public init<T>(_ secret: T) where T: Secret {
if let secret = secret as? AnySecret { if let secret = secret as? AnySecret {

View File

@ -4,12 +4,12 @@ import Combine
public class AnySecretStore: SecretStore { public class AnySecretStore: SecretStore {
let base: Any let base: Any
fileprivate let _isAvailable: () -> Bool private let _isAvailable: () -> Bool
fileprivate let _id: () -> UUID private let _id: () -> UUID
fileprivate let _name: () -> String private let _name: () -> String
fileprivate let _secrets: () -> [AnySecret] private let _secrets: () -> [AnySecret]
fileprivate let _sign: (Data, AnySecret) throws -> Data private let _sign: (Data, AnySecret, SigningRequestProvenance) throws -> Data
fileprivate var sink: AnyCancellable? private var sink: AnyCancellable?
public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStore { public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStore {
base = secretStore base = secretStore
@ -17,7 +17,7 @@ public class AnySecretStore: SecretStore {
_name = { secretStore.name } _name = { secretStore.name }
_id = { secretStore.id } _id = { secretStore.id }
_secrets = { secretStore.secrets.map { AnySecret($0) } } _secrets = { secretStore.secrets.map { AnySecret($0) } }
_sign = { try secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType) } _sign = { try secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType, for: $2) }
sink = secretStore.objectWillChange.sink { _ in sink = secretStore.objectWillChange.sink { _ in
self.objectWillChange.send() self.objectWillChange.send()
} }
@ -39,20 +39,22 @@ public class AnySecretStore: SecretStore {
return _secrets() return _secrets()
} }
public func sign(data: Data, with secret: AnySecret) throws -> Data { public func sign(data: Data, with secret: AnySecret, for provenance: SigningRequestProvenance) throws -> Data {
try _sign(data, secret) try _sign(data, secret, provenance)
} }
} }
public class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable { public class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable {
fileprivate let _create: (String, Bool) throws -> Void private let _create: (String, Bool) throws -> Void
fileprivate let _delete: (AnySecret) throws -> Void private let _delete: (AnySecret) throws -> Void
private let _update: (AnySecret, String) throws -> Void
public init<SecretStoreType>(modifiable secretStore: SecretStoreType) where SecretStoreType: SecretStoreModifiable { public init<SecretStoreType>(modifiable secretStore: SecretStoreType) where SecretStoreType: SecretStoreModifiable {
_create = { try secretStore.create(name: $0, requiresAuthentication: $1) } _create = { try secretStore.create(name: $0, requiresAuthentication: $1) }
_delete = { try secretStore.delete(secret: $0.base as! SecretStoreType.SecretType) } _delete = { try secretStore.delete(secret: $0.base as! SecretStoreType.SecretType) }
_update = { try secretStore.update(secret: $0.base as! SecretStoreType.SecretType, name: $1) }
super.init(secretStore) super.init(secretStore)
} }
@ -64,4 +66,7 @@ public class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable {
try _delete(secret) try _delete(secret)
} }
public func update(secret: AnySecret, name: String) throws {
try _update(secret, name)
}
} }

View File

@ -13,11 +13,21 @@ public struct OpenSSHKeyWriter {
lengthAndData(of: secret.publicKey) lengthAndData(of: secret.publicKey)
} }
public func openSSHString<SecretType: Secret>(secret: SecretType) -> String { public func openSSHString<SecretType: Secret>(secret: SecretType, comment: String? = nil) -> String {
"\(curveType(for: secret.algorithm, length: secret.keySize)) \(data(secret: secret).base64EncodedString())" [curveType(for: secret.algorithm, length: secret.keySize), data(secret: secret).base64EncodedString(), comment]
.compactMap { $0 }
.joined(separator: " ")
} }
public func openSSHFingerprint<SecretType: Secret>(secret: SecretType) -> String { public func openSSHSHA256Fingerprint<SecretType: Secret>(secret: SecretType) -> String {
// OpenSSL format seems to strip the padding at the end.
let base64 = Data(SHA256.hash(data: data(secret: secret))).base64EncodedString()
let paddingRange = base64.index(base64.endIndex, offsetBy: -2)..<base64.endIndex
let cleaned = base64.replacingOccurrences(of: "=", with: "", range: paddingRange)
return "SHA256:\(cleaned)"
}
public func openSSHMD5Fingerprint<SecretType: Secret>(secret: SecretType) -> String {
Insecure.MD5.hash(data: data(secret: secret)) Insecure.MD5.hash(data: data(secret: secret))
.compactMap { ("0" + String($0, radix: 16, uppercase: false)).suffix(2) } .compactMap { ("0" + String($0, radix: 16, uppercase: false)).suffix(2) }
.joined(separator: ":") .joined(separator: ":")

View File

@ -5,7 +5,7 @@ public class SecretStoreList: ObservableObject {
@Published public var stores: [AnySecretStore] = [] @Published public var stores: [AnySecretStore] = []
@Published public var modifiableStore: AnySecretStoreModifiable? @Published public var modifiableStore: AnySecretStoreModifiable?
fileprivate var sinks: [AnyCancellable] = [] private var sinks: [AnyCancellable] = []
public init() { public init() {
} }
@ -28,7 +28,7 @@ public class SecretStoreList: ObservableObject {
extension SecretStoreList { extension SecretStoreList {
fileprivate func addInternal(store: AnySecretStore) { private func addInternal(store: AnySecretStore) {
stores.append(store) stores.append(store)
let sink = store.objectWillChange.sink { let sink = store.objectWillChange.sink {
self.objectWillChange.send() self.objectWillChange.send()

View File

@ -9,7 +9,7 @@ public protocol SecretStore: ObservableObject, Identifiable {
var name: String { get } var name: String { get }
var secrets: [SecretType] { get } var secrets: [SecretType] { get }
func sign(data: Data, with secret: SecretType) throws -> Data func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data
} }
@ -17,6 +17,7 @@ public protocol SecretStoreModifiable: SecretStore {
func create(name: String, requiresAuthentication: Bool) throws func create(name: String, requiresAuthentication: Bool) throws
func delete(secret: SecretType) throws func delete(secret: SecretType) throws
func update(secret: SecretType, name: String) throws
} }

View File

@ -17,7 +17,7 @@ extension SigningRequestProvenance {
} }
public var intact: Bool { public var intact: Bool {
return chain.reduce(true) { $0 && $1.validSignature } chain.allSatisfy { $0.validSignature }
} }
} }
@ -27,19 +27,27 @@ extension SigningRequestProvenance {
public struct Process: Equatable { public struct Process: Equatable {
public let pid: Int32 public let pid: Int32
public let name: String public let processName: String
public let appName: String?
public let iconURL: URL?
public let path: String public let path: String
public let validSignature: Bool public let validSignature: Bool
let parentPID: Int32? public let parentPID: Int32?
init(pid: Int32, name: String, path: String, validSignature: Bool, parentPID: Int32?) { public init(pid: Int32, processName: String, appName: String?, iconURL: URL?, path: String, validSignature: Bool, parentPID: Int32?) {
self.pid = pid self.pid = pid
self.name = name self.processName = processName
self.appName = appName
self.iconURL = iconURL
self.path = path self.path = path
self.validSignature = validSignature self.validSignature = validSignature
self.parentPID = parentPID self.parentPID = parentPID
} }
public var displayName: String {
appName ?? processName
}
} }
} }

View File

@ -1,6 +1,7 @@
import Foundation import Foundation
import Security import Security
import CryptoTokenKit import CryptoTokenKit
import LocalAuthentication
extension SecureEnclave { extension SecureEnclave {
@ -14,7 +15,7 @@ extension SecureEnclave {
} }
public let id = UUID() public let id = UUID()
public let name = NSLocalizedString("Secure Enclave", comment: "Secure Enclave") public let name = NSLocalizedString("Secure Enclave", comment: "Secure Enclave")
@Published public fileprivate(set) var secrets: [Secret] = [] @Published public private(set) var secrets: [Secret] = []
public init() { public init() {
DistributedNotificationCenter.default().addObserver(forName: .secretStoreUpdated, object: nil, queue: .main) { _ in DistributedNotificationCenter.default().addObserver(forName: .secretStoreUpdated, object: nil, queue: .main) { _ in
@ -67,7 +68,7 @@ extension SecureEnclave {
let deleteAttributes = [ let deleteAttributes = [
kSecClass: kSecClassKey, kSecClass: kSecClassKey,
kSecAttrApplicationLabel: secret.id as CFData kSecAttrApplicationLabel: secret.id as CFData
] as CFDictionary ] as CFDictionary
let status = SecItemDelete(deleteAttributes) let status = SecItemDelete(deleteAttributes)
if status != errSecSuccess { if status != errSecSuccess {
throw KeychainError(statusCode: status) throw KeychainError(statusCode: status)
@ -75,7 +76,27 @@ extension SecureEnclave {
reloadSecrets() reloadSecrets()
} }
public func sign(data: Data, with secret: SecretType) throws -> Data { public func update(secret: Secret, name: String) throws {
let updateQuery = [
kSecClass: kSecClassKey,
kSecAttrApplicationLabel: secret.id as CFData
] as CFDictionary
let updatedAttributes = [
kSecAttrLabel: name,
] as CFDictionary
let status = SecItemUpdate(updateQuery, updatedAttributes)
if status != errSecSuccess {
throw KeychainError(statusCode: status)
}
reloadSecrets()
}
public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data {
let context = LAContext()
context.localizedReason = "sign a request from \"\(provenance.origin.displayName)\" using secret \"\(secret.name)\""
context.localizedCancelTitle = "Deny"
let attributes = [ let attributes = [
kSecClass: kSecClassKey, kSecClass: kSecClassKey,
kSecAttrKeyClass: kSecAttrKeyClassPrivate, kSecAttrKeyClass: kSecAttrKeyClassPrivate,
@ -83,6 +104,7 @@ extension SecureEnclave {
kSecAttrKeyType: Constants.keyType, kSecAttrKeyType: Constants.keyType,
kSecAttrTokenID: kSecAttrTokenIDSecureEnclave, kSecAttrTokenID: kSecAttrTokenIDSecureEnclave,
kSecAttrApplicationTag: Constants.keyTag, kSecAttrApplicationTag: Constants.keyTag,
kSecUseAuthenticationContext: context,
kSecReturnRef: true kSecReturnRef: true
] as CFDictionary ] as CFDictionary
var untyped: CFTypeRef? var untyped: CFTypeRef?
@ -107,7 +129,7 @@ extension SecureEnclave {
extension SecureEnclave.Store { extension SecureEnclave.Store {
fileprivate func reloadSecrets(notify: Bool = true) { private func reloadSecrets(notify: Bool = true) {
secrets.removeAll() secrets.removeAll()
loadSecrets() loadSecrets()
if notify { if notify {
@ -115,7 +137,7 @@ extension SecureEnclave.Store {
} }
} }
fileprivate func loadSecrets() { private func loadSecrets() {
let attributes = [ let attributes = [
kSecClass: kSecClassKey, kSecClass: kSecClassKey,
kSecAttrKeyType: SecureEnclave.Constants.keyType, kSecAttrKeyType: SecureEnclave.Constants.keyType,
@ -139,7 +161,7 @@ extension SecureEnclave.Store {
secrets.append(contentsOf: wrapped) secrets.append(contentsOf: wrapped)
} }
fileprivate func savePublicKey(_ publicKey: SecKey, name: String) throws { private func savePublicKey(_ publicKey: SecKey, name: String) throws {
let attributes = [ let attributes = [
kSecClass: kSecClassKey, kSecClass: kSecClassKey,
kSecAttrKeyType: SecureEnclave.Constants.keyType, kSecAttrKeyType: SecureEnclave.Constants.keyType,
@ -178,8 +200,8 @@ extension SecureEnclave {
extension SecureEnclave { extension SecureEnclave {
enum Constants { enum Constants {
fileprivate static let keyTag = "com.maxgoedjen.secretive.secureenclave.key".data(using: .utf8)! as CFData static let keyTag = "com.maxgoedjen.secretive.secureenclave.key".data(using: .utf8)! as CFData
fileprivate static let keyType = kSecAttrKeyTypeECSECPrimeRandom static let keyType = kSecAttrKeyTypeECSECPrimeRandom
} }
} }

View File

@ -1,6 +1,7 @@
import Foundation import Foundation
import Security import Security
import CryptoTokenKit import CryptoTokenKit
import LocalAuthentication
// TODO: Might need to split this up into "sub-stores?" // TODO: Might need to split this up into "sub-stores?"
// ie, each token has its own Store. // ie, each token has its own Store.
@ -11,10 +12,10 @@ extension SmartCard {
// TODO: Read actual smart card name, eg "YubiKey 5c" // TODO: Read actual smart card name, eg "YubiKey 5c"
@Published public var isAvailable: Bool = false @Published public var isAvailable: Bool = false
public let id = UUID() public let id = UUID()
public fileprivate(set) var name = NSLocalizedString("Smart Card", comment: "Smart Card") public private(set) var name = NSLocalizedString("Smart Card", comment: "Smart Card")
@Published public fileprivate(set) var secrets: [Secret] = [] @Published public private(set) var secrets: [Secret] = []
fileprivate let watcher = TKTokenWatcher() private let watcher = TKTokenWatcher()
fileprivate var tokenID: String? private var tokenID: String?
public init() { public init() {
tokenID = watcher.nonSecureEnclaveTokens.first tokenID = watcher.nonSecureEnclaveTokens.first
@ -43,13 +44,17 @@ extension SmartCard {
fatalError("Keys must be deleted on the smart card.") fatalError("Keys must be deleted on the smart card.")
} }
public func sign(data: Data, with secret: SecretType) throws -> Data { public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> Data {
guard let tokenID = tokenID else { fatalError() } guard let tokenID = tokenID else { fatalError() }
let context = LAContext()
context.localizedReason = "sign a request from \"\(provenance.origin.displayName)\" using secret \"\(secret.name)\""
context.localizedCancelTitle = "Deny"
let attributes = [ let attributes = [
kSecClass: kSecClassKey, kSecClass: kSecClassKey,
kSecAttrKeyClass: kSecAttrKeyClassPrivate, kSecAttrKeyClass: kSecAttrKeyClassPrivate,
kSecAttrApplicationLabel: secret.id as CFData, kSecAttrApplicationLabel: secret.id as CFData,
kSecAttrTokenID: tokenID, kSecAttrTokenID: tokenID,
kSecUseAuthenticationContext: context,
kSecReturnRef: true kSecReturnRef: true
] as CFDictionary ] as CFDictionary
var untyped: CFTypeRef? var untyped: CFTypeRef?
@ -83,12 +88,12 @@ extension SmartCard {
extension SmartCard.Store { extension SmartCard.Store {
fileprivate func smartcardRemoved(for tokenID: String? = nil) { private func smartcardRemoved(for tokenID: String? = nil) {
self.tokenID = nil self.tokenID = nil
reloadSecrets() reloadSecrets()
} }
fileprivate func reloadSecrets() { private func reloadSecrets() {
DispatchQueue.main.async { DispatchQueue.main.async {
self.isAvailable = self.tokenID != nil self.isAvailable = self.tokenID != nil
self.secrets.removeAll() self.secrets.removeAll()
@ -96,7 +101,7 @@ extension SmartCard.Store {
} }
} }
fileprivate func loadSecrets() { private func loadSecrets() {
guard let tokenID = tokenID else { return } guard let tokenID = tokenID else { return }
// Hack to read name if there's only one smart card // Hack to read name if there's only one smart card
let slotNames = TKSmartCardSlotManager().slotNames let slotNames = TKSmartCardSlotManager().slotNames

View File

@ -6,8 +6,12 @@ class OpenSSHWriterTests: XCTestCase {
let writer = OpenSSHKeyWriter() let writer = OpenSSHKeyWriter()
func testECDSA256Fingerprint() { func testECDSA256MD5Fingerprint() {
XCTAssertEqual(writer.openSSHFingerprint(secret: Constants.ecdsa256Secret), "dc:60:4d:ff:c2:d9:18:8b:2f:24:40:b5:7f:43:47:e5") XCTAssertEqual(writer.openSSHMD5Fingerprint(secret: Constants.ecdsa256Secret), "dc:60:4d:ff:c2:d9:18:8b:2f:24:40:b5:7f:43:47:e5")
}
func testECDSA256SHA256Fingerprint() {
XCTAssertEqual(writer.openSSHSHA256Fingerprint(secret: Constants.ecdsa256Secret), "SHA256:/VQFeGyM8qKA8rB6WGMuZZxZLJln2UgXLk3F0uTF650")
} }
func testECDSA256PublicKey() { func testECDSA256PublicKey() {
@ -19,8 +23,12 @@ class OpenSSHWriterTests: XCTestCase {
XCTAssertEqual(writer.data(secret: Constants.ecdsa256Secret), Data(base64Encoded: "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOVEjgAA5PHqRgwykjN5qM21uWCHFSY/Sqo5gkHAkn+e1MMQKHOLga7ucB9b3mif33MBid59GRK9GEPVlMiSQwo=")) XCTAssertEqual(writer.data(secret: Constants.ecdsa256Secret), Data(base64Encoded: "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOVEjgAA5PHqRgwykjN5qM21uWCHFSY/Sqo5gkHAkn+e1MMQKHOLga7ucB9b3mif33MBid59GRK9GEPVlMiSQwo="))
} }
func testECDSA384Fingerprint() { func testECDSA384MD5Fingerprint() {
XCTAssertEqual(writer.openSSHFingerprint(secret: Constants.ecdsa384Secret), "66:e0:66:d7:41:ed:19:8e:e2:20:df:ce:ac:7e:2b:6e") XCTAssertEqual(writer.openSSHMD5Fingerprint(secret: Constants.ecdsa384Secret), "66:e0:66:d7:41:ed:19:8e:e2:20:df:ce:ac:7e:2b:6e")
}
func testECDSA384SHA256Fingerprint() {
XCTAssertEqual(writer.openSSHSHA256Fingerprint(secret: Constants.ecdsa384Secret), "SHA256:GJUEymQNL9ymaMRRJCMGY4rWIJHu/Lm8Yhao/PAiz1I")
} }
func testECDSA384PublicKey() { func testECDSA384PublicKey() {

View File

@ -7,16 +7,19 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
2C4A9D2F2636FFD3008CC8E2 /* RenameSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4A9D2E2636FFD3008CC8E2 /* RenameSecretView.swift */; };
50020BB024064869003D4025 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50020BAF24064869003D4025 /* AppDelegate.swift */; }; 50020BB024064869003D4025 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50020BAF24064869003D4025 /* AppDelegate.swift */; };
50153E20250AFCB200525160 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E1F250AFCB200525160 /* UpdateView.swift */; };
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; };
5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; }; 5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; };
501B7AE1251C56F700776EC7 /* SigningRequestProvenance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507CE4F32420A8C10029F750 /* SigningRequestProvenance.swift */; };
50524B442420969E008DBD97 /* OpenSSHWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50524B432420969D008DBD97 /* OpenSSHWriterTests.swift */; }; 50524B442420969E008DBD97 /* OpenSSHWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50524B432420969D008DBD97 /* OpenSSHWriterTests.swift */; };
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */; }; 50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */; };
50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0424393D1500F76F6C /* LaunchAgentController.swift */; }; 50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0424393D1500F76F6C /* LaunchAgentController.swift */; };
50617D8323FCE48E0099B055 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* AppDelegate.swift */; }; 50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.swift */; };
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8423FCE48E0099B055 /* ContentView.swift */; }; 50617D8523FCE48E0099B055 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8423FCE48E0099B055 /* ContentView.swift */; };
50617D8723FCE48E0099B055 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8623FCE48E0099B055 /* Assets.xcassets */; }; 50617D8723FCE48E0099B055 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8623FCE48E0099B055 /* Assets.xcassets */; };
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 */; };
50617D8D23FCE48E0099B055 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8B23FCE48E0099B055 /* Main.storyboard */; };
50617D9923FCE48E0099B055 /* SecretiveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D9823FCE48E0099B055 /* SecretiveTests.swift */; }; 50617D9923FCE48E0099B055 /* SecretiveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D9823FCE48E0099B055 /* SecretiveTests.swift */; };
50617DB123FCE4AB0099B055 /* SecretKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50617DA823FCE4AB0099B055 /* SecretKit.framework */; }; 50617DB123FCE4AB0099B055 /* SecretKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50617DA823FCE4AB0099B055 /* SecretKit.framework */; };
50617DBA23FCE4AB0099B055 /* SecretKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 50617DAA23FCE4AB0099B055 /* SecretKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 50617DBA23FCE4AB0099B055 /* SecretKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 50617DAA23FCE4AB0099B055 /* SecretKit.h */; settings = {ATTRIBUTES = (Public, ); }; };
@ -28,6 +31,9 @@
50617DCE23FCECFA0099B055 /* SecureEnclaveSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DCD23FCECFA0099B055 /* SecureEnclaveSecret.swift */; }; 50617DCE23FCECFA0099B055 /* SecureEnclaveSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DCD23FCECFA0099B055 /* SecureEnclaveSecret.swift */; };
50617DD023FCED2C0099B055 /* SecureEnclave.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DCF23FCED2C0099B055 /* SecureEnclave.swift */; }; 50617DD023FCED2C0099B055 /* SecureEnclave.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DCF23FCED2C0099B055 /* SecureEnclave.swift */; };
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DD123FCEFA90099B055 /* PreviewStore.swift */; }; 50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DD123FCEFA90099B055 /* PreviewStore.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 */; };
506772C72424784600034DED /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 506772C62424784600034DED /* Credits.rtf */; }; 506772C72424784600034DED /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 506772C62424784600034DED /* Credits.rtf */; };
506772C92425BB8500034DED /* NoStoresView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506772C82425BB8500034DED /* NoStoresView.swift */; }; 506772C92425BB8500034DED /* NoStoresView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506772C82425BB8500034DED /* NoStoresView.swift */; };
506772FF2426F3F400034DED /* Brief.h in Headers */ = {isa = PBXBuildFile; fileRef = 506772FD2426F3F400034DED /* Brief.h */; settings = {ATTRIBUTES = (Public, ); }; }; 506772FF2426F3F400034DED /* Brief.h in Headers */ = {isa = PBXBuildFile; fileRef = 506772FD2426F3F400034DED /* Brief.h */; settings = {ATTRIBUTES = (Public, ); }; };
@ -41,11 +47,10 @@
506838A12415EA5600F55094 /* AnySecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506838A02415EA5600F55094 /* AnySecret.swift */; }; 506838A12415EA5600F55094 /* AnySecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506838A02415EA5600F55094 /* AnySecret.swift */; };
506838A32415EA5D00F55094 /* AnySecretStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506838A22415EA5D00F55094 /* AnySecretStore.swift */; }; 506838A32415EA5D00F55094 /* AnySecretStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506838A22415EA5D00F55094 /* AnySecretStore.swift */; };
506AB87E2412334700335D91 /* SecretAgent.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = 50A3B78A24026B7500D209EA /* SecretAgent.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 506AB87E2412334700335D91 /* SecretAgent.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = 50A3B78A24026B7500D209EA /* SecretAgent.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
50731669241E00C20023809E /* NoticeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50731668241E00C20023809E /* NoticeView.swift */; }; 5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */; };
507CE4ED2420A3C70029F750 /* Agent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A3B79F24026B9900D209EA /* Agent.swift */; }; 507CE4ED2420A3C70029F750 /* Agent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A3B79F24026B9900D209EA /* Agent.swift */; };
507CE4EE2420A3CA0029F750 /* SocketController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A3B79D24026B9900D209EA /* SocketController.swift */; }; 507CE4EE2420A3CA0029F750 /* SocketController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A3B79D24026B9900D209EA /* SocketController.swift */; };
507CE4F02420A4C50029F750 /* SigningWitness.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507CE4EF2420A4C50029F750 /* SigningWitness.swift */; }; 507CE4F02420A4C50029F750 /* SigningWitness.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507CE4EF2420A4C50029F750 /* SigningWitness.swift */; };
507CE4F42420A8C10029F750 /* SigningRequestProvenance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507CE4F32420A8C10029F750 /* SigningRequestProvenance.swift */; };
507CE4F62420A96F0029F750 /* SigningRequestTracer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507CE4F52420A96F0029F750 /* SigningRequestTracer.swift */; }; 507CE4F62420A96F0029F750 /* SigningRequestTracer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507CE4F52420A96F0029F750 /* SigningRequestTracer.swift */; };
507EE34224281E12003C4FE3 /* FileHandleProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507EE34124281E12003C4FE3 /* FileHandleProtocols.swift */; }; 507EE34224281E12003C4FE3 /* FileHandleProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507EE34124281E12003C4FE3 /* FileHandleProtocols.swift */; };
507EE34624281F89003C4FE3 /* StubFileHandleReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507EE34524281F89003C4FE3 /* StubFileHandleReader.swift */; }; 507EE34624281F89003C4FE3 /* StubFileHandleReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 507EE34524281F89003C4FE3 /* StubFileHandleReader.swift */; };
@ -57,6 +62,11 @@
508A58B5241ED48F0069DC07 /* PreviewAgentStatusChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508A58B4241ED48F0069DC07 /* PreviewAgentStatusChecker.swift */; }; 508A58B5241ED48F0069DC07 /* PreviewAgentStatusChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508A58B4241ED48F0069DC07 /* PreviewAgentStatusChecker.swift */; };
508A5911241EF09C0069DC07 /* SecretAgentKit.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 5099A06C240242BA0062B6F2 /* SecretAgentKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 508A5911241EF09C0069DC07 /* SecretAgentKit.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 5099A06C240242BA0062B6F2 /* SecretAgentKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
508A5913241EF0B20069DC07 /* SecretKit.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 50617DA823FCE4AB0099B055 /* SecretKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 508A5913241EF0B20069DC07 /* SecretKit.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 50617DA823FCE4AB0099B055 /* SecretKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
508BF28E25B4F005009EFB7E /* InternetAccessPolicy.plist in Resources */ = {isa = PBXBuildFile; fileRef = 508BF28D25B4F005009EFB7E /* InternetAccessPolicy.plist */; };
508BF2AA25B4F1CB009EFB7E /* InternetAccessPolicy.plist in Resources */ = {isa = PBXBuildFile; fileRef = 508BF29425B4F140009EFB7E /* InternetAccessPolicy.plist */; };
5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */; };
5091D3222519D56D0049FD9B /* SemVerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5091D3212519D56D0049FD9B /* SemVerTests.swift */; };
5091D3242519D56D0049FD9B /* Brief.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 506772FB2426F3F400034DED /* Brief.framework */; };
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */; }; 5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */; };
5099A02723FE34FA0062B6F2 /* SmartCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02623FE34FA0062B6F2 /* SmartCard.swift */; }; 5099A02723FE34FA0062B6F2 /* SmartCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02623FE34FA0062B6F2 /* SmartCard.swift */; };
5099A02923FE35240062B6F2 /* SmartCardStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02823FE35240062B6F2 /* SmartCardStore.swift */; }; 5099A02923FE35240062B6F2 /* SmartCardStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02823FE35240062B6F2 /* SmartCardStore.swift */; };
@ -66,6 +76,7 @@
5099A07C240242BA0062B6F2 /* AgentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A07B240242BA0062B6F2 /* AgentTests.swift */; }; 5099A07C240242BA0062B6F2 /* AgentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A07B240242BA0062B6F2 /* AgentTests.swift */; };
5099A07E240242BA0062B6F2 /* SecretAgentKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 5099A06E240242BA0062B6F2 /* SecretAgentKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 5099A07E240242BA0062B6F2 /* SecretAgentKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 5099A06E240242BA0062B6F2 /* SecretAgentKit.h */; settings = {ATTRIBUTES = (Public, ); }; };
5099A08A240242C20062B6F2 /* SSHAgentProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A089240242C20062B6F2 /* SSHAgentProtocol.swift */; }; 5099A08A240242C20062B6F2 /* SSHAgentProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A089240242C20062B6F2 /* SSHAgentProtocol.swift */; };
509FA3B625B53C49005E2535 /* ReleaseParsingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 509FA3B525B53C49005E2535 /* ReleaseParsingTests.swift */; };
50A3B79124026B7600D209EA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50A3B79024026B7600D209EA /* Assets.xcassets */; }; 50A3B79124026B7600D209EA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50A3B79024026B7600D209EA /* Assets.xcassets */; };
50A3B79424026B7600D209EA /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50A3B79324026B7600D209EA /* Preview Assets.xcassets */; }; 50A3B79424026B7600D209EA /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50A3B79324026B7600D209EA /* Preview Assets.xcassets */; };
50A3B79724026B7600D209EA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50A3B79524026B7600D209EA /* Main.storyboard */; }; 50A3B79724026B7600D209EA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50A3B79524026B7600D209EA /* Main.storyboard */; };
@ -77,7 +88,7 @@
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */; }; 50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */; };
50C385A3240789E600AF2719 /* OpenSSHReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A2240789E600AF2719 /* OpenSSHReader.swift */; }; 50C385A3240789E600AF2719 /* OpenSSHReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A2240789E600AF2719 /* OpenSSHReader.swift */; };
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A42407A76D00AF2719 /* SecretDetailView.swift */; }; 50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A42407A76D00AF2719 /* SecretDetailView.swift */; };
50C385A9240B636500AF2719 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A8240B636500AF2719 /* SetupView.swift */; }; FA0B34672599619E0013AB3A /* BundleIDs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA0B34662599619E0013AB3A /* BundleIDs.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -137,6 +148,13 @@
remoteGlobalIDString = 50617DA723FCE4AB0099B055; remoteGlobalIDString = 50617DA723FCE4AB0099B055;
remoteInfo = SecretKit; remoteInfo = SecretKit;
}; };
5091D3252519D56D0049FD9B /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 50617D7723FCE48D0099B055 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 506772FA2426F3F400034DED;
remoteInfo = Brief;
};
5099A076240242BA0062B6F2 /* PBXContainerItemProxy */ = { 5099A076240242BA0062B6F2 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy; isa = PBXContainerItemProxy;
containerPortal = 50617D7723FCE48D0099B055 /* Project object */; containerPortal = 50617D7723FCE48D0099B055 /* Project object */;
@ -205,17 +223,19 @@
/* End PBXCopyFilesBuildPhase section */ /* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
2C4A9D2E2636FFD3008CC8E2 /* RenameSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenameSecretView.swift; sourceTree = "<group>"; };
50020BAF24064869003D4025 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 50020BAF24064869003D4025 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
50153E1F250AFCB200525160 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = "<group>"; };
50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = "<group>"; };
5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; }; 5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; };
50524B432420969D008DBD97 /* OpenSSHWriterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenSSHWriterTests.swift; sourceTree = "<group>"; }; 50524B432420969D008DBD97 /* OpenSSHWriterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenSSHWriterTests.swift; sourceTree = "<group>"; };
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustUpdatedChecker.swift; sourceTree = "<group>"; }; 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustUpdatedChecker.swift; sourceTree = "<group>"; };
50571E0424393D1500F76F6C /* LaunchAgentController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAgentController.swift; sourceTree = "<group>"; }; 50571E0424393D1500F76F6C /* LaunchAgentController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAgentController.swift; sourceTree = "<group>"; };
50617D7F23FCE48E0099B055 /* Secretive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Secretive.app; sourceTree = BUILT_PRODUCTS_DIR; }; 50617D7F23FCE48E0099B055 /* Secretive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Secretive.app; sourceTree = BUILT_PRODUCTS_DIR; };
50617D8223FCE48E0099B055 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 50617D8223FCE48E0099B055 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
50617D8423FCE48E0099B055 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; 50617D8423FCE48E0099B055 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
50617D8623FCE48E0099B055 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 50617D8623FCE48E0099B055 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
50617D8923FCE48E0099B055 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; }; 50617D8923FCE48E0099B055 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
50617D8C23FCE48E0099B055 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
50617D8E23FCE48E0099B055 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 50617D8E23FCE48E0099B055 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
50617D8F23FCE48E0099B055 /* Secretive.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Secretive.entitlements; sourceTree = "<group>"; }; 50617D8F23FCE48E0099B055 /* Secretive.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Secretive.entitlements; sourceTree = "<group>"; };
50617D9423FCE48E0099B055 /* SecretiveTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SecretiveTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 50617D9423FCE48E0099B055 /* SecretiveTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SecretiveTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@ -232,6 +252,9 @@
50617DCD23FCECFA0099B055 /* SecureEnclaveSecret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureEnclaveSecret.swift; sourceTree = "<group>"; }; 50617DCD23FCECFA0099B055 /* SecureEnclaveSecret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureEnclaveSecret.swift; sourceTree = "<group>"; };
50617DCF23FCED2C0099B055 /* SecureEnclave.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureEnclave.swift; sourceTree = "<group>"; }; 50617DCF23FCED2C0099B055 /* SecureEnclave.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureEnclave.swift; 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>"; };
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>"; };
506772C62424784600034DED /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = "<group>"; }; 506772C62424784600034DED /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = "<group>"; };
506772C82425BB8500034DED /* NoStoresView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoStoresView.swift; sourceTree = "<group>"; }; 506772C82425BB8500034DED /* NoStoresView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoStoresView.swift; sourceTree = "<group>"; };
506772FB2426F3F400034DED /* Brief.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Brief.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 506772FB2426F3F400034DED /* Brief.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Brief.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -243,7 +266,7 @@
5068389D241471CD00F55094 /* SecretStoreList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretStoreList.swift; sourceTree = "<group>"; }; 5068389D241471CD00F55094 /* SecretStoreList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretStoreList.swift; sourceTree = "<group>"; };
506838A02415EA5600F55094 /* AnySecret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnySecret.swift; sourceTree = "<group>"; }; 506838A02415EA5600F55094 /* AnySecret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnySecret.swift; sourceTree = "<group>"; };
506838A22415EA5D00F55094 /* AnySecretStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnySecretStore.swift; sourceTree = "<group>"; }; 506838A22415EA5D00F55094 /* AnySecretStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnySecretStore.swift; sourceTree = "<group>"; };
50731668241E00C20023809E /* NoticeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeView.swift; sourceTree = "<group>"; }; 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreListView.swift; sourceTree = "<group>"; };
507CE4EF2420A4C50029F750 /* SigningWitness.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SigningWitness.swift; sourceTree = "<group>"; }; 507CE4EF2420A4C50029F750 /* SigningWitness.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SigningWitness.swift; sourceTree = "<group>"; };
507CE4F32420A8C10029F750 /* SigningRequestProvenance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SigningRequestProvenance.swift; sourceTree = "<group>"; }; 507CE4F32420A8C10029F750 /* SigningRequestProvenance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SigningRequestProvenance.swift; sourceTree = "<group>"; };
507CE4F52420A96F0029F750 /* SigningRequestTracer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SigningRequestTracer.swift; sourceTree = "<group>"; }; 507CE4F52420A96F0029F750 /* SigningRequestTracer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SigningRequestTracer.swift; sourceTree = "<group>"; };
@ -257,6 +280,12 @@
508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentStatusChecker.swift; sourceTree = "<group>"; }; 508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentStatusChecker.swift; sourceTree = "<group>"; };
508A58B4241ED48F0069DC07 /* PreviewAgentStatusChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewAgentStatusChecker.swift; sourceTree = "<group>"; }; 508A58B4241ED48F0069DC07 /* PreviewAgentStatusChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewAgentStatusChecker.swift; sourceTree = "<group>"; };
508A590F241EEF6D0069DC07 /* Secretive.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = Secretive.xctestplan; sourceTree = "<group>"; }; 508A590F241EEF6D0069DC07 /* Secretive.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = Secretive.xctestplan; sourceTree = "<group>"; };
508BF28D25B4F005009EFB7E /* InternetAccessPolicy.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = InternetAccessPolicy.plist; sourceTree = "<group>"; };
508BF29425B4F140009EFB7E /* InternetAccessPolicy.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; name = InternetAccessPolicy.plist; path = SecretAgent/InternetAccessPolicy.plist; sourceTree = SOURCE_ROOT; };
5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationDirectoryController.swift; sourceTree = "<group>"; };
5091D31F2519D56D0049FD9B /* BriefTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BriefTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
5091D3212519D56D0049FD9B /* SemVerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemVerTests.swift; sourceTree = "<group>"; };
5091D3232519D56D0049FD9B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSecretView.swift; sourceTree = "<group>"; }; 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSecretView.swift; sourceTree = "<group>"; };
5099A02623FE34FA0062B6F2 /* SmartCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartCard.swift; sourceTree = "<group>"; }; 5099A02623FE34FA0062B6F2 /* SmartCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartCard.swift; sourceTree = "<group>"; };
5099A02823FE35240062B6F2 /* SmartCardStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartCardStore.swift; sourceTree = "<group>"; }; 5099A02823FE35240062B6F2 /* SmartCardStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartCardStore.swift; sourceTree = "<group>"; };
@ -269,6 +298,7 @@
5099A07B240242BA0062B6F2 /* AgentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentTests.swift; sourceTree = "<group>"; }; 5099A07B240242BA0062B6F2 /* AgentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentTests.swift; sourceTree = "<group>"; };
5099A07D240242BA0062B6F2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 5099A07D240242BA0062B6F2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
5099A089240242C20062B6F2 /* SSHAgentProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSHAgentProtocol.swift; sourceTree = "<group>"; }; 5099A089240242C20062B6F2 /* SSHAgentProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSHAgentProtocol.swift; sourceTree = "<group>"; };
509FA3B525B53C49005E2535 /* ReleaseParsingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReleaseParsingTests.swift; sourceTree = "<group>"; };
50A3B78A24026B7500D209EA /* SecretAgent.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SecretAgent.app; sourceTree = BUILT_PRODUCTS_DIR; }; 50A3B78A24026B7500D209EA /* SecretAgent.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SecretAgent.app; sourceTree = BUILT_PRODUCTS_DIR; };
50A3B79024026B7600D209EA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 50A3B79024026B7600D209EA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
50A3B79324026B7600D209EA /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; }; 50A3B79324026B7600D209EA /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
@ -281,7 +311,7 @@
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStoreView.swift; sourceTree = "<group>"; }; 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStoreView.swift; sourceTree = "<group>"; };
50C385A2240789E600AF2719 /* OpenSSHReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OpenSSHReader.swift; path = SecretKit/Common/OpenSSH/OpenSSHReader.swift; sourceTree = SOURCE_ROOT; }; 50C385A2240789E600AF2719 /* OpenSSHReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OpenSSHReader.swift; path = SecretKit/Common/OpenSSH/OpenSSHReader.swift; sourceTree = SOURCE_ROOT; };
50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = "<group>"; }; 50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = "<group>"; };
50C385A8240B636500AF2719 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = "<group>"; }; FA0B34662599619E0013AB3A /* BundleIDs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIDs.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -323,6 +353,14 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
5091D31C2519D56D0049FD9B /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
5091D3242519D56D0049FD9B /* Brief.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
5099A069240242BA0062B6F2 /* Frameworks */ = { 5099A069240242BA0062B6F2 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@ -353,6 +391,7 @@
504BA92D243171F20064740E /* Types */ = { 504BA92D243171F20064740E /* Types */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
507CE4F32420A8C10029F750 /* SigningRequestProvenance.swift */,
50617DCA23FCECA10099B055 /* Secret.swift */, 50617DCA23FCECA10099B055 /* Secret.swift */,
50617DC623FCE4EA0099B055 /* SecretStore.swift */, 50617DC623FCE4EA0099B055 /* SecretStore.swift */,
); );
@ -371,6 +410,7 @@
5099A07A240242BA0062B6F2 /* SecretAgentKitTests */, 5099A07A240242BA0062B6F2 /* SecretAgentKitTests */,
508A58AF241E144C0069DC07 /* Config */, 508A58AF241E144C0069DC07 /* Config */,
506772FC2426F3F400034DED /* Brief */, 506772FC2426F3F400034DED /* Brief */,
5091D3202519D56D0049FD9B /* BriefTests */,
50617D8023FCE48E0099B055 /* Products */, 50617D8023FCE48E0099B055 /* Products */,
5099A08B240243730062B6F2 /* Frameworks */, 5099A08B240243730062B6F2 /* Frameworks */,
); );
@ -387,6 +427,7 @@
5099A074240242BA0062B6F2 /* SecretAgentKitTests.xctest */, 5099A074240242BA0062B6F2 /* SecretAgentKitTests.xctest */,
50A3B78A24026B7500D209EA /* SecretAgent.app */, 50A3B78A24026B7500D209EA /* SecretAgent.app */,
506772FB2426F3F400034DED /* Brief.framework */, 506772FB2426F3F400034DED /* Brief.framework */,
5091D31F2519D56D0049FD9B /* BriefTests.xctest */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
@ -394,12 +435,12 @@
50617D8123FCE48E0099B055 /* Secretive */ = { 50617D8123FCE48E0099B055 /* Secretive */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
50617D8223FCE48E0099B055 /* AppDelegate.swift */, 50617D8223FCE48E0099B055 /* App.swift */,
508A58B0241ED1C40069DC07 /* Views */, 508A58B0241ED1C40069DC07 /* Views */,
508A58B1241ED1EA0069DC07 /* Controllers */, 508A58B1241ED1EA0069DC07 /* Controllers */,
50617D8623FCE48E0099B055 /* Assets.xcassets */, 50617D8623FCE48E0099B055 /* Assets.xcassets */,
50617D8B23FCE48E0099B055 /* Main.storyboard */,
50617D8E23FCE48E0099B055 /* Info.plist */, 50617D8E23FCE48E0099B055 /* Info.plist */,
508BF28D25B4F005009EFB7E /* InternetAccessPolicy.plist */,
50617D8F23FCE48E0099B055 /* Secretive.entitlements */, 50617D8F23FCE48E0099B055 /* Secretive.entitlements */,
506772C62424784600034DED /* Credits.rtf */, 506772C62424784600034DED /* Credits.rtf */,
50617D8823FCE48E0099B055 /* Preview Content */, 50617D8823FCE48E0099B055 /* Preview Content */,
@ -501,13 +542,17 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
50617D8423FCE48E0099B055 /* ContentView.swift */, 50617D8423FCE48E0099B055 /* ContentView.swift */,
50731668241E00C20023809E /* NoticeView.swift */, 5079BA0E250F29BF00EA86F4 /* StoreListView.swift */,
50153E21250DECA300525160 /* SecretListItemView.swift */,
50C385A42407A76D00AF2719 /* SecretDetailView.swift */, 50C385A42407A76D00AF2719 /* SecretDetailView.swift */,
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */, 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */,
50B8550C24138C4F009958AC /* DeleteSecretView.swift */, 50B8550C24138C4F009958AC /* DeleteSecretView.swift */,
2C4A9D2E2636FFD3008CC8E2 /* RenameSecretView.swift */,
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */, 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */,
506772C82425BB8500034DED /* NoStoresView.swift */, 506772C82425BB8500034DED /* NoStoresView.swift */,
50C385A8240B636500AF2719 /* SetupView.swift */, 50153E1F250AFCB200525160 /* UpdateView.swift */,
5066A6C12516F303004B5A36 /* SetupView.swift */,
5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
); );
path = Views; path = Views;
sourceTree = "<group>"; sourceTree = "<group>";
@ -516,12 +561,24 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */, 508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */,
5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */,
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */, 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */,
50571E0424393D1500F76F6C /* LaunchAgentController.swift */, 50571E0424393D1500F76F6C /* LaunchAgentController.swift */,
5066A6F6251829B1004B5A36 /* ShellConfigurationController.swift */,
); );
path = Controllers; path = Controllers;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
5091D3202519D56D0049FD9B /* BriefTests */ = {
isa = PBXGroup;
children = (
5091D3212519D56D0049FD9B /* SemVerTests.swift */,
509FA3B525B53C49005E2535 /* ReleaseParsingTests.swift */,
5091D3232519D56D0049FD9B /* Info.plist */,
);
path = BriefTests;
sourceTree = "<group>";
};
5099A02523FE34DE0062B6F2 /* SmartCard */ = { 5099A02523FE34DE0062B6F2 /* SmartCard */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -539,6 +596,7 @@
5068389F2415EA4F00F55094 /* Erasers */, 5068389F2415EA4F00F55094 /* Erasers */,
506838A42415EA6800F55094 /* OpenSSH */, 506838A42415EA6800F55094 /* OpenSSH */,
5068389D241471CD00F55094 /* SecretStoreList.swift */, 5068389D241471CD00F55094 /* SecretStoreList.swift */,
FA0B34662599619E0013AB3A /* BundleIDs.swift */,
); );
path = Common; path = Common;
sourceTree = "<group>"; sourceTree = "<group>";
@ -550,7 +608,6 @@
5099A089240242C20062B6F2 /* SSHAgentProtocol.swift */, 5099A089240242C20062B6F2 /* SSHAgentProtocol.swift */,
50A3B79D24026B9900D209EA /* SocketController.swift */, 50A3B79D24026B9900D209EA /* SocketController.swift */,
507CE4EF2420A4C50029F750 /* SigningWitness.swift */, 507CE4EF2420A4C50029F750 /* SigningWitness.swift */,
507CE4F32420A8C10029F750 /* SigningRequestProvenance.swift */,
507CE4F52420A96F0029F750 /* SigningRequestTracer.swift */, 507CE4F52420A96F0029F750 /* SigningRequestTracer.swift */,
50A3B79F24026B9900D209EA /* Agent.swift */, 50A3B79F24026B9900D209EA /* Agent.swift */,
507EE34124281E12003C4FE3 /* FileHandleProtocols.swift */, 507EE34124281E12003C4FE3 /* FileHandleProtocols.swift */,
@ -587,6 +644,7 @@
50A3B79024026B7600D209EA /* Assets.xcassets */, 50A3B79024026B7600D209EA /* Assets.xcassets */,
50A3B79524026B7600D209EA /* Main.storyboard */, 50A3B79524026B7600D209EA /* Main.storyboard */,
50A3B79824026B7600D209EA /* Info.plist */, 50A3B79824026B7600D209EA /* Info.plist */,
508BF29425B4F140009EFB7E /* InternetAccessPolicy.plist */,
50A3B79924026B7600D209EA /* SecretAgent.entitlements */, 50A3B79924026B7600D209EA /* SecretAgent.entitlements */,
50A3B79224026B7600D209EA /* Preview Content */, 50A3B79224026B7600D209EA /* Preview Content */,
); );
@ -725,6 +783,24 @@
productReference = 506772FB2426F3F400034DED /* Brief.framework */; productReference = 506772FB2426F3F400034DED /* Brief.framework */;
productType = "com.apple.product-type.framework"; productType = "com.apple.product-type.framework";
}; };
5091D31E2519D56D0049FD9B /* BriefTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 5091D32A2519D56D0049FD9B /* Build configuration list for PBXNativeTarget "BriefTests" */;
buildPhases = (
5091D31B2519D56D0049FD9B /* Sources */,
5091D31C2519D56D0049FD9B /* Frameworks */,
5091D31D2519D56D0049FD9B /* Resources */,
);
buildRules = (
);
dependencies = (
5091D3262519D56D0049FD9B /* PBXTargetDependency */,
);
name = BriefTests;
productName = BriefTests;
productReference = 5091D31F2519D56D0049FD9B /* BriefTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
5099A06B240242BA0062B6F2 /* SecretAgentKit */ = { 5099A06B240242BA0062B6F2 /* SecretAgentKit */ = {
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 5099A083240242BA0062B6F2 /* Build configuration list for PBXNativeTarget "SecretAgentKit" */; buildConfigurationList = 5099A083240242BA0062B6F2 /* Build configuration list for PBXNativeTarget "SecretAgentKit" */;
@ -790,7 +866,7 @@
50617D7723FCE48D0099B055 /* Project object */ = { 50617D7723FCE48D0099B055 /* Project object */ = {
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastSwiftUpdateCheck = 1140; LastSwiftUpdateCheck = 1220;
LastUpgradeCheck = 1130; LastUpgradeCheck = 1130;
ORGANIZATIONNAME = "Max Goedjen"; ORGANIZATIONNAME = "Max Goedjen";
TargetAttributes = { TargetAttributes = {
@ -812,6 +888,9 @@
CreatedOnToolsVersion = 11.4; CreatedOnToolsVersion = 11.4;
LastSwiftMigration = 1140; LastSwiftMigration = 1140;
}; };
5091D31E2519D56D0049FD9B = {
CreatedOnToolsVersion = 12.2;
};
5099A06B240242BA0062B6F2 = { 5099A06B240242BA0062B6F2 = {
CreatedOnToolsVersion = 11.4; CreatedOnToolsVersion = 11.4;
LastSwiftMigration = 1140; LastSwiftMigration = 1140;
@ -845,6 +924,7 @@
5099A06B240242BA0062B6F2 /* SecretAgentKit */, 5099A06B240242BA0062B6F2 /* SecretAgentKit */,
5099A073240242BA0062B6F2 /* SecretAgentKitTests */, 5099A073240242BA0062B6F2 /* SecretAgentKitTests */,
506772FA2426F3F400034DED /* Brief */, 506772FA2426F3F400034DED /* Brief */,
5091D31E2519D56D0049FD9B /* BriefTests */,
); );
}; };
/* End PBXProject section */ /* End PBXProject section */
@ -854,10 +934,10 @@
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
50617D8D23FCE48E0099B055 /* Main.storyboard in Resources */,
50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */, 50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */,
50617D8723FCE48E0099B055 /* Assets.xcassets in Resources */, 50617D8723FCE48E0099B055 /* Assets.xcassets in Resources */,
506772C72424784600034DED /* Credits.rtf in Resources */, 506772C72424784600034DED /* Credits.rtf in Resources */,
508BF28E25B4F005009EFB7E /* InternetAccessPolicy.plist in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -889,6 +969,13 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
5091D31D2519D56D0049FD9B /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
5099A06A240242BA0062B6F2 /* Resources */ = { 5099A06A240242BA0062B6F2 /* Resources */ = {
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@ -910,6 +997,7 @@
50A3B79724026B7600D209EA /* Main.storyboard in Resources */, 50A3B79724026B7600D209EA /* Main.storyboard in Resources */,
50A3B79424026B7600D209EA /* Preview Assets.xcassets in Resources */, 50A3B79424026B7600D209EA /* Preview Assets.xcassets in Resources */,
50A3B79124026B7600D209EA /* Assets.xcassets in Resources */, 50A3B79124026B7600D209EA /* Assets.xcassets in Resources */,
508BF2AA25B4F1CB009EFB7E /* InternetAccessPolicy.plist in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -920,19 +1008,25 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
50C385A9240B636500AF2719 /* SetupView.swift in Sources */, 2C4A9D2F2636FFD3008CC8E2 /* RenameSecretView.swift in Sources */,
5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */,
5066A6C22516F303004B5A36 /* SetupView.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 */,
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */, 50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */,
5066A6F7251829B1004B5A36 /* ShellConfigurationController.swift in Sources */,
508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */, 508A58B3241ED2180069DC07 /* AgentStatusChecker.swift in Sources */,
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */, 50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */,
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */, 5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */,
50153E20250AFCB200525160 /* UpdateView.swift in Sources */,
50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */, 50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */,
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */,
50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */, 50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */,
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */, 50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */,
50731669241E00C20023809E /* NoticeView.swift in Sources */, 50617D8323FCE48E0099B055 /* App.swift in Sources */,
50617D8323FCE48E0099B055 /* AppDelegate.swift in Sources */,
506772C92425BB8500034DED /* NoStoresView.swift in Sources */, 506772C92425BB8500034DED /* NoStoresView.swift in Sources */,
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */,
508A58B5241ED48F0069DC07 /* PreviewAgentStatusChecker.swift in Sources */, 508A58B5241ED48F0069DC07 /* PreviewAgentStatusChecker.swift in Sources */,
508A58AA241E06B40069DC07 /* PreviewUpdater.swift in Sources */, 508A58AA241E06B40069DC07 /* PreviewUpdater.swift in Sources */,
); );
@ -950,6 +1044,8 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
FA0B34672599619E0013AB3A /* BundleIDs.swift in Sources */,
501B7AE1251C56F700776EC7 /* SigningRequestProvenance.swift in Sources */,
50617DC723FCE4EA0099B055 /* SecretStore.swift in Sources */, 50617DC723FCE4EA0099B055 /* SecretStore.swift in Sources */,
5099A02723FE34FA0062B6F2 /* SmartCard.swift in Sources */, 5099A02723FE34FA0062B6F2 /* SmartCard.swift in Sources */,
50617DCB23FCECA10099B055 /* Secret.swift in Sources */, 50617DCB23FCECA10099B055 /* Secret.swift in Sources */,
@ -984,6 +1080,15 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
5091D31B2519D56D0049FD9B /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
509FA3B625B53C49005E2535 /* ReleaseParsingTests.swift in Sources */,
5091D3222519D56D0049FD9B /* SemVerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
5099A068240242BA0062B6F2 /* Sources */ = { 5099A068240242BA0062B6F2 /* Sources */ = {
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@ -994,7 +1099,6 @@
507CE4ED2420A3C70029F750 /* Agent.swift in Sources */, 507CE4ED2420A3C70029F750 /* Agent.swift in Sources */,
507CE4F02420A4C50029F750 /* SigningWitness.swift in Sources */, 507CE4F02420A4C50029F750 /* SigningWitness.swift in Sources */,
507CE4F62420A96F0029F750 /* SigningRequestTracer.swift in Sources */, 507CE4F62420A96F0029F750 /* SigningRequestTracer.swift in Sources */,
507CE4F42420A8C10029F750 /* SigningRequestProvenance.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -1062,6 +1166,11 @@
target = 50617DA723FCE4AB0099B055 /* SecretKit */; target = 50617DA723FCE4AB0099B055 /* SecretKit */;
targetProxy = 507CE4F12420A6B50029F750 /* PBXContainerItemProxy */; targetProxy = 507CE4F12420A6B50029F750 /* PBXContainerItemProxy */;
}; };
5091D3262519D56D0049FD9B /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 506772FA2426F3F400034DED /* Brief */;
targetProxy = 5091D3252519D56D0049FD9B /* PBXContainerItemProxy */;
};
5099A077240242BA0062B6F2 /* PBXTargetDependency */ = { 5099A077240242BA0062B6F2 /* PBXTargetDependency */ = {
isa = PBXTargetDependency; isa = PBXTargetDependency;
target = 5099A06B240242BA0062B6F2 /* SecretAgentKit */; target = 5099A06B240242BA0062B6F2 /* SecretAgentKit */;
@ -1070,14 +1179,6 @@
/* End PBXTargetDependency section */ /* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */ /* Begin PBXVariantGroup section */
50617D8B23FCE48E0099B055 /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
50617D8C23FCE48E0099B055 /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
50A3B79524026B7600D209EA /* Main.storyboard */ = { 50A3B79524026B7600D209EA /* Main.storyboard */ = {
isa = PBXVariantGroup; isa = PBXVariantGroup;
children = ( children = (
@ -1140,7 +1241,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15; MACOSX_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
@ -1195,7 +1296,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15; MACOSX_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
SDKROOT = macosx; SDKROOT = macosx;
@ -1223,7 +1324,6 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 1; MARKETING_VERSION = 1;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@ -1251,7 +1351,6 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 1; MARKETING_VERSION = 1;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@ -1274,7 +1373,6 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
"@loader_path/../Frameworks", "@loader_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 10.15;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.SecretiveTests; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.SecretiveTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -1296,7 +1394,6 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
"@loader_path/../Frameworks", "@loader_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 10.15;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.SecretiveTests; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.SecretiveTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -1436,11 +1533,10 @@
buildSettings = { buildSettings = {
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "-"; CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = Z72PRUAWF6;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1; DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath"; DYLIB_INSTALL_NAME_BASE = "@rpath";
@ -1541,7 +1637,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15; MACOSX_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
@ -1556,25 +1652,20 @@
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = Secretive/Secretive.entitlements;
CODE_SIGN_IDENTITY = "Developer ID Application";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"Secretive/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"Secretive/Preview Content\"";
DEVELOPMENT_TEAM = Z72PRUAWF6; ENABLE_HARDENED_RUNTIME = NO;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = Secretive/Info.plist; INFOPLIST_FILE = Secretive/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 1; MARKETING_VERSION = 1;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "Secretive - Host";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
}; };
name = Test; name = Test;
@ -1584,17 +1675,15 @@
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_IDENTITY = "Developer ID Application"; CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = Z72PRUAWF6;
INFOPLIST_FILE = SecretiveTests/Info.plist; INFOPLIST_FILE = SecretiveTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
"@loader_path/../Frameworks", "@loader_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 10.15;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.SecretiveTests; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.SecretiveTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -1607,12 +1696,9 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = SecretAgent/SecretAgent.entitlements;
CODE_SIGN_IDENTITY = "Developer ID Application";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_ASSET_PATHS = "\"SecretAgent/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"SecretAgent/Preview Content\"";
DEVELOPMENT_TEAM = Z72PRUAWF6;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = SecretAgent/Info.plist; INFOPLIST_FILE = SecretAgent/Info.plist;
@ -1620,11 +1706,9 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 1; MARKETING_VERSION = 1;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "Secretive - Secret Agent";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
}; };
name = Test; name = Test;
@ -1634,11 +1718,10 @@
buildSettings = { buildSettings = {
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "-"; CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = Z72PRUAWF6;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1; DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath"; DYLIB_INSTALL_NAME_BASE = "@rpath";
@ -1663,10 +1746,9 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_IDENTITY = "Developer ID Application"; CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = Z72PRUAWF6;
INFOPLIST_FILE = SecretKitTests/Info.plist; INFOPLIST_FILE = SecretKitTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -1685,11 +1767,10 @@
buildSettings = { buildSettings = {
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "-"; CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = Z72PRUAWF6;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1; DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath"; DYLIB_INSTALL_NAME_BASE = "@rpath";
@ -1714,10 +1795,9 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_IDENTITY = "Developer ID Application"; CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = Z72PRUAWF6;
INFOPLIST_FILE = SecretAgentKitTests/Info.plist; INFOPLIST_FILE = SecretAgentKitTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -1731,6 +1811,64 @@
}; };
name = Test; name = Test;
}; };
5091D3272519D56D0049FD9B /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = Z72PRUAWF6;
INFOPLIST_FILE = BriefTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.BriefTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
5091D3282519D56D0049FD9B /* Test */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = BriefTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.BriefTests;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Test;
};
5091D3292519D56D0049FD9B /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = Z72PRUAWF6;
INFOPLIST_FILE = BriefTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.BriefTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
};
name = Release;
};
5099A084240242BA0062B6F2 /* Debug */ = { 5099A084240242BA0062B6F2 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
@ -1844,7 +1982,6 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 1; MARKETING_VERSION = 1;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@ -1869,7 +2006,6 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 1; MARKETING_VERSION = 1;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent; PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.SecretAgent;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@ -1941,6 +2077,16 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
5091D32A2519D56D0049FD9B /* Build configuration list for PBXNativeTarget "BriefTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
5091D3272519D56D0049FD9B /* Debug */,
5091D3282519D56D0049FD9B /* Test */,
5091D3292519D56D0049FD9B /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
5099A083240242BA0062B6F2 /* Build configuration list for PBXNativeTarget "SecretAgentKit" */ = { 5099A083240242BA0062B6F2 /* Build configuration list for PBXNativeTarget "SecretAgentKit" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (

96
Secretive/App.swift Normal file
View File

@ -0,0 +1,96 @@
import Cocoa
import SwiftUI
import SecretKit
import Brief
@main
struct Secretive: App {
private let storeList: SecretStoreList = {
let list = SecretStoreList()
list.add(store: SecureEnclave.Store())
list.add(store: SmartCard.Store())
return list
}()
private let agentStatusChecker = AgentStatusChecker()
private let justUpdatedChecker = JustUpdatedChecker()
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false
@State private var showingSetup = false
@State private var showingCreation = false
@SceneBuilder var body: some Scene {
WindowGroup {
ContentView<Updater, AgentStatusChecker>(showingCreation: $showingCreation, runningSetup: $showingSetup, hasRunSetup: $hasRunSetup)
.environmentObject(storeList)
.environmentObject(Updater(checkOnLaunch: hasRunSetup))
.environmentObject(agentStatusChecker)
.onAppear {
if !hasRunSetup {
showingSetup = true
}
}
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
guard hasRunSetup else { return }
agentStatusChecker.check()
if agentStatusChecker.running && justUpdatedChecker.justUpdated {
// Relaunch the agent, since it'll be running from earlier update still
reinstallAgent()
} else if !agentStatusChecker.running {
forceLaunchAgent()
}
}
}
.commands {
CommandGroup(after: CommandGroupPlacement.newItem) {
Button("New Secret") {
showingCreation = true
}
.keyboardShortcut(KeyboardShortcut(KeyEquivalent("N"), modifiers: [.command, .shift]))
}
CommandGroup(replacing: .help) {
Button("Help") {
NSWorkspace.shared.open(Constants.helpURL)
}
}
CommandGroup(after: .help) {
Button("Setup Secretive") {
showingSetup = true
}
}
SidebarCommands()
}
}
}
extension Secretive {
private func reinstallAgent() {
justUpdatedChecker.check()
LaunchAgentController().install {
// Wait a second for launchd to kick in (next runloop isn't enough).
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
agentStatusChecker.check()
if !agentStatusChecker.running {
forceLaunchAgent()
}
}
}
}
private func forceLaunchAgent() {
// We've run setup, we didn't just update, launchd is just not doing it's thing.
// Force a launch directly.
LaunchAgentController().forceLaunch { _ in
agentStatusChecker.check()
}
}
}
private enum Constants {
static let helpURL = URL(string: "https://github.com/maxgoedjen/secretive/blob/main/FAQ.md")!
}

View File

@ -1,108 +0,0 @@
import Cocoa
import SwiftUI
import SecretKit
import Brief
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
@IBOutlet var newMenuItem: NSMenuItem!
@IBOutlet var toolbar: NSToolbar!
let storeList: SecretStoreList = {
let list = SecretStoreList()
list.add(store: SecureEnclave.Store())
list.add(store: SmartCard.Store())
return list
}()
let updater = Updater()
let agentStatusChecker = AgentStatusChecker()
let justUpdatedChecker = JustUpdatedChecker()
func applicationDidFinishLaunching(_ aNotification: Notification) {
let contentView = ContentView(storeList: storeList, updater: updater, agentStatusChecker: agentStatusChecker, runSetupBlock: { self.runSetup(sender: nil) })
// Create the window and set the content view.
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
window.titleVisibility = .hidden
window.toolbar = toolbar
window.isReleasedWhenClosed = false
if storeList.modifiableStore?.isAvailable ?? false {
let plus = NSTitlebarAccessoryViewController()
plus.view = NSButton(image: NSImage(named: NSImage.addTemplateName)!, target: self, action: #selector(add(sender:)))
plus.layoutAttribute = .right
window.addTitlebarAccessoryViewController(plus)
newMenuItem.isEnabled = true
}
runSetupIfNeeded()
relaunchAgentIfNeeded()
}
func applicationDidBecomeActive(_ notification: Notification) {
agentStatusChecker.check()
}
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
guard !flag else { return false }
window.makeKeyAndOrderFront(self)
return true
}
@IBAction func add(sender: AnyObject?) {
var addWindow: NSWindow!
let addView = CreateSecretView(store: storeList.modifiableStore!) {
self.window.endSheet(addWindow)
}
addWindow = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
addWindow.contentView = NSHostingView(rootView: addView)
window.beginSheet(addWindow, completionHandler: nil)
}
@IBAction func runSetup(sender: AnyObject?) {
let setupWindow = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 0, height: 0),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
let setupView = SetupView() { success in
self.window.endSheet(setupWindow)
self.agentStatusChecker.check()
}
setupWindow.contentView = NSHostingView(rootView: setupView)
window.beginSheet(setupWindow, completionHandler: nil)
}
}
extension AppDelegate {
func runSetupIfNeeded() {
if !UserDefaults.standard.bool(forKey: Constants.defaultsHasRunSetup) {
UserDefaults.standard.set(true, forKey: Constants.defaultsHasRunSetup)
runSetup(sender: nil)
}
}
func relaunchAgentIfNeeded() {
if agentStatusChecker.running && justUpdatedChecker.justUpdated {
LaunchAgentController().relaunch()
}
}
}
extension AppDelegate {
enum Constants {
static let defaultsHasRunSetup = "defaultsHasRunSetup"
}
}

View File

@ -31,13 +31,13 @@
"size" : "128x128" "size" : "128x128"
}, },
{ {
"filename" : "Icon 2@1x.png", "filename" : "Mac Icon.png",
"idiom" : "mac", "idiom" : "mac",
"scale" : "1x", "scale" : "1x",
"size" : "256x256" "size" : "256x256"
}, },
{ {
"filename" : "Icon 2@2x.png", "filename" : "Mac Icon@0.25x.png",
"idiom" : "mac", "idiom" : "mac",
"scale" : "2x", "scale" : "2x",
"size" : "256x256" "size" : "256x256"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -1,160 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="16085" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="16085"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Application-->
<scene sceneID="JPo-4y-FX3">
<objects>
<application id="hnw-xV-0zn" sceneMemberID="viewController">
<menu key="mainMenu" title="Main Menu" systemMenu="main" autoenablesItems="NO" id="AYu-sK-qS6">
<items>
<menuItem title="Secretive" id="1Xt-HY-uBw">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Secretive" systemMenu="apple" id="uQy-DD-JDr">
<items>
<menuItem title="About Secretive" id="5kV-Vb-QxS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontStandardAboutPanel:" target="Ady-hI-5gd" id="Exp-CZ-Vem"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
<menuItem title="Services" id="NMo-om-nkz">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
</menuItem>
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
<menuItem title="Hide Secretive" keyEquivalent="h" id="Olw-nP-bQN">
<connections>
<action selector="hide:" target="Ady-hI-5gd" id="PnN-Uc-m68"/>
</connections>
</menuItem>
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="hideOtherApplications:" target="Ady-hI-5gd" id="VT4-aY-XCT"/>
</connections>
</menuItem>
<menuItem title="Show All" id="Kd2-mp-pUS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="unhideAllApplications:" target="Ady-hI-5gd" id="Dhg-Le-xox"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
<menuItem title="Quit Secretive" keyEquivalent="q" id="4sb-4s-VLi">
<connections>
<action selector="terminate:" target="Ady-hI-5gd" id="Te7-pn-YzF"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="File" id="dMs-cI-mzQ">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="File" autoenablesItems="NO" id="bib-Uj-vzu">
<items>
<menuItem title="New" enabled="NO" keyEquivalent="n" id="Was-JA-tGl">
<connections>
<action selector="addWithSender:" target="Voe-Tx-rLC" id="U1t-YZ-Hn5"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="m54-Is-iLE"/>
<menuItem title="Close" keyEquivalent="w" id="DVo-aG-piG">
<connections>
<action selector="performClose:" target="Ady-hI-5gd" id="HmO-Ls-i7Q"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Window" id="aUF-d1-5bR">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
<items>
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
<connections>
<action selector="performMiniaturize:" target="Ady-hI-5gd" id="VwT-WD-YPe"/>
</connections>
</menuItem>
<menuItem title="Zoom" id="R4o-n2-Eq4">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="performZoom:" target="Ady-hI-5gd" id="DIl-cC-cCs"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="arrangeInFront:" target="Ady-hI-5gd" id="DRN-fu-gQh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Help" id="wpr-3q-Mcd">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
<items>
<menuItem title="Setup Helper App" id="04y-R6-7bF">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="runSetupWithSender:" target="Voe-Tx-rLC" id="Fty-2m-eng"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="Ddf-5M-Bmf"/>
<menuItem title="Secretive Help" keyEquivalent="?" id="FKE-Sm-Kum">
<connections>
<action selector="showHelp:" target="Ady-hI-5gd" id="y7X-2Q-9no"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
<connections>
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
</connections>
</application>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Secretive" customModuleProvider="target">
<connections>
<outlet property="newMenuItem" destination="Was-JA-tGl" id="C8s-uk-gMA"/>
<outlet property="toolbar" destination="bvo-mt-QR4" id="XSF-g2-znt"/>
</connections>
</customObject>
<toolbar implicitIdentifier="09D11707-F4A3-4FD5-970E-AC5832E91C2B" autosavesConfiguration="NO" displayMode="iconAndLabel" sizeMode="regular" id="bvo-mt-QR4">
<allowedToolbarItems>
<toolbarItem implicitItemIdentifier="NSToolbarFlexibleSpaceItem" id="9Xm-OQ-a7h"/>
<toolbarItem implicitItemIdentifier="728E7E6E-F692-41A1-9439-C6EF9BE96CBA" label="Secretive" paletteLabel="" sizingBehavior="auto" id="xbD-W8-Ypr">
<nil key="toolTip"/>
<textField key="view" horizontalHuggingPriority="251" verticalHuggingPriority="750" id="Mg0-Hm-7bW">
<rect key="frame" x="0.0" y="14" width="65" height="16"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Secretive" id="EXw-BM-zF7">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="windowFrameTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</toolbarItem>
</allowedToolbarItems>
<defaultToolbarItems>
<toolbarItem reference="9Xm-OQ-a7h"/>
<toolbarItem reference="xbD-W8-Ypr"/>
<toolbarItem reference="9Xm-OQ-a7h"/>
</defaultToolbarItems>
</toolbar>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="75" y="0.0"/>
</scene>
</scenes>
</document>

View File

@ -1,6 +1,7 @@
import Foundation import Foundation
import Combine import Combine
import AppKit import AppKit
import SecretKit
protocol AgentStatusCheckerProtocol: ObservableObject { protocol AgentStatusCheckerProtocol: ObservableObject {
var running: Bool { get } var running: Bool { get }
@ -15,19 +16,26 @@ class AgentStatusChecker: ObservableObject, AgentStatusCheckerProtocol {
} }
func check() { func check() {
running = secretAgentProcess != nil running = instanceSecretAgentProcess != nil
} }
var secretAgentProcess: NSRunningApplication? { // All processes, including ones from older versions, etc
NSRunningApplication.runningApplications(withBundleIdentifier: Constants.secretAgentAppID).first var secretAgentProcesses: [NSRunningApplication] {
NSRunningApplication.runningApplications(withBundleIdentifier: Bundle.main.agentBundleID)
}
// The process corresponding to this instance of Secretive
var instanceSecretAgentProcess: NSRunningApplication? {
let agents = secretAgentProcesses
for agent in agents {
guard let url = agent.bundleURL else { continue }
if url.absoluteString.hasPrefix(Bundle.main.bundleURL.absoluteString) {
return agent
}
}
return nil
} }
} }
extension AgentStatusChecker {
enum Constants {
static let secretAgentAppID = "com.maxgoedjen.Secretive.SecretAgent"
}
}

View File

@ -0,0 +1,21 @@
import Foundation
struct ApplicationDirectoryController {
}
extension ApplicationDirectoryController {
var isInApplicationsDirectory: Bool {
let bundlePath = Bundle.main.bundlePath
for directory in NSSearchPathForDirectoriesInDomains(.allApplicationsDirectory, .allDomainsMask, true) {
if bundlePath.hasPrefix(directory) {
return true
}
}
if bundlePath.contains("/Library/Developer/Xcode") {
return true
}
return false
}
}

View File

@ -18,9 +18,7 @@ class JustUpdatedChecker: ObservableObject, JustUpdatedCheckerProtocol {
let lastBuild = UserDefaults.standard.object(forKey: Constants.previousVersionUserDefaultsKey) as? String ?? "None" let lastBuild = UserDefaults.standard.object(forKey: Constants.previousVersionUserDefaultsKey) as? String ?? "None"
let currentBuild = Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String let currentBuild = Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String
UserDefaults.standard.set(currentBuild, forKey: Constants.previousVersionUserDefaultsKey) UserDefaults.standard.set(currentBuild, forKey: Constants.previousVersionUserDefaultsKey)
if lastBuild != currentBuild { justUpdated = lastBuild != currentBuild
justUpdated = true
}
} }

View File

@ -1,19 +1,43 @@
import Foundation import Foundation
import ServiceManagement import ServiceManagement
import AppKit
import OSLog
import SecretKit
struct LaunchAgentController { struct LaunchAgentController {
func install(completion: (() -> Void)? = nil) {
Logger().debug("Installing agent")
_ = setEnabled(false)
// This is definitely a bit of a "seems to work better" thing but:
// Seems to more reliably hit if these are on separate runloops, otherwise it seems like it sometimes doesn't kill old
// and start new?
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
_ = setEnabled(true)
completion?()
}
func install() -> Bool {
setEnabled(true)
} }
func relaunch() { func forceLaunch(completion: ((Bool) -> Void)?) {
_ = setEnabled(false) Logger().debug("Agent is not running, attempting to force launch")
_ = setEnabled(true) let url = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LoginItems/SecretAgent.app")
let config = NSWorkspace.OpenConfiguration()
config.activates = false
NSWorkspace.shared.openApplication(at: url, configuration: config) { app, error in
DispatchQueue.main.async {
completion?(error == nil)
}
if let error = error {
Logger().error("Error force launching \(error.localizedDescription)")
} else {
Logger().debug("Agent force launched")
}
}
} }
private func setEnabled(_ enabled: Bool) -> Bool { private func setEnabled(_ enabled: Bool) -> Bool {
SMLoginItemSetEnabled("com.maxgoedjen.Secretive.SecretAgent" as CFString, enabled) SMLoginItemSetEnabled(Bundle.main.agentBundleID as CFString, enabled)
} }
} }

View File

@ -0,0 +1,63 @@
import Foundation
import Cocoa
import SecretKit
struct ShellConfigurationController {
let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.main.hostBundleID, with: Bundle.main.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String
var shellInstructions: [ShellConfigInstruction] {
[
ShellConfigInstruction(shell: "global",
shellConfigDirectory: "~/.ssh/",
shellConfigFilename: "config",
text: "Host *\n\tIdentityAgent \(socketPath)"),
ShellConfigInstruction(shell: "zsh",
shellConfigDirectory: "~/",
shellConfigFilename: ".zshrc",
text: "export SSH_AUTH_SOCK=\(socketPath)"),
ShellConfigInstruction(shell: "bash",
shellConfigDirectory: "~/",
shellConfigFilename: ".bashrc",
text: "export SSH_AUTH_SOCK=\(socketPath)"),
ShellConfigInstruction(shell: "fish",
shellConfigDirectory: "~/.config/fish",
shellConfigFilename: "config.fish",
text: "set -x SSH_AUTH_SOCK \(socketPath)"),
]
}
func addToShell(shellInstructions: ShellConfigInstruction) -> Bool {
let openPanel = NSOpenPanel()
// This is sync, so no need to strongly retain
let delegate = Delegate(name: shellInstructions.shellConfigFilename)
openPanel.delegate = delegate
openPanel.message = "Select \(shellInstructions.shellConfigFilename) to let Secretive configure your shell automatically."
openPanel.prompt = "Add to \(shellInstructions.shellConfigFilename)"
openPanel.canChooseFiles = true
openPanel.canChooseDirectories = false
openPanel.showsHiddenFiles = true
openPanel.directoryURL = URL(fileURLWithPath: shellInstructions.shellConfigDirectory)
openPanel.nameFieldStringValue = shellInstructions.shellConfigFilename
openPanel.allowedContentTypes = [.symbolicLink, .data, .plainText]
openPanel.runModal()
guard let fileURL = openPanel.urls.first else { return false }
let handle: FileHandle
do {
handle = try FileHandle(forUpdating: fileURL)
guard let existing = try handle.readToEnd(),
let existingString = String(data: existing, encoding: .utf8) else { return false }
guard !existingString.contains(shellInstructions.text) else {
return true
}
try handle.seekToEnd()
} catch {
return false
}
handle.write("\n# Secretive Config\n\(shellInstructions.text)\n".data(using: .utf8)!)
return true
}
}

View File

@ -1,4 +1,4 @@
{\rtf1\ansi\ansicpg1252\cocoartf2511 {\rtf1\ansi\ansicpg1252\cocoartf2580
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;} \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;} {\colortbl;\red255\green255\blue255;}
{\*\expandedcolortbl;;} {\*\expandedcolortbl;;}
@ -12,6 +12,19 @@
{\field{\*\fldinst{HYPERLINK "GITHUB_BUILD_URL"}}{\fldrslt Build Log}}\ {\field{\*\fldinst{HYPERLINK "GITHUB_BUILD_URL"}}{\fldrslt Build Log}}\
\ \
Special Thanks To:\ Special Thanks To:\
\
{\field{\*\fldinst{HYPERLINK "https://github.com/maxgoedjen/secretive/graphs/contributors"}}{\fldrslt Contributors}}:\
{\field{\*\fldinst{HYPERLINK "https://github.com/0xflotus"}}{\fldrslt 0xflotus}}\
{\field{\*\fldinst{HYPERLINK "https://github.com/aaron-trout"}}{\fldrslt Aaron Trout}}\
\pard\pardeftab720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://github.com/EppO"}}{\fldrslt \cf0 Florent Monbillard}}\
{\field{\*\fldinst{HYPERLINK "https://github.com/vladimyr"}}{\fldrslt Dario Vladovi\uc0\u263 }}\
{\field{\*\fldinst{HYPERLINK "https://github.com/lavalleeale"}}{\fldrslt Alex Lavallee}}\
{\field{\*\fldinst{HYPERLINK "https://github.com/joshheyse"}}{\fldrslt Josh}}\
{\field{\*\fldinst{HYPERLINK "https://github.com/diesal11"}}{\fldrslt Dylan Lundy}}\
\
\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0
\cf0 Testers:\
{\field{\*\fldinst{HYPERLINK "https://github.com/bdash"}}{\fldrslt Mark Rowe}}\ {\field{\*\fldinst{HYPERLINK "https://github.com/bdash"}}{\fldrslt Mark Rowe}}\
{\field{\*\fldinst{HYPERLINK "https://github.com/danielctull"}}{\fldrslt Daniel Tull}}\ {\field{\*\fldinst{HYPERLINK "https://github.com/danielctull"}}{\fldrslt Daniel Tull}}\
{\field{\*\fldinst{HYPERLINK "https://github.com/davedelong"}}{\fldrslt Dave DeLong}}\ {\field{\*\fldinst{HYPERLINK "https://github.com/davedelong"}}{\fldrslt Dave DeLong}}\

View File

@ -24,8 +24,6 @@
<string>$(MACOSX_DEPLOYMENT_TARGET)</string> <string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key> <key>NSHumanReadableCopyright</key>
<string>$(PRODUCT_NAME) is MIT Licensed.</string> <string>$(PRODUCT_NAME) is MIT Licensed.</string>
<key>NSMainStoryboardFile</key>
<string>Main</string>
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>
<string>NSApplication</string> <string>NSApplication</string>
<key>NSSupportsAutomaticTermination</key> <key>NSSupportsAutomaticTermination</key>

View File

@ -0,0 +1,31 @@
<?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>ApplicationDescription</key>
<string>Secretive is an app for storing and managing SSH keys in the Secure Enclave</string>
<key>DeveloperName</key>
<string>Max Goedjen</string>
<key>Website</key>
<string>https://github.com/maxgoedjen/secretive</string>
<key>Connections</key>
<array>
<dict>
<key>IsIncoming</key>
<false/>
<key>Host</key>
<string>api.github.com</string>
<key>NetworkProtocol</key>
<string>TCP</string>
<key>Port</key>
<string>443</string>
<key>Purpose</key>
<string>Secretive checks GitHub for new versions and security updates.</string>
<key>DenyConsequences</key>
<string>If you deny these connections, you will not be notified about new versions and critical security updates.</string>
</dict>
</array>
<key>Services</key>
<array/>
</dict>
</plist>

View File

@ -35,14 +35,13 @@ extension Preview {
self.secrets.append(contentsOf: new) self.secrets.append(contentsOf: new)
} }
func sign(data: Data, with secret: Preview.Secret) throws -> Data { func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance) throws -> Data {
return data return data
} }
} }
class StoreModifiable: Store, SecretStoreModifiable { class StoreModifiable: Store, SecretStoreModifiable {
override var name: String { "Modifiable Preview Store" } override var name: String { "Modifiable Preview Store" }
func create(name: String, requiresAuthentication: Bool) throws { func create(name: String, requiresAuthentication: Bool) throws {
@ -50,8 +49,10 @@ extension Preview {
func delete(secret: Preview.Secret) throws { func delete(secret: Preview.Secret) throws {
} }
}
func update(secret: Preview.Secret, name: String) throws {
}
}
} }
extension Preview { extension Preview {

View File

@ -11,9 +11,9 @@ class PreviewUpdater: UpdaterProtocol {
case .none: case .none:
self.update = nil self.update = nil
case .advisory: case .advisory:
self.update = Release(name: "10.10.10", html_url: URL(string: "https://example.com")!, body: "Some regular update") self.update = Release(name: "10.10.10", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Some regular update")
case .critical: case .critical:
self.update = Release(name: "10.10.10", html_url: URL(string: "https://example.com")!, body: "Critical Security Update") self.update = Release(name: "10.10.10", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update")
} }
} }

View File

@ -4,10 +4,12 @@
<dict> <dict>
<key>com.apple.security.app-sandbox</key> <key>com.apple.security.app-sandbox</key>
<true/> <true/>
<key>com.apple.security.smartcard</key> <key>com.apple.security.files.user-selected.read-write</key>
<true/> <true/>
<key>com.apple.security.network.client</key> <key>com.apple.security.network.client</key>
<true/> <true/>
<key>com.apple.security.smartcard</key>
<true/>
<key>keychain-access-groups</key> <key>keychain-access-groups</key>
<array> <array>
<string>$(AppIdentifierPrefix)com.maxgoedjen.Secretive</string> <string>$(AppIdentifierPrefix)com.maxgoedjen.Secretive</string>

View File

@ -3,146 +3,190 @@ import SecretKit
import Brief import Brief
struct ContentView<UpdaterType: UpdaterProtocol, AgentStatusCheckerType: AgentStatusCheckerProtocol>: View { struct ContentView<UpdaterType: UpdaterProtocol, AgentStatusCheckerType: AgentStatusCheckerProtocol>: View {
@ObservedObject var storeList: SecretStoreList
@ObservedObject var updater: UpdaterType
@ObservedObject var agentStatusChecker: AgentStatusCheckerType
var runSetupBlock: (() -> Void)?
@State fileprivate var active: AnySecret.ID? @Binding var showingCreation: Bool
@State fileprivate var showingDeletion = false @Binding var runningSetup: Bool
@State fileprivate var deletingSecret: AnySecret? @Binding var hasRunSetup: Bool
@EnvironmentObject private var storeList: SecretStoreList
@EnvironmentObject private var updater: UpdaterType
@EnvironmentObject private var agentStatusChecker: AgentStatusCheckerType
@State private var selectedUpdate: Release?
@State private var showingAppPathNotice = false
var body: some View { var body: some View {
VStack { VStack {
if updater.update != nil {
updateNotice()
}
if !agentStatusChecker.running {
agentNotice()
}
if storeList.anyAvailable { if storeList.anyAvailable {
NavigationView { StoreListView(showingCreation: $showingCreation)
List(selection: $active) {
ForEach(storeList.stores) { store in
if store.isAvailable {
Section(header: Text(store.name)) {
if store.secrets.isEmpty {
if store is AnySecretStoreModifiable {
NavigationLink(destination: EmptyStoreModifiableView(), tag: Constants.emptyStoreModifiableTag, selection: self.$active) {
Text("No Secrets")
}
} else {
NavigationLink(destination: EmptyStoreView(), tag: Constants.emptyStoreTag, selection: self.$active) {
Text("No Secrets")
}
}
} else {
ForEach(store.secrets) { secret in
NavigationLink(destination: SecretDetailView(secret: secret), tag: secret.id, selection: self.$active) {
Text(secret.name)
}.contextMenu {
if store is AnySecretStoreModifiable {
Button(action: { self.delete(secret: secret) }) {
Text("Delete")
}
}
}
}
}
}
}
}
}.onAppear {
self.active = self.nextDefaultSecret
}
.listStyle(SidebarListStyle())
.frame(minWidth: 100, idealWidth: 240)
}
.navigationViewStyle(DoubleColumnNavigationViewStyle())
.sheet(isPresented: $showingDeletion) {
if self.storeList.modifiableStore != nil {
DeleteSecretView(secret: self.deletingSecret!, store: self.storeList.modifiableStore!) { deleted in
self.showingDeletion = false
if deleted {
self.active = self.nextDefaultSecret
}
}
}
}
} else { } else {
NoStoresView() NoStoresView()
} }
}.frame(minWidth: 640, minHeight: 320) }
.frame(minWidth: 640, minHeight: 320)
.toolbar {
updateNotice
setupNotice
appPathNotice
newItem
}
} }
func updateNotice() -> some View { }
guard let update = updater.update else { return AnyView(Spacer()) }
let severity: NoticeView.Severity extension ContentView {
var updateNotice: ToolbarItem<Void, AnyView> {
guard let update = updater.update else {
return ToolbarItem { AnyView(EmptyView()) }
}
let color: Color
let text: String let text: String
if update.critical { if update.critical {
severity = .critical
text = "Critical Security Update Required" text = "Critical Security Update Required"
color = .red
} else { } else {
severity = .advisory
text = "Update Available" text = "Update Available"
color = .orange
} }
return AnyView(NoticeView(text: text, severity: severity, actionTitle: "Update") { return ToolbarItem {
NSWorkspace.shared.open(update.html_url) AnyView(
}) Button(action: {
} selectedUpdate = update
}, label: {
func agentNotice() -> some View { Text(text)
NoticeView(text: "Secret Agent isn't running. Run setup again to fix.", severity: .advisory, actionTitle: "Run Setup") { .font(.headline)
self.runSetupBlock?() .foregroundColor(.white)
})
.background(color)
.cornerRadius(5)
.popover(item: $selectedUpdate, attachmentAnchor: .point(.bottom), arrowEdge: .bottom) { update in
UpdateDetailView(update: update)
}
)
} }
} }
func delete<SecretType: Secret>(secret: SecretType) { var newItem: ToolbarItem<Void, AnyView> {
deletingSecret = AnySecret(secret) guard storeList.modifiableStore?.isAvailable ?? false else {
self.showingDeletion = true 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 nextDefaultSecret: AnyHashable? { var setupNotice: ToolbarItem<Void, AnyView> {
let fallback: AnyHashable return ToolbarItem {
if self.storeList.modifiableStore?.isAvailable ?? false { AnyView(
fallback = Constants.emptyStoreModifiableTag Group {
} else { if runningSetup || !hasRunSetup || !agentStatusChecker.running {
fallback = Constants.emptyStoreTag 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()
}
}
.sheet(isPresented: $runningSetup) {
SetupView(visible: $runningSetup, setupComplete: $hasRunSetup)
}
)
} }
return self.storeList.stores.compactMap { $0.secrets.first }.first?.id ?? fallback
} }
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)
})
.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()
}
)
}
}
} }
fileprivate enum Constants {
static let emptyStoreModifiableTag: AnyHashable = "emptyStoreModifiableTag"
static let emptyStoreTag: AnyHashable = "emptyStoreModifiableTag"
}
#if DEBUG #if DEBUG
struct ContentView_Previews: PreviewProvider { struct ContentView_Previews: PreviewProvider {
private static let storeList: SecretStoreList = {
let list = SecretStoreList()
list.add(store: SecureEnclave.Store())
list.add(store: SmartCard.Store())
return list
}()
private static let agentStatusChecker = AgentStatusChecker()
private static let justUpdatedChecker = JustUpdatedChecker()
@State var hasRunSetup = false
@State private var showingSetup = false
@State private var showingCreation = false
static var previews: some View { static var previews: some View {
Group { Group {
ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], // Empty on modifiable and nonmodifiable
modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]), ContentView<PreviewUpdater, AgentStatusChecker>(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true))
updater: PreviewUpdater(), .environmentObject(Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]))
agentStatusChecker: PreviewAgentStatusChecker()) .environmentObject(PreviewUpdater())
ContentView(storeList: Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()]), updater: PreviewUpdater(), .environmentObject(agentStatusChecker)
agentStatusChecker: PreviewAgentStatusChecker())
ContentView(storeList: Preview.storeList(stores: [Preview.Store()]), updater: PreviewUpdater(), // 5 items on modifiable and nonmodifiable
agentStatusChecker: PreviewAgentStatusChecker()) ContentView<PreviewUpdater, AgentStatusChecker>(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true))
ContentView(storeList: Preview.storeList(modifiableStores: [Preview.StoreModifiable()]), updater: PreviewUpdater(), .environmentObject(Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()]))
agentStatusChecker: PreviewAgentStatusChecker()) .environmentObject(PreviewUpdater())
ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]), updater: PreviewUpdater(update: .advisory), .environmentObject(agentStatusChecker)
agentStatusChecker: PreviewAgentStatusChecker())
ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]), updater: PreviewUpdater(update: .critical),
agentStatusChecker: PreviewAgentStatusChecker())
ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]), updater: PreviewUpdater(update: .critical),
agentStatusChecker: PreviewAgentStatusChecker(running: false))
} }
.environmentObject(agentStatusChecker)
} }
} }

View File

@ -0,0 +1,135 @@
import SwiftUI
struct CopyableView: View {
var title: String
var image: Image
var text: String
@State private var interactionState: InteractionState = .normal
var body: some View {
VStack(alignment: .leading) {
HStack {
image
.renderingMode(.template)
.imageScale(.large)
.foregroundColor(primaryTextColor)
Text(title)
.font(.headline)
.foregroundColor(primaryTextColor)
Spacer()
if interactionState != .normal {
Text(hoverText)
.bold()
.textCase(.uppercase)
.foregroundColor(secondaryTextColor)
.transition(.opacity)
}
}
.padding(EdgeInsets(top: 20, leading: 20, bottom: 10, trailing: 20))
Divider()
Text(text)
.fixedSize(horizontal: false, vertical: true)
.foregroundColor(primaryTextColor)
.padding(EdgeInsets(top: 10, leading: 20, bottom: 20, trailing: 20))
.multilineTextAlignment(.leading)
.font(.system(.body, design: .monospaced))
}
.background(backgroundColor)
.frame(minWidth: 150, maxWidth: .infinity)
.cornerRadius(10)
.onHover { hovering in
withAnimation {
interactionState = hovering ? .hovering : .normal
}
}
.onDrag {
NSItemProvider(item: NSData(data: text.data(using: .utf8)!), typeIdentifier: kUTTypeUTF8PlainText as String)
}
.onTapGesture {
copy()
withAnimation {
interactionState = .clicking
}
}
.gesture(
TapGesture()
.onEnded {
withAnimation {
interactionState = .normal
}
}
)
}
var hoverText: String {
switch interactionState {
case .hovering:
return "Click to Copy"
case .clicking:
return "Copied"
case .normal:
fatalError()
}
}
var backgroundColor: Color {
let color: NSColor
switch interactionState {
case .normal:
color = .windowBackgroundColor
case .hovering:
color = .unemphasizedSelectedContentBackgroundColor
case .clicking:
color = .selectedContentBackgroundColor
}
return Color(color)
}
var primaryTextColor: Color {
let color: NSColor
switch interactionState {
case .normal, .hovering:
color = .textColor
case .clicking:
color = .white
}
return Color(color)
}
var secondaryTextColor: Color {
let color: NSColor
switch interactionState {
case .normal, .hovering:
color = .secondaryLabelColor
case .clicking:
color = .white
}
return Color(color)
}
func copy() {
NSPasteboard.general.declareTypes([.string], owner: nil)
NSPasteboard.general.setString(text, forType: .string)
}
private enum InteractionState {
case normal, hovering, clicking
}
}
#if DEBUG
struct CopyableView_Previews: PreviewProvider {
static var previews: some View {
Group {
CopyableView(title: "Title", image: Image(systemName: "figure.wave"), text: "Hello world.")
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. ")
}
}
}
#endif

View File

@ -1,15 +1,14 @@
import SwiftUI import SwiftUI
import SecretKit import SecretKit
struct CreateSecretView: View { struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
@ObservedObject var store: AnySecretStoreModifiable @ObservedObject var store: StoreType
@Binding var showing: Bool
@State var name = ""
@State var requiresAuthentication = true @State private var name = ""
@State private var requiresAuthentication = true
var dismissalBlock: () -> ()
var body: some View { var body: some View {
VStack { VStack {
HStack { HStack {
@ -24,31 +23,35 @@ struct CreateSecretView: View {
} }
HStack { HStack {
Text("Name:") Text("Name:")
TextField("Shhhhh", text: $name) TextField("Shhhhh", text: $name).focusable()
} }
HStack { HStack {
Toggle(isOn: $requiresAuthentication) { VStack(spacing: 20) {
Text("Requires Authentication (Biometrics or Password)") 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() Spacer()
} }
} }
.onExitCommand(perform: dismissalBlock)
} }
HStack { HStack {
Spacer() Spacer()
Button(action: dismissalBlock) { Button("Cancel") {
Text("Cancel") showing = false
} }
Button(action: save) { .keyboardShortcut(.cancelAction)
Text("Create") Button("Create", action: save)
}.disabled(name.isEmpty) .disabled(name.isEmpty)
.keyboardShortcut(.defaultAction)
} }
}.padding() }.padding()
} }
func save() { func save() {
try! store.create(name: name, requiresAuthentication: requiresAuthentication) try! store.create(name: name, requiresAuthentication: requiresAuthentication)
dismissalBlock() showing = false
} }
} }

View File

@ -2,20 +2,13 @@ import SwiftUI
import SecretKit import SecretKit
struct DeleteSecretView<StoreType: SecretStoreModifiable>: View { struct DeleteSecretView<StoreType: SecretStoreModifiable>: View {
let secret: StoreType.SecretType
@ObservedObject var store: StoreType @ObservedObject var store: StoreType
let secret: StoreType.SecretType
@State var confirm = "" var dismissalBlock: (Bool) -> ()
fileprivate var dismissalBlock: (Bool) -> () @State private var confirm = ""
init(secret: StoreType.SecretType, store: StoreType, dismissalBlock: @escaping (Bool) -> ()) {
self.secret = secret
self.store = store
self.dismissalBlock = dismissalBlock
}
var body: some View { var body: some View {
VStack { VStack {
HStack { HStack {
@ -37,24 +30,28 @@ struct DeleteSecretView<StoreType: SecretStoreModifiable>: View {
TextField(secret.name, text: $confirm) TextField(secret.name, text: $confirm)
} }
} }
.onExitCommand {
self.dismissalBlock(false)
}
} }
HStack { HStack {
Spacer() Spacer()
Button(action: delete) { Button("Delete", action: delete)
Text("Delete") .disabled(confirm != secret.name)
}.disabled(confirm != secret.name) .keyboardShortcut(.delete)
Button(action: { self.dismissalBlock(false) }) { Button("Don't Delete") {
Text("Don't Delete") dismissalBlock(false)
} }
.keyboardShortcut(.cancelAction)
} }
}.padding() }
.padding()
.frame(minWidth: 400)
.onExitCommand {
dismissalBlock(false)
}
} }
func delete() { func delete() {
try! store.delete(secret: secret) try! store.delete(secret: secret)
self.dismissalBlock(true) dismissalBlock(true)
} }
} }

View File

@ -1,6 +1,34 @@
import SwiftUI import SwiftUI
import SecretKit
struct EmptyStoreView: View { struct EmptyStoreView: View {
@ObservedObject var store: AnySecretStore
@Binding var activeSecret: AnySecret.ID?
var body: some View {
if store is AnySecretStoreModifiable {
NavigationLink(destination: EmptyStoreModifiableView(), tag: Constants.emptyStoreModifiableTag, selection: $activeSecret) {
Text("No Secrets")
}
} else {
NavigationLink(destination: EmptyStoreImmutableView(), tag: Constants.emptyStoreTag, selection: $activeSecret) {
Text("No Secrets")
}
}
}
}
extension EmptyStoreView {
enum Constants {
static let emptyStoreModifiableTag: AnyHashable = "emptyStoreModifiableTag"
static let emptyStoreTag: AnyHashable = "emptyStoreModifiableTag"
}
}
struct EmptyStoreImmutableView: View {
var body: some View { var body: some View {
VStack { VStack {
@ -25,14 +53,14 @@ struct EmptyStoreModifiableView: View {
CGPoint(x: g.size.width / 2, y: g.size.height * (1/2)), control2: CGPoint(x: g.size.width / 2, y: g.size.height * (1/2)), control2:
CGPoint(x: g.size.width * (3/4), y: g.size.height * (1/2))) CGPoint(x: g.size.width * (3/4), y: g.size.height * (1/2)))
path.addCurve(to: path.addCurve(to:
CGPoint(x: g.size.width, y: 0), control1: CGPoint(x: g.size.width - 13, y: 0), control1:
CGPoint(x: g.size.width, y: g.size.height * (1/2)), control2: CGPoint(x: g.size.width - 13 , y: g.size.height * (1/2)), control2:
CGPoint(x: g.size.width, y: 0)) CGPoint(x: g.size.width - 13, y: 0))
}.stroke(style: StrokeStyle(lineWidth: 5, lineCap: .round)) }.stroke(style: StrokeStyle(lineWidth: 5, lineCap: .round))
Path { path in Path { path in
path.move(to: CGPoint(x: g.size.width - 10, y: 0)) path.move(to: CGPoint(x: g.size.width - 23, y: 0))
path.addLine(to: CGPoint(x: g.size.width, y: -10)) path.addLine(to: CGPoint(x: g.size.width - 13, y: -10))
path.addLine(to: CGPoint(x: g.size.width + 10, y: 0)) path.addLine(to: CGPoint(x: g.size.width - 3, y: 0))
}.fill() }.fill()
}.frame(height: (windowGeometry.size.height/2) - 20).padding() }.frame(height: (windowGeometry.size.height/2) - 20).padding()
Text("No Secrets").bold() Text("No Secrets").bold()
@ -48,7 +76,7 @@ struct EmptyStoreModifiableView: View {
struct EmptyStoreModifiableView_Previews: PreviewProvider { struct EmptyStoreModifiableView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
Group { Group {
EmptyStoreView() EmptyStoreImmutableView()
EmptyStoreModifiableView() EmptyStoreModifiableView()
} }
} }

View File

@ -1,29 +1,23 @@
//
// NoStoresView.swift
// Secretive
//
// Created by Max Goedjen on 3/20/20.
// Copyright © 2020 Max Goedjen. All rights reserved.
//
import SwiftUI import SwiftUI
struct NoStoresView: View { struct NoStoresView: View {
var body: some View { var body: some View {
VStack { VStack {
Text("No Secure Storage Available").bold() Text("No Secure Storage Available").bold()
Text("Your Mac doesn't have a Secure Enclave, and there's not a compatible Smart Card inserted.") Text("Your Mac doesn't have a Secure Enclave, and there's not a compatible Smart Card inserted.")
Button(action: { Link("If you're looking to add one to your Mac, the YubiKey 5 Series are great.", destination: URL(string: "https://www.yubico.com/products/compare-yubikey-5-series/")!)
NSWorkspace.shared.open(URL(string: "https://www.yubico.com/products/compare-yubikey-5-series/")!)
}) {
Text("If you're looking to add one to your Mac, the YubiKey 5 Series are great.")
}
}.padding() }.padding()
} }
} }
#if DEBUG
struct NoStoresView_Previews: PreviewProvider { struct NoStoresView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
NoStoresView() NoStoresView()
} }
} }
#endif

View File

@ -1,57 +0,0 @@
import Foundation
import SwiftUI
struct NoticeView: View {
let text: String
let severity: Severity
let actionTitle: String?
let action: (() -> Void)?
var body: some View {
HStack {
Text(text).bold()
Spacer()
if action != nil {
Button(action: action!) {
Text(actionTitle!)
}
}
}.padding().background(color)
}
var color: Color {
switch severity {
case .advisory:
return Color.orange
case .critical:
return Color.red
}
}
}
extension NoticeView {
enum Severity {
case advisory, critical
}
}
#if DEBUG
struct NoticeView_Previews: PreviewProvider {
static var previews: some View {
Group {
NoticeView(text: "Agent Not Running", severity: .advisory, actionTitle: "Run Setup") {
print("OK")
}
NoticeView(text: "Critical Security Update Required", severity: .critical, actionTitle: "Update") {
print("OK")
}
}
}
}
#endif

View File

@ -0,0 +1,50 @@
import SwiftUI
import SecretKit
struct RenameSecretView<StoreType: SecretStoreModifiable>: View {
@ObservedObject var store: StoreType
let secret: StoreType.SecretType
var dismissalBlock: (_ renamed: Bool) -> ()
@State private var newName = ""
var body: some View {
VStack {
HStack {
Image(nsImage: NSApp.applicationIconImage)
.resizable()
.frame(width: 64, height: 64)
.padding()
VStack {
HStack {
Text("Type your new name for \"\(secret.name)\" below.")
Spacer()
}
HStack {
TextField(secret.name, text: $newName).focusable()
}
}
}
HStack {
Spacer()
Button("Rename", action: rename)
.disabled(newName.count == 0)
.keyboardShortcut(.return)
Button("Cancel") {
dismissalBlock(false)
}.keyboardShortcut(.cancelAction)
}
}
.padding()
.frame(minWidth: 400)
.onExitCommand {
dismissalBlock(false)
}
}
func rename() {
try? store.update(secret: secret, name: newName)
dismissalBlock(true)
}
}

View File

@ -2,58 +2,48 @@ import SwiftUI
import SecretKit import SecretKit
struct SecretDetailView<SecretType: Secret>: View { struct SecretDetailView<SecretType: Secret>: View {
@State var secret: SecretType @State var secret: SecretType
let keyWriter = OpenSSHKeyWriter()
private let keyWriter = OpenSSHKeyWriter()
var body: some View { var body: some View {
Form { Form {
Section { Section {
GroupBox(label: Text("Fingerprint")) { CopyableView(title: "SHA256 Fingerprint", image: Image(systemName: "touchid"), text: keyWriter.openSSHSHA256Fingerprint(secret: secret))
HStack { Spacer()
Text(keyWriter.openSSHFingerprint(secret: secret)) .frame(height: 20)
Spacer() CopyableView(title: "MD5 Fingerprint", image: Image(systemName: "touchid"), text: keyWriter.openSSHMD5Fingerprint(secret: secret))
} Spacer()
.frame(minWidth: 150, maxWidth: .infinity) .frame(height: 20)
.padding() CopyableView(title: "Public Key", image: Image(systemName: "key"), text: keyString)
}.onDrag {
return NSItemProvider(item: NSData(data: self.keyWriter.openSSHFingerprint(secret: self.secret).data(using: .utf8)!), typeIdentifier: kUTTypeUTF8PlainText as String)
}
Spacer().frame(height: 10)
GroupBox(label: Text("Public Key")) {
VStack {
Text(keyWriter.openSSHString(secret: secret))
.multilineTextAlignment(.leading)
.frame(minWidth: 150, maxWidth: .infinity)
HStack {
Spacer()
Button(action: copy) {
Text("Copy")
}
}
}
.padding()
}
.onDrag {
return NSItemProvider(item: NSData(data: self.keyString.data(using: .utf8)!), typeIdentifier: kUTTypeUTF8PlainText as String)
}
Spacer() Spacer()
} }
}.padding() }
.frame(minHeight: 150, maxHeight: .infinity) .padding()
.frame(minHeight: 200, maxHeight: .infinity)
} }
var dashedKeyName: String {
secret.name.replacingOccurrences(of: " ", with: "-")
}
var dashedHostName: String {
["secretive", Host.current().localizedName, "local"]
.compactMap { $0 }
.joined(separator: ".")
.replacingOccurrences(of: " ", with: "-")
}
var keyString: String { var keyString: String {
keyWriter.openSSHString(secret: secret) keyWriter.openSSHString(secret: secret, comment: "\(dashedKeyName)@\(dashedHostName)")
} }
func copy() { func copy() {
NSPasteboard.general.declareTypes([.string], owner: nil) NSPasteboard.general.declareTypes([.string], owner: nil)
NSPasteboard.general.setString(keyString, forType: .string) NSPasteboard.general.setString(keyString, forType: .string)
} }
} }
#if DEBUG #if DEBUG

View File

@ -0,0 +1,54 @@
import SwiftUI
import SecretKit
struct SecretListItemView: View {
@ObservedObject var store: AnySecretStore
var secret: AnySecret
@Binding var activeSecret: AnySecret.ID?
@State var isDeleting: Bool = false
@State var isRenaming: Bool = false
var deletedSecret: (AnySecret) -> Void
var renamedSecret: (AnySecret) -> Void
var body: some View {
let showingPopupWrapped = Binding(
get: { isDeleting || isRenaming },
set: { if $0 == false { isDeleting = false; isRenaming = false } }
)
return NavigationLink(destination: SecretDetailView(secret: secret), tag: secret.id, selection: $activeSecret) {
Text(secret.name)
}.contextMenu {
if store is AnySecretStoreModifiable {
Button(action: { isRenaming = true }) {
Text("Rename")
}
Button(action: { isDeleting = true }) {
Text("Delete")
}
}
}
.popover(isPresented: showingPopupWrapped) {
if let modifiable = store as? AnySecretStoreModifiable {
if isDeleting {
DeleteSecretView(store: modifiable, secret: secret) { deleted in
isDeleting = false
if deleted {
deletedSecret(secret)
}
}
} else if isRenaming {
RenameSecretView(store: modifiable, secret: secret) { renamed in
isRenaming = false
if renamed {
renamedSecret(secret)
}
}
}
}
}
}
}

View File

@ -1,145 +1,296 @@
import Foundation
import SwiftUI import SwiftUI
struct SetupView: View { struct SetupView: View {
var completion: ((Bool) -> Void)? @State var stepIndex = 0
@Binding var visible: Bool
@Binding var setupComplete: Bool
var body: some View { var body: some View {
Form { GeometryReader { proxy in
SetupStepView<Spacer>(text: "Secretive needs to install a helper app to sign requests when the main app isn't running. This app is called \"SecretAgent\" and you might see it in Activity Manager from time to time.", VStack {
index: 1, StepView(numberOfSteps: 3, currentStep: stepIndex, width: proxy.size.width)
nestedView: nil, GeometryReader { _ in
actionText: "Install") { HStack(spacing: 0) {
self.installLaunchAgent() SecretAgentSetupView(buttonAction: advance)
} .frame(width: proxy.size.width)
SetupStepView(text: "Add this line to your shell config (.bashrc or .zshrc) telling SSH to talk to SecretAgent when it wants to authenticate. Drag this into your config file.", SSHAgentSetupView(buttonAction: advance)
index: 2, .frame(width: proxy.size.width)
nestedView: SetupStepCommandView(text: Constants.socketPrompt), UpdaterExplainerView {
actionText: "Added") { visible = false
self.markAsDone() setupComplete = true
} }
HStack { .frame(width: proxy.size.width)
Spacer() }
Button(action: { self.completion?(true) }) { .offset(x: -proxy.size.width * CGFloat(stepIndex), y: 0)
Text("Finish")
} }
.padding()
} }
}.frame(minWidth: 640, minHeight: 400) }
.frame(idealWidth: 500, idealHeight: 500)
} }
func advance() {
withAnimation(.spring()) {
stepIndex += 1
}
}
} }
struct SetupStepView<NestedViewType: View>: View { struct StepView: View {
let text: String let numberOfSteps: Int
let index: Int let currentStep: Int
let nestedView: NestedViewType?
@State var completed = false // Ideally we'd have a geometry reader inside this view doing this for us, but that crashes on 11.0b7
let actionText: String let width: CGFloat
let action: (() -> Bool)
var body: some View { var body: some View {
Section { ZStack(alignment: .leading) {
Rectangle()
.foregroundColor(.blue)
.frame(height: 5)
Rectangle()
.foregroundColor(.green)
.frame(width: max(0, ((width - (Constants.padding * 2)) / CGFloat(numberOfSteps - 1)) * CGFloat(currentStep) - (Constants.circleWidth / 2)), height: 5)
.animation(.spring())
HStack { HStack {
ZStack { ForEach(0..<numberOfSteps) { index in
if completed { ZStack {
Circle().foregroundColor(.green) if currentStep > index {
.frame(width: 30, height: 30) Circle()
Text("") .foregroundColor(.green)
.foregroundColor(.white) .frame(width: Constants.circleWidth, height: Constants.circleWidth)
.bold() Text("")
} else { .foregroundColor(.white)
Circle().foregroundColor(.blue) .bold()
.frame(width: 30, height: 30) } else {
Text(String(describing: index)) Circle()
.foregroundColor(.white) .foregroundColor(.blue)
.bold() .frame(width: Constants.circleWidth, height: Constants.circleWidth)
if currentStep == index {
Circle()
.strokeBorder(Color.white, lineWidth: 3)
.frame(width: Constants.circleWidth, height: Constants.circleWidth)
}
Text(String(describing: index + 1))
.foregroundColor(.white)
.bold()
}
}
if index < numberOfSteps - 1 {
Spacer(minLength: 30)
} }
} }
.padding()
VStack {
Text(text)
.opacity(completed ? 0.5 : 1)
.lineLimit(nil)
if nestedView != nil {
nestedView!.padding()
}
}
.padding()
Button(action: {
self.completed = self.action()
}) {
Text(actionText)
}.disabled(completed)
.padding()
} }
} }.padding(Constants.padding)
} }
} }
struct SetupStepCommandView: View { extension StepView {
let text: String
var body: some View {
VStack(alignment: .leading) {
Text(text)
.lineLimit(nil)
.font(.system(.caption, design: .monospaced))
.multilineTextAlignment(.leading)
.frame(minHeight: 50)
HStack {
Spacer()
Button(action: copy) {
Text("Copy")
}
}
}
.padding()
.background(Color(white: 0, opacity: 0.10))
.cornerRadius(10)
.onDrag {
return NSItemProvider(item: NSData(data: self.text.data(using: .utf8)!), typeIdentifier: kUTTypeUTF8PlainText as String)
}
}
func copy() {
NSPasteboard.general.declareTypes([.string], owner: nil)
NSPasteboard.general.setString(text, forType: .string)
}
}
extension SetupView {
func installLaunchAgent() -> Bool {
LaunchAgentController().install()
}
func markAsDone() -> Bool {
return true
}
}
extension SetupView {
enum Constants { enum Constants {
static let socketPath = (NSHomeDirectory().replacingOccurrences(of: "com.maxgoedjen.Secretive.Host", with: "com.maxgoedjen.Secretive.SecretAgent") as NSString).appendingPathComponent("socket.ssh") as String
static let socketPrompt = "export SSH_AUTH_SOCK=\(socketPath)" static let padding: CGFloat = 15
static let circleWidth: CGFloat = 30
} }
}
struct SetupStepView<Content> : View where Content : View {
let title: String
let image: Image
let bodyText: String
let buttonTitle: String
let buttonAction: () -> Void
let content: Content
init(title: String, image: Image, bodyText: String, buttonTitle: String, buttonAction: @escaping () -> Void = {}, @ViewBuilder content: () -> Content) {
self.title = title
self.image = image
self.bodyText = bodyText
self.buttonTitle = buttonTitle
self.buttonAction = buttonAction
self.content = content()
}
var body: some View {
VStack {
Text(title)
.font(.title)
Spacer()
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 64)
Spacer()
Text(bodyText)
.multilineTextAlignment(.center)
Spacer()
content
Spacer()
Button(buttonTitle) {
buttonAction()
}
}.padding()
}
}
struct SecretAgentSetupView: View {
let buttonAction: () -> Void
var body: some View {
SetupStepView(title: "Setup Secret Agent",
image: Image(nsImage: NSApp.applicationIconImage),
bodyText: "Secretive needs to set up a helper app to work properly. It will sign requests from SSH clients in the background, so you don't need to keep the main Secretive app open.",
buttonTitle: "Install",
buttonAction: install) {
(Text("This helper app is called ") + Text("Secret Agent").bold().underline() + Text(" and you may see it in Activity Manager from time to time."))
.multilineTextAlignment(.center)
}
}
func install() {
LaunchAgentController().install()
buttonAction()
}
}
struct SSHAgentSetupView: View {
let buttonAction: () -> Void
private static let controller = ShellConfigurationController()
@State private var selectedShellInstruction: ShellConfigInstruction = controller.shellInstructions.first!
var body: some View {
SetupStepView(title: "Configure your SSH Agent",
image: Image(systemName: "terminal"),
bodyText: "Add this line to your shell config telling SSH to talk to Secret Agent when it wants to authenticate. Secretive can either do this for you automatically, or you can copy and paste this into your config file.",
buttonTitle: "I Added it Manually",
buttonAction: buttonAction) {
Link("If you're trying to set up a third party app, check out the FAQ.", destination: URL(string: "https://github.com/maxgoedjen/secretive/blob/main/APP_CONFIG.md")!)
Picker(selection: $selectedShellInstruction, label: EmptyView()) {
ForEach(SSHAgentSetupView.controller.shellInstructions) { instruction in
Text(instruction.shell)
.tag(instruction)
.padding()
}
}.pickerStyle(SegmentedPickerStyle())
CopyableView(title: "Add to \(selectedShellInstruction.shellConfigPath)", image: Image(systemName: "greaterthan.square"), text: selectedShellInstruction.text)
Button("Add it For Me") {
let controller = ShellConfigurationController()
if controller.addToShell(shellInstructions: selectedShellInstruction) {
buttonAction()
}
}
}
}
}
class Delegate: NSObject, NSOpenSavePanelDelegate {
private let name: String
init(name: String) {
self.name = name
}
func panel(_ sender: Any, shouldEnable url: URL) -> Bool {
return url.lastPathComponent == name
}
}
struct UpdaterExplainerView: View {
let buttonAction: () -> Void
var body: some View {
SetupStepView(title: "Updates",
image: Image(systemName: "dot.radiowaves.left.and.right"),
bodyText: "Secretive will periodically check with GitHub to see if there's a new release. If you see any network requests to GitHub, that's why.",
buttonTitle: "Okay",
buttonAction: buttonAction) {
Link("Read more about this here.", destination: SetupView.Constants.updaterFAQURL)
}
}
}
extension SetupView {
enum Constants {
static let updaterFAQURL = URL(string: "https://github.com/maxgoedjen/secretive/blob/main/FAQ.md#whats-this-network-request-to-github")!
}
}
struct ShellConfigInstruction: Identifiable, Hashable {
var shell: String
var shellConfigDirectory: String
var shellConfigFilename: String
var text: String
var id: String {
shell
}
var shellConfigPath: String {
return (shellConfigDirectory as NSString).appendingPathComponent(shellConfigFilename)
}
} }
#if DEBUG #if DEBUG
struct SetupView_Previews: PreviewProvider { struct SetupView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
SetupView() Group {
SetupView(visible: .constant(true), setupComplete: .constant(false))
}
} }
}
struct SecretAgentSetupView_Previews: PreviewProvider {
static var previews: some View {
Group {
SecretAgentSetupView(buttonAction: {})
}
}
}
struct SSHAgentSetupView_Previews: PreviewProvider {
static var previews: some View {
Group {
SSHAgentSetupView(buttonAction: {})
}
}
}
struct UpdaterExplainerView_Previews: PreviewProvider {
static var previews: some View {
Group {
UpdaterExplainerView(buttonAction: {})
}
}
} }
#endif #endif

View File

@ -0,0 +1,64 @@
import SwiftUI
import SecretKit
struct StoreListView: View {
@Binding var showingCreation: Bool
@State private var activeSecret: AnySecret.ID?
@EnvironmentObject private var storeList: SecretStoreList
private func secretDeleted(secret: AnySecret) {
activeSecret = nextDefaultSecret
}
private func secretRenamed(secret: AnySecret) {
activeSecret = nextDefaultSecret
}
var body: some View {
NavigationView {
List(selection: $activeSecret) {
ForEach(storeList.stores) { store in
if store.isAvailable {
Section(header: Text(store.name)) {
if store.secrets.isEmpty {
EmptyStoreView(store: store, activeSecret: $activeSecret)
} else {
ForEach(store.secrets) { secret in
SecretListItemView(
store: store,
secret: secret,
activeSecret: $activeSecret,
deletedSecret: self.secretDeleted,
renamedSecret: self.secretRenamed
)
}
}
}
}
}
}
.listStyle(SidebarListStyle())
.onAppear {
activeSecret = nextDefaultSecret
}
.frame(minWidth: 100, idealWidth: 240)
}
}
}
extension StoreListView {
var nextDefaultSecret: AnyHashable? {
let fallback: AnyHashable
if storeList.modifiableStore?.isAvailable ?? false {
fallback = EmptyStoreView.Constants.emptyStoreModifiableTag
} else {
fallback = EmptyStoreView.Constants.emptyStoreTag
}
return storeList.stores.compactMap(\.secrets.first).first?.id ?? fallback
}
}

View File

@ -0,0 +1,61 @@
import SwiftUI
import Brief
struct UpdateDetailView<UpdaterType: Updater>: View {
@EnvironmentObject var updater: UpdaterType
let update: Release
var body: some View {
VStack {
Text("Secretive \(update.name)").font(.title)
GroupBox(label: Text("Release Notes")) {
ScrollView {
attributedBody
}
}
HStack {
if !update.critical {
Button("Ignore") {
updater.ignore(release: update)
}
Spacer()
}
Button("Update") {
NSWorkspace.shared.open(update.html_url)
}
.keyboardShortcut(.defaultAction)
}
}
.padding()
.frame(maxWidth: 500)
}
var attributedBody: Text {
var text = Text("")
for line in update.body.split(whereSeparator: \.isNewline) {
let attributed: Text
let split = line.split(separator: " ")
let unprefixed = split.dropFirst().joined()
if let prefix = split.first {
switch prefix {
case "#":
attributed = Text(unprefixed).font(.title) + Text("\n")
case "##":
attributed = Text(unprefixed).font(.title2) + Text("\n")
case "###":
attributed = Text(unprefixed).font(.title3) + Text("\n")
default:
attributed = Text(line) + Text("\n\n")
}
} else {
attributed = Text(line) + Text("\n\n")
}
text = text + attributed
}
return text
}
}