mirror of
https://github.com/maxgoedjen/secretive.git
synced 2025-07-02 02:03:36 +00:00
Compare commits
19 Commits
Author | SHA1 | Date | |
---|---|---|---|
9d6bbd2a04 | |||
35aea0729d | |||
c5bd4c2189 | |||
8114acf50a | |||
cd965b9ec6 | |||
f30d1f802f | |||
af479408a4 | |||
42b034270a | |||
dca340ad40 | |||
8dfabd35aa | |||
9c28efab5c | |||
698a69a034 | |||
4105c1d6f6 | |||
4de805dd37 | |||
0544287141 | |||
8ec19d1fb7 | |||
6c3748b6bf | |||
dc91f61dba | |||
702388fdf8 |
BIN
.github/readme/app.png
vendored
BIN
.github/readme/app.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 338 KiB After Width: | Height: | Size: 456 KiB |
5
.github/scripts/signing.sh
vendored
5
.github/scripts/signing.sh
vendored
@ -10,10 +10,13 @@ security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k ci ci.keyc
|
||||
|
||||
# Import Profiles
|
||||
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
|
||||
|
||||
echo $HOST_PROFILE_DATA | base64 -d -o Host.provisionprofile
|
||||
HOST_UUID=`grep UUID -A1 -a Host.provisionprofile | grep -io "[-A-F0-9]\{36\}"`
|
||||
cp Host.provisionprofile ~/Library/MobileDevice/Provisioning\ Profiles/$HOST_UUID.provisionprofile
|
||||
echo $AGENT_PROFILE_DATA | base64 -d -o Agent.provisionprofile
|
||||
AGENT_UUID=`grep UUID -A1 -a Agent.provisionprofile | grep -io "[-A-F0-9]\{36\}"`
|
||||
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
|
||||
|
49
.github/workflows/release.yml
vendored
49
.github/workflows/release.yml
vendored
@ -16,9 +16,11 @@ jobs:
|
||||
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
|
||||
HOST_PROFILE_DATA: ${{ secrets.HOST_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
|
||||
- name: Set Environment
|
||||
run: sudo xcrun xcode-select -s /Applications/Xcode_12.2.app
|
||||
run: sudo xcrun xcode-select -s /Applications/Xcode_12.5.1.app
|
||||
- name: Test
|
||||
run: xcrun xcodebuild test -project Secretive.xcodeproj -scheme Secretive
|
||||
build:
|
||||
@ -26,26 +28,17 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- 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\nhttps://github.com/maxgoedjen/secretive/actions/runs/${{ github.run_id }}"
|
||||
draft: true
|
||||
prerelease: false
|
||||
- name: Setup Signing
|
||||
env:
|
||||
SIGNING_DATA: ${{ secrets.SIGNING_DATA }}
|
||||
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
|
||||
HOST_PROFILE_DATA: ${{ secrets.HOST_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
|
||||
- name: Set Environment
|
||||
run: sudo xcrun xcode-select -s /Applications/Xcode_12.2.app
|
||||
run: sudo xcrun xcode-select -s /Applications/Xcode_12.5.1.app
|
||||
- name: Update Build Number
|
||||
env:
|
||||
TAG_NAME: ${{ github.ref }}
|
||||
@ -63,13 +56,37 @@ jobs:
|
||||
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive ./Archive.zip
|
||||
- name: Notarize
|
||||
env:
|
||||
APPLE_USERNAME: ${{ secrets.APPLE_USERNAME }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
run: xcrun altool --notarize-app --primary-bundle-id "com.maxgoedjen.secretive.host" --username $APPLE_USERNAME --password $APPLE_PASSWORD --file Secretive.zip
|
||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||
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
|
||||
run: |
|
||||
shasum -a 512 Secretive.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
|
||||
id: upload-release-asset
|
||||
uses: actions/upload-release-asset@v1.0.1
|
||||
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -8,6 +8,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set Environment
|
||||
run: sudo xcrun xcode-select -s /Applications/Xcode_12.2.app
|
||||
run: sudo xcrun xcode-select -s /Applications/Xcode_12.5.1.app
|
||||
- name: Test
|
||||
run: xcrun xcodebuild test -project Secretive.xcodeproj -scheme Secretive
|
||||
|
@ -11,7 +11,10 @@ public class Updater: ObservableObject, UpdaterProtocol {
|
||||
|
||||
@Published public var update: Release?
|
||||
|
||||
public init(checkOnLaunch: Bool) {
|
||||
private let osVersion: SemVer
|
||||
|
||||
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()
|
||||
@ -25,8 +28,8 @@ public class Updater: ObservableObject, UpdaterProtocol {
|
||||
public func checkForUpdates() {
|
||||
URLSession.shared.dataTask(with: Constants.updateURL) { data, _, _ in
|
||||
guard let data = data else { return }
|
||||
guard let release = try? JSONDecoder().decode(Release.self, from: data) else { return }
|
||||
self.evaluate(release: release)
|
||||
guard let releases = try? JSONDecoder().decode([Release].self, from: data) else { return }
|
||||
self.evaluate(releases: releases)
|
||||
}.resume()
|
||||
}
|
||||
|
||||
@ -42,11 +45,16 @@ public class Updater: ObservableObject, UpdaterProtocol {
|
||||
|
||||
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 !release.prerelease else { return }
|
||||
let latestVersion = SemVer(release.name)
|
||||
let currentVersion = SemVer(Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String)
|
||||
let currentVersion = SemVer(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0")
|
||||
if latestVersion > currentVersion {
|
||||
DispatchQueue.main.async {
|
||||
self.update = release
|
||||
@ -64,11 +72,11 @@ extension Updater {
|
||||
}
|
||||
}
|
||||
|
||||
struct SemVer {
|
||||
public struct SemVer {
|
||||
|
||||
let versionNumbers: [Int]
|
||||
|
||||
init(_ version: String) {
|
||||
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) }
|
||||
@ -78,11 +86,15 @@ struct SemVer {
|
||||
versionNumbers = split
|
||||
}
|
||||
|
||||
public init(_ version: OperatingSystemVersion) {
|
||||
versionNumbers = [version.majorVersion, version.minorVersion, version.patchVersion]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SemVer: Comparable {
|
||||
|
||||
static func < (lhs: SemVer, rhs: SemVer) -> Bool {
|
||||
public static func < (lhs: SemVer, rhs: SemVer) -> Bool {
|
||||
for (latest, current) in zip(lhs.versionNumbers, rhs.versionNumbers) {
|
||||
if latest < current {
|
||||
return true
|
||||
@ -99,7 +111,7 @@ extension SemVer: Comparable {
|
||||
extension Updater {
|
||||
|
||||
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")!
|
||||
}
|
||||
|
||||
}
|
||||
@ -128,12 +140,32 @@ extension Release: Identifiable {
|
||||
|
||||
}
|
||||
|
||||
extension Release: Comparable {
|
||||
|
||||
public static func < (lhs: Release, rhs: Release) -> Bool {
|
||||
lhs.version < rhs.version
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Release {
|
||||
|
||||
public var critical: Bool {
|
||||
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]))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Release {
|
||||
|
104
BriefTests/ReleaseParsingTests.swift
Normal file
104
BriefTests/ReleaseParsingTests.swift
Normal 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)
|
||||
}
|
||||
|
||||
}
|
@ -27,6 +27,21 @@ class SemVerTests: XCTestCase {
|
||||
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")
|
@ -14,6 +14,14 @@ Secretive is designed to be easily auditable by people who are considering using
|
||||
|
||||
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
|
||||
|
||||
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).
|
||||
|
4
FAQ.md
4
FAQ.md
@ -24,6 +24,10 @@ Please run `ssh -Tv git@github.com` in your terminal and paste the output in a [
|
||||

|
||||

|
||||
|
||||
### 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?
|
||||
|
||||
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).
|
||||
|
BIN
Icon.sketch
BIN
Icon.sketch
Binary file not shown.
@ -38,7 +38,7 @@ You can download the latest release over on the [Releases Page](https://github.c
|
||||
|
||||
#### Using Homebrew
|
||||
|
||||
brew cask install secretive
|
||||
brew install secretive
|
||||
|
||||
### FAQ
|
||||
|
||||
|
31
SecretAgent/InternetAccessPolicy.plist
Normal file
31
SecretAgent/InternetAccessPolicy.plist
Normal 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'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>
|
7
SecretKit/Common/BundleIDs.swift
Normal file
7
SecretKit/Common/BundleIDs.swift
Normal 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"))!}
|
||||
}
|
@ -49,10 +49,12 @@ public class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable {
|
||||
|
||||
private let _create: (String, Bool) throws -> Void
|
||||
private let _delete: (AnySecret) throws -> Void
|
||||
private let _update: (AnySecret, String) throws -> Void
|
||||
|
||||
public init<SecretStoreType>(modifiable secretStore: SecretStoreType) where SecretStoreType: SecretStoreModifiable {
|
||||
_create = { try secretStore.create(name: $0, requiresAuthentication: $1) }
|
||||
_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)
|
||||
}
|
||||
|
||||
@ -64,4 +66,7 @@ public class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable {
|
||||
try _delete(secret)
|
||||
}
|
||||
|
||||
public func update(secret: AnySecret, name: String) throws {
|
||||
try _update(secret, name)
|
||||
}
|
||||
}
|
||||
|
@ -13,11 +13,21 @@ public struct OpenSSHKeyWriter {
|
||||
lengthAndData(of: secret.publicKey)
|
||||
}
|
||||
|
||||
public func openSSHString<SecretType: Secret>(secret: SecretType) -> String {
|
||||
"\(curveType(for: secret.algorithm, length: secret.keySize)) \(data(secret: secret).base64EncodedString())"
|
||||
public func openSSHString<SecretType: Secret>(secret: SecretType, comment: String? = nil) -> String {
|
||||
[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))
|
||||
.compactMap { ("0" + String($0, radix: 16, uppercase: false)).suffix(2) }
|
||||
.joined(separator: ":")
|
||||
|
@ -17,6 +17,7 @@ public protocol SecretStoreModifiable: SecretStore {
|
||||
|
||||
func create(name: String, requiresAuthentication: Bool) throws
|
||||
func delete(secret: SecretType) throws
|
||||
func update(secret: SecretType, name: String) throws
|
||||
|
||||
}
|
||||
|
||||
|
@ -76,6 +76,23 @@ extension SecureEnclave {
|
||||
reloadSecrets()
|
||||
}
|
||||
|
||||
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)\""
|
||||
|
@ -6,8 +6,12 @@ class OpenSSHWriterTests: XCTestCase {
|
||||
|
||||
let writer = OpenSSHKeyWriter()
|
||||
|
||||
func testECDSA256Fingerprint() {
|
||||
XCTAssertEqual(writer.openSSHFingerprint(secret: Constants.ecdsa256Secret), "dc:60:4d:ff:c2:d9:18:8b:2f:24:40:b5:7f:43:47:e5")
|
||||
func testECDSA256MD5Fingerprint() {
|
||||
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() {
|
||||
@ -19,8 +23,12 @@ class OpenSSHWriterTests: XCTestCase {
|
||||
XCTAssertEqual(writer.data(secret: Constants.ecdsa256Secret), Data(base64Encoded: "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOVEjgAA5PHqRgwykjN5qM21uWCHFSY/Sqo5gkHAkn+e1MMQKHOLga7ucB9b3mif33MBid59GRK9GEPVlMiSQwo="))
|
||||
}
|
||||
|
||||
func testECDSA384Fingerprint() {
|
||||
XCTAssertEqual(writer.openSSHFingerprint(secret: Constants.ecdsa384Secret), "66:e0:66:d7:41:ed:19:8e:e2:20:df:ce:ac:7e:2b:6e")
|
||||
func testECDSA384MD5Fingerprint() {
|
||||
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() {
|
||||
|
@ -7,9 +7,10 @@
|
||||
objects = {
|
||||
|
||||
/* 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 */; };
|
||||
50153E20250AFCB200525160 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E1F250AFCB200525160 /* UpdateView.swift */; };
|
||||
50153E22250DECA300525160 /* SecretListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListView.swift */; };
|
||||
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.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 */; };
|
||||
@ -61,8 +62,10 @@
|
||||
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, ); }; };
|
||||
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 /* BriefTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5091D3212519D56D0049FD9B /* BriefTests.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 */; };
|
||||
5099A02723FE34FA0062B6F2 /* SmartCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02623FE34FA0062B6F2 /* SmartCard.swift */; };
|
||||
@ -73,6 +76,7 @@
|
||||
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, ); }; };
|
||||
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 */; };
|
||||
50A3B79424026B7600D209EA /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50A3B79324026B7600D209EA /* Preview Assets.xcassets */; };
|
||||
50A3B79724026B7600D209EA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50A3B79524026B7600D209EA /* Main.storyboard */; };
|
||||
@ -84,6 +88,7 @@
|
||||
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */; };
|
||||
50C385A3240789E600AF2719 /* OpenSSHReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A2240789E600AF2719 /* OpenSSHReader.swift */; };
|
||||
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A42407A76D00AF2719 /* SecretDetailView.swift */; };
|
||||
FA0B34672599619E0013AB3A /* BundleIDs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA0B34662599619E0013AB3A /* BundleIDs.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@ -218,9 +223,10 @@
|
||||
/* End PBXCopyFilesBuildPhase 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>"; };
|
||||
50153E1F250AFCB200525160 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = "<group>"; };
|
||||
50153E21250DECA300525160 /* SecretListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListView.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>"; };
|
||||
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>"; };
|
||||
@ -274,9 +280,11 @@
|
||||
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>"; };
|
||||
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 /* BriefTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BriefTests.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
5099A02623FE34FA0062B6F2 /* SmartCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartCard.swift; sourceTree = "<group>"; };
|
||||
@ -290,6 +298,7 @@
|
||||
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>"; };
|
||||
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; };
|
||||
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>"; };
|
||||
@ -302,6 +311,7 @@
|
||||
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; };
|
||||
50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = "<group>"; };
|
||||
FA0B34662599619E0013AB3A /* BundleIDs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIDs.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -430,6 +440,7 @@
|
||||
508A58B1241ED1EA0069DC07 /* Controllers */,
|
||||
50617D8623FCE48E0099B055 /* Assets.xcassets */,
|
||||
50617D8E23FCE48E0099B055 /* Info.plist */,
|
||||
508BF28D25B4F005009EFB7E /* InternetAccessPolicy.plist */,
|
||||
50617D8F23FCE48E0099B055 /* Secretive.entitlements */,
|
||||
506772C62424784600034DED /* Credits.rtf */,
|
||||
50617D8823FCE48E0099B055 /* Preview Content */,
|
||||
@ -532,10 +543,11 @@
|
||||
children = (
|
||||
50617D8423FCE48E0099B055 /* ContentView.swift */,
|
||||
5079BA0E250F29BF00EA86F4 /* StoreListView.swift */,
|
||||
50153E21250DECA300525160 /* SecretListView.swift */,
|
||||
50153E21250DECA300525160 /* SecretListItemView.swift */,
|
||||
50C385A42407A76D00AF2719 /* SecretDetailView.swift */,
|
||||
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */,
|
||||
50B8550C24138C4F009958AC /* DeleteSecretView.swift */,
|
||||
2C4A9D2E2636FFD3008CC8E2 /* RenameSecretView.swift */,
|
||||
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */,
|
||||
506772C82425BB8500034DED /* NoStoresView.swift */,
|
||||
50153E1F250AFCB200525160 /* UpdateView.swift */,
|
||||
@ -560,7 +572,8 @@
|
||||
5091D3202519D56D0049FD9B /* BriefTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5091D3212519D56D0049FD9B /* BriefTests.swift */,
|
||||
5091D3212519D56D0049FD9B /* SemVerTests.swift */,
|
||||
509FA3B525B53C49005E2535 /* ReleaseParsingTests.swift */,
|
||||
5091D3232519D56D0049FD9B /* Info.plist */,
|
||||
);
|
||||
path = BriefTests;
|
||||
@ -583,6 +596,7 @@
|
||||
5068389F2415EA4F00F55094 /* Erasers */,
|
||||
506838A42415EA6800F55094 /* OpenSSH */,
|
||||
5068389D241471CD00F55094 /* SecretStoreList.swift */,
|
||||
FA0B34662599619E0013AB3A /* BundleIDs.swift */,
|
||||
);
|
||||
path = Common;
|
||||
sourceTree = "<group>";
|
||||
@ -630,6 +644,7 @@
|
||||
50A3B79024026B7600D209EA /* Assets.xcassets */,
|
||||
50A3B79524026B7600D209EA /* Main.storyboard */,
|
||||
50A3B79824026B7600D209EA /* Info.plist */,
|
||||
508BF29425B4F140009EFB7E /* InternetAccessPolicy.plist */,
|
||||
50A3B79924026B7600D209EA /* SecretAgent.entitlements */,
|
||||
50A3B79224026B7600D209EA /* Preview Content */,
|
||||
);
|
||||
@ -922,6 +937,7 @@
|
||||
50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */,
|
||||
50617D8723FCE48E0099B055 /* Assets.xcassets in Resources */,
|
||||
506772C72424784600034DED /* Credits.rtf in Resources */,
|
||||
508BF28E25B4F005009EFB7E /* InternetAccessPolicy.plist in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -981,6 +997,7 @@
|
||||
50A3B79724026B7600D209EA /* Main.storyboard in Resources */,
|
||||
50A3B79424026B7600D209EA /* Preview Assets.xcassets in Resources */,
|
||||
50A3B79124026B7600D209EA /* Assets.xcassets in Resources */,
|
||||
508BF2AA25B4F1CB009EFB7E /* InternetAccessPolicy.plist in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -991,6 +1008,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
2C4A9D2F2636FFD3008CC8E2 /* RenameSecretView.swift in Sources */,
|
||||
5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */,
|
||||
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */,
|
||||
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */,
|
||||
@ -1008,7 +1026,7 @@
|
||||
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */,
|
||||
50617D8323FCE48E0099B055 /* App.swift in Sources */,
|
||||
506772C92425BB8500034DED /* NoStoresView.swift in Sources */,
|
||||
50153E22250DECA300525160 /* SecretListView.swift in Sources */,
|
||||
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */,
|
||||
508A58B5241ED48F0069DC07 /* PreviewAgentStatusChecker.swift in Sources */,
|
||||
508A58AA241E06B40069DC07 /* PreviewUpdater.swift in Sources */,
|
||||
);
|
||||
@ -1026,6 +1044,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
FA0B34672599619E0013AB3A /* BundleIDs.swift in Sources */,
|
||||
501B7AE1251C56F700776EC7 /* SigningRequestProvenance.swift in Sources */,
|
||||
50617DC723FCE4EA0099B055 /* SecretStore.swift in Sources */,
|
||||
5099A02723FE34FA0062B6F2 /* SmartCard.swift in Sources */,
|
||||
@ -1065,7 +1084,8 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5091D3222519D56D0049FD9B /* BriefTests.swift in Sources */,
|
||||
509FA3B625B53C49005E2535 /* ReleaseParsingTests.swift in Sources */,
|
||||
5091D3222519D56D0049FD9B /* SemVerTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
import AppKit
|
||||
import SecretKit
|
||||
|
||||
protocol AgentStatusCheckerProtocol: ObservableObject {
|
||||
var running: Bool { get }
|
||||
@ -20,7 +21,7 @@ class AgentStatusChecker: ObservableObject, AgentStatusCheckerProtocol {
|
||||
|
||||
// All processes, including ones from older versions, etc
|
||||
var secretAgentProcesses: [NSRunningApplication] {
|
||||
NSRunningApplication.runningApplications(withBundleIdentifier: Constants.secretAgentAppID)
|
||||
NSRunningApplication.runningApplications(withBundleIdentifier: Bundle.main.agentBundleID)
|
||||
}
|
||||
|
||||
// The process corresponding to this instance of Secretive
|
||||
@ -37,10 +38,4 @@ class AgentStatusChecker: ObservableObject, AgentStatusCheckerProtocol {
|
||||
|
||||
}
|
||||
|
||||
extension AgentStatusChecker {
|
||||
|
||||
enum Constants {
|
||||
static let secretAgentAppID = "com.maxgoedjen.Secretive.SecretAgent"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ extension ApplicationDirectoryController {
|
||||
|
||||
var isInApplicationsDirectory: Bool {
|
||||
let bundlePath = Bundle.main.bundlePath
|
||||
for directory in NSSearchPathForDirectoriesInDomains(.applicationDirectory, .allDomainsMask, true) {
|
||||
for directory in NSSearchPathForDirectoriesInDomains(.allApplicationsDirectory, .allDomainsMask, true) {
|
||||
if bundlePath.hasPrefix(directory) {
|
||||
return true
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import Foundation
|
||||
import ServiceManagement
|
||||
import AppKit
|
||||
import OSLog
|
||||
import SecretKit
|
||||
|
||||
struct LaunchAgentController {
|
||||
|
||||
@ -21,8 +22,12 @@ struct LaunchAgentController {
|
||||
func forceLaunch(completion: ((Bool) -> Void)?) {
|
||||
Logger().debug("Agent is not running, attempting to force launch")
|
||||
let url = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LoginItems/SecretAgent.app")
|
||||
NSWorkspace.shared.openApplication(at: url, configuration: NSWorkspace.OpenConfiguration()) { app, error in
|
||||
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 {
|
||||
@ -32,7 +37,7 @@ struct LaunchAgentController {
|
||||
}
|
||||
|
||||
private func setEnabled(_ enabled: Bool) -> Bool {
|
||||
SMLoginItemSetEnabled("com.maxgoedjen.Secretive.SecretAgent" as CFString, enabled)
|
||||
SMLoginItemSetEnabled(Bundle.main.agentBundleID as CFString, enabled)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,12 +1,17 @@
|
||||
import Foundation
|
||||
import Cocoa
|
||||
import SecretKit
|
||||
|
||||
struct ShellConfigurationController {
|
||||
|
||||
let socketPath = (NSHomeDirectory().replacingOccurrences(of: "com.maxgoedjen.Secretive.Host", with: "com.maxgoedjen.Secretive.SecretAgent") as NSString).appendingPathComponent("socket.ssh") as String
|
||||
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",
|
||||
|
@ -1,4 +1,4 @@
|
||||
{\rtf1\ansi\ansicpg1252\cocoartf2511
|
||||
{\rtf1\ansi\ansicpg1252\cocoartf2580
|
||||
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
|
||||
{\colortbl;\red255\green255\blue255;}
|
||||
{\*\expandedcolortbl;;}
|
||||
@ -12,6 +12,19 @@
|
||||
{\field{\*\fldinst{HYPERLINK "GITHUB_BUILD_URL"}}{\fldrslt Build Log}}\
|
||||
\
|
||||
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/danielctull"}}{\fldrslt Daniel Tull}}\
|
||||
{\field{\*\fldinst{HYPERLINK "https://github.com/davedelong"}}{\fldrslt Dave DeLong}}\
|
||||
@ -21,4 +34,3 @@ Special Thanks To:\
|
||||
{\field{\*\fldinst{HYPERLINK "https://github.com/mergesort"}}{\fldrslt Joe Fabisevich}}\
|
||||
{\field{\*\fldinst{HYPERLINK "https://github.com/phillco"}}{\fldrslt Phil Cohen}}\
|
||||
{\field{\*\fldinst{HYPERLINK "https://github.com/zackdotcomputer"}}{\fldrslt Zack Sheppard}}}
|
||||
{\field{\*\fldinst{HYPERLINK "https://github.com/zacwest"}}{\fldrslt Zac West}}}
|
||||
|
31
Secretive/InternetAccessPolicy.plist
Normal file
31
Secretive/InternetAccessPolicy.plist
Normal 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>
|
@ -42,7 +42,6 @@ extension Preview {
|
||||
}
|
||||
|
||||
class StoreModifiable: Store, SecretStoreModifiable {
|
||||
|
||||
override var name: String { "Modifiable Preview Store" }
|
||||
|
||||
func create(name: String, requiresAuthentication: Bool) throws {
|
||||
@ -50,8 +49,10 @@ extension Preview {
|
||||
|
||||
func delete(secret: Preview.Secret) throws {
|
||||
}
|
||||
}
|
||||
|
||||
func update(secret: Preview.Secret, name: String) throws {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Preview {
|
||||
|
@ -26,8 +26,12 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
|
||||
TextField("Shhhhh", text: $name).focusable()
|
||||
}
|
||||
HStack {
|
||||
Toggle(isOn: $requiresAuthentication) {
|
||||
Text("Requires Authentication (Biometrics or Password)")
|
||||
VStack(spacing: 20) {
|
||||
Picker("", selection: $requiresAuthentication) {
|
||||
Text("Requires Authentication (Biometrics or Password) before each use").tag(true)
|
||||
Text("Authentication not required when Mac is unlocked").tag(false)
|
||||
}
|
||||
.pickerStyle(RadioGroupPickerStyle())
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
|
@ -30,9 +30,6 @@ struct DeleteSecretView<StoreType: SecretStoreModifiable>: View {
|
||||
TextField(secret.name, text: $confirm)
|
||||
}
|
||||
}
|
||||
.onExitCommand {
|
||||
dismissalBlock(false)
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Spacer()
|
||||
@ -47,6 +44,9 @@ struct DeleteSecretView<StoreType: SecretStoreModifiable>: View {
|
||||
}
|
||||
.padding()
|
||||
.frame(minWidth: 400)
|
||||
.onExitCommand {
|
||||
dismissalBlock(false)
|
||||
}
|
||||
}
|
||||
|
||||
func delete() {
|
||||
|
@ -53,14 +53,14 @@ struct EmptyStoreModifiableView: View {
|
||||
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)))
|
||||
path.addCurve(to:
|
||||
CGPoint(x: g.size.width, y: 0), control1:
|
||||
CGPoint(x: g.size.width, y: g.size.height * (1/2)), control2:
|
||||
CGPoint(x: g.size.width, y: 0))
|
||||
CGPoint(x: g.size.width - 13, y: 0), control1:
|
||||
CGPoint(x: g.size.width - 13 , y: g.size.height * (1/2)), control2:
|
||||
CGPoint(x: g.size.width - 13, y: 0))
|
||||
}.stroke(style: StrokeStyle(lineWidth: 5, lineCap: .round))
|
||||
Path { path in
|
||||
path.move(to: CGPoint(x: g.size.width - 10, y: 0))
|
||||
path.addLine(to: CGPoint(x: g.size.width, y: -10))
|
||||
path.addLine(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 - 13, y: -10))
|
||||
path.addLine(to: CGPoint(x: g.size.width - 3, y: 0))
|
||||
}.fill()
|
||||
}.frame(height: (windowGeometry.size.height/2) - 20).padding()
|
||||
Text("No Secrets").bold()
|
||||
|
50
Secretive/Views/RenameSecretView.swift
Normal file
50
Secretive/Views/RenameSecretView.swift
Normal 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)
|
||||
}
|
||||
}
|
@ -10,10 +10,13 @@ struct SecretDetailView<SecretType: Secret>: View {
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
CopyableView(title: "Fingerprint", image: Image(systemName: "touchid"), text: keyWriter.openSSHFingerprint(secret: secret))
|
||||
CopyableView(title: "SHA256 Fingerprint", image: Image(systemName: "touchid"), text: keyWriter.openSSHSHA256Fingerprint(secret: secret))
|
||||
Spacer()
|
||||
.frame(height: 20)
|
||||
CopyableView(title: "Public Key", image: Image(systemName: "key"), text: keyWriter.openSSHString(secret: secret))
|
||||
CopyableView(title: "MD5 Fingerprint", image: Image(systemName: "touchid"), text: keyWriter.openSSHMD5Fingerprint(secret: secret))
|
||||
Spacer()
|
||||
.frame(height: 20)
|
||||
CopyableView(title: "Public Key", image: Image(systemName: "key"), text: keyString)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
@ -21,8 +24,19 @@ struct SecretDetailView<SecretType: Secret>: View {
|
||||
.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 {
|
||||
keyWriter.openSSHString(secret: secret)
|
||||
keyWriter.openSSHString(secret: secret, comment: "\(dashedKeyName)@\(dashedHostName)")
|
||||
}
|
||||
|
||||
func copy() {
|
||||
|
54
Secretive/Views/SecretListItemView.swift
Normal file
54
Secretive/Views/SecretListItemView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
import SwiftUI
|
||||
import SecretKit
|
||||
|
||||
struct SecretListView: View {
|
||||
|
||||
@ObservedObject var store: AnySecretStore
|
||||
@Binding var activeSecret: AnySecret.ID?
|
||||
@Binding var deletingSecret: AnySecret?
|
||||
|
||||
var deletedSecret: (AnySecret) -> Void
|
||||
|
||||
var body: some View {
|
||||
ForEach(store.secrets) { secret in
|
||||
NavigationLink(destination: SecretDetailView(secret: secret), tag: secret.id, selection: $activeSecret) {
|
||||
Text(secret.name)
|
||||
}.contextMenu {
|
||||
if store is AnySecretStoreModifiable {
|
||||
Button(action: { delete(secret: secret) }) {
|
||||
Text("Delete")
|
||||
}
|
||||
}
|
||||
}
|
||||
.popover(isPresented: .constant(deletingSecret == secret)) {
|
||||
if let modifiable = store as? AnySecretStoreModifiable {
|
||||
DeleteSecretView(store: modifiable, secret: secret) { deleted in
|
||||
deletingSecret = nil
|
||||
if deleted {
|
||||
deletedSecret(AnySecret(secret))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func delete<SecretType: Secret>(secret: SecretType) {
|
||||
deletingSecret = AnySecret(secret)
|
||||
}
|
||||
|
||||
}
|
@ -6,10 +6,17 @@ struct StoreListView: View {
|
||||
@Binding var showingCreation: Bool
|
||||
|
||||
@State private var activeSecret: AnySecret.ID?
|
||||
@State private var deletingSecret: AnySecret?
|
||||
|
||||
@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) {
|
||||
@ -19,9 +26,15 @@ struct StoreListView: View {
|
||||
if store.secrets.isEmpty {
|
||||
EmptyStoreView(store: store, activeSecret: $activeSecret)
|
||||
} else {
|
||||
SecretListView(store: store, activeSecret: $activeSecret, deletingSecret: $deletingSecret, deletedSecret: { _ in
|
||||
activeSecret = nextDefaultSecret
|
||||
})
|
||||
ForEach(store.secrets) { secret in
|
||||
SecretListItemView(
|
||||
store: store,
|
||||
secret: secret,
|
||||
activeSecret: $activeSecret,
|
||||
deletedSecret: self.secretDeleted,
|
||||
renamedSecret: self.secretRenamed
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -33,9 +46,7 @@ struct StoreListView: View {
|
||||
}
|
||||
.frame(minWidth: 100, idealWidth: 240)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension StoreListView {
|
||||
|
Reference in New Issue
Block a user