Compare commits

...

19 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
34 changed files with 550 additions and 127 deletions

BIN
.github/readme/app.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 338 KiB

After

Width:  |  Height:  |  Size: 456 KiB

View File

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

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

View File

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

View File

@ -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 {

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

@ -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")

View File

@ -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
View File

@ -24,6 +24,10 @@ Please run `ssh -Tv git@github.com` in your terminal and paste the output in a [
![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?
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).

Binary file not shown.

View File

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

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

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

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

View File

@ -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: ":")

View File

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

View File

@ -68,7 +68,7 @@ extension SecureEnclave {
let deleteAttributes = [
kSecClass: kSecClassKey,
kSecAttrApplicationLabel: secret.id as CFData
] as CFDictionary
] as CFDictionary
let status = SecItemDelete(deleteAttributes)
if status != errSecSuccess {
throw KeychainError(statusCode: status)
@ -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)\""

View File

@ -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() {

View File

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

View File

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

View File

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

View File

@ -2,9 +2,10 @@ import Foundation
import ServiceManagement
import AppKit
import OSLog
import SecretKit
struct LaunchAgentController {
func install(completion: (() -> Void)? = nil) {
Logger().debug("Installing agent")
_ = setEnabled(false)
@ -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
completion?(error == nil)
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)
}
}

View File

@ -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",

View File

@ -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}}\
@ -20,5 +33,4 @@ Special Thanks To:\
{\field{\*\fldinst{HYPERLINK "https://github.com/marksands"}}{\fldrslt Mark Sands}}\
{\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}}}
{\field{\*\fldinst{HYPERLINK "https://github.com/zackdotcomputer"}}{\fldrslt Zack Sheppard}}}

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

@ -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 {

View File

@ -2,10 +2,10 @@ import SwiftUI
import SecretKit
struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
@ObservedObject var store: StoreType
@Binding var showing: Bool
@State private var name = ""
@State private var requiresAuthentication = true
@ -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()
}
@ -45,7 +49,7 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
}
}.padding()
}
func save() {
try! store.create(name: name, requiresAuthentication: requiresAuthentication)
showing = false

View File

@ -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() {

View File

@ -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()

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

@ -10,19 +10,33 @@ 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()
}
}
.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 {
keyWriter.openSSHString(secret: secret)
keyWriter.openSSHString(secret: secret, comment: "\(dashedKeyName)@\(dashedHostName)")
}
func copy() {

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

View File

@ -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 {