Compare commits
1 Commits
projected_
...
v1.0.3
Author | SHA1 | Date | |
---|---|---|---|
cd0a1b0a68 |
BIN
.github/readme/app.png
vendored
Before Width: | Height: | Size: 456 KiB After Width: | Height: | Size: 348 KiB |
BIN
.github/readme/apple_watch_auth_mac.png
vendored
Before Width: | Height: | Size: 192 KiB |
BIN
.github/readme/apple_watch_auth_watch.png
vendored
Before Width: | Height: | Size: 26 KiB |
BIN
.github/readme/apple_watch_system_prefs.png
vendored
Before Width: | Height: | Size: 631 KiB |
BIN
.github/readme/notification.png
vendored
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.6 MiB |
BIN
.github/readme/touchid.png
vendored
Before Width: | Height: | Size: 190 KiB After Width: | Height: | Size: 135 KiB |
7
.github/scripts/signing.sh
vendored
@ -10,13 +10,10 @@ security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k ci ci.keyc
|
|||||||
|
|
||||||
# Import Profiles
|
# Import Profiles
|
||||||
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
|
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
|
||||||
|
|
||||||
echo $HOST_PROFILE_DATA | base64 -d -o Host.provisionprofile
|
echo $HOST_PROFILE_DATA | base64 -d -o Host.provisionprofile
|
||||||
HOST_UUID=`grep UUID -A1 -a Host.provisionprofile | grep -io "[-A-F0-9]\{36\}"`
|
HOST_UUID=`grep UUID -A1 -a Host.provisionprofile | grep -io "[-A-F0-9]\{36\}"`
|
||||||
cp Host.provisionprofile ~/Library/MobileDevice/Provisioning\ Profiles/$HOST_UUID.provisionprofile
|
cp Host.provisionprofile ~/Library/MobileDevice/Provisioning\ Profiles/$HOST_UUID.provisionprofile
|
||||||
echo $AGENT_PROFILE_DATA | base64 -d -o Agent.provisionprofile
|
echo $AGENT_PROFILE_DATA | base64 -d -o Agent.provisionprofile
|
||||||
AGENT_UUID=`grep UUID -A1 -a Agent.provisionprofile | grep -io "[-A-F0-9]\{36\}"`
|
AGENT_UUID=`grep UUID -A1 -a Agent.provisionprofile | grep -io "[-A-F0-9]\{36\}"`
|
||||||
cp Agent.provisionprofile ~/Library/MobileDevice/Provisioning\ Profiles/$AGENT_UUID.provisionprofile
|
cp Agent.provisionprofile ~/Library/MobileDevice/Provisioning\ Profiles/$AGENT_UUID.provisionprofile
|
||||||
|
|
||||||
# Create directories for ASC key
|
|
||||||
mkdir ~/.private_keys
|
|
||||||
echo -n "$APPLE_API_KEY_DATA" > ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8
|
|
110
.github/workflows/release.yml
vendored
@ -6,7 +6,7 @@ on:
|
|||||||
- '*'
|
- '*'
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: macos-11.0
|
runs-on: macOS-latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
@ -16,56 +16,14 @@ jobs:
|
|||||||
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
|
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
|
||||||
HOST_PROFILE_DATA: ${{ secrets.HOST_PROFILE_DATA }}
|
HOST_PROFILE_DATA: ${{ secrets.HOST_PROFILE_DATA }}
|
||||||
AGENT_PROFILE_DATA: ${{ secrets.AGENT_PROFILE_DATA }}
|
AGENT_PROFILE_DATA: ${{ secrets.AGENT_PROFILE_DATA }}
|
||||||
APPLE_API_KEY_DATA: ${{ secrets.APPLE_API_KEY_DATA }}
|
|
||||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
|
||||||
run: ./.github/scripts/signing.sh
|
run: ./.github/scripts/signing.sh
|
||||||
- name: Set Environment
|
|
||||||
run: sudo xcrun xcode-select -s /Applications/Xcode_13.2.1.app
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: |
|
run: xcrun xcodebuild test -project Secretive.xcodeproj -scheme Secretive
|
||||||
pushd Sources/Packages
|
|
||||||
swift test
|
|
||||||
popd
|
|
||||||
build:
|
build:
|
||||||
runs-on: macos-11.0
|
runs-on: macOS-latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v1
|
||||||
- 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_13.2.1.app
|
|
||||||
- name: Update Build Number
|
|
||||||
env:
|
|
||||||
TAG_NAME: ${{ github.ref }}
|
|
||||||
RUN_ID: ${{ github.run_id }}
|
|
||||||
run: |
|
|
||||||
export CLEAN_TAG=$(echo $TAG_NAME | sed -e 's/refs\/tags\/v//')
|
|
||||||
sed -i '' -e "s/GITHUB_CI_VERSION/$CLEAN_TAG/g" Sources/Config/Config.xcconfig
|
|
||||||
sed -i '' -e "s/GITHUB_BUILD_NUMBER/1.$RUN_ID/g" Sources/Config/Config.xcconfig
|
|
||||||
sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Secretive/Credits.rtf
|
|
||||||
- name: Build
|
|
||||||
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
|
|
||||||
- name: Create ZIPs
|
|
||||||
run: |
|
|
||||||
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive/Products/Applications/Secretive.app ./Secretive.zip
|
|
||||||
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive ./Archive.zip
|
|
||||||
- name: Notarize
|
|
||||||
env:
|
|
||||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
|
||||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
|
||||||
run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip
|
|
||||||
- name: Document SHAs
|
|
||||||
run: |
|
|
||||||
shasum -a 512 Secretive.zip
|
|
||||||
shasum -a 512 Archive.zip
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
id: create_release
|
id: create_release
|
||||||
uses: actions/create-release@v1
|
uses: actions/create-release@v1
|
||||||
@ -74,24 +32,42 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
tag_name: ${{ github.ref }}
|
tag_name: ${{ github.ref }}
|
||||||
release_name: ${{ github.ref }}
|
release_name: ${{ github.ref }}
|
||||||
body: |
|
body: "Build: https://github.com/maxgoedjen/secretive/actions/runs/${{ github.run_id }}"
|
||||||
Update description
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
|
|
||||||
## Fixes
|
|
||||||
|
|
||||||
|
|
||||||
## Minimum macOS Version
|
|
||||||
|
|
||||||
|
|
||||||
## Build
|
|
||||||
https://github.com/maxgoedjen/secretive/actions/runs/${{ github.run_id }}
|
|
||||||
draft: true
|
draft: true
|
||||||
prerelease: false
|
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 }}
|
||||||
|
run: ./.github/scripts/signing.sh
|
||||||
|
- name: Update Build Number
|
||||||
|
env:
|
||||||
|
TAG_NAME: ${{ github.ref }}
|
||||||
|
RUN_ID: ${{ github.run_id }}
|
||||||
|
run: |
|
||||||
|
export CLEAN_TAG=$(echo $TAG_NAME | sed -e 's/refs\/tags\/v//')
|
||||||
|
sed -i '' -e "s/GITHUB_CI_VERSION/$CLEAN_TAG/g" Config/Config.xcconfig
|
||||||
|
sed -i '' -e "s/GITHUB_BUILD_NUMBER/1.$RUN_ID/g" Config/Config.xcconfig
|
||||||
|
sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Secretive/Credits.rtf
|
||||||
|
- name: Build
|
||||||
|
run: xcrun xcodebuild -project Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
|
||||||
|
- name: Create ZIPs
|
||||||
|
run: |
|
||||||
|
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive/Products/Applications/Secretive.app ./Secretive.zip
|
||||||
|
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
|
||||||
|
- name: Document SHAs
|
||||||
|
run: |
|
||||||
|
shasum -a 512 Secretive.zip
|
||||||
|
shasum -a 512 Archive.zip
|
||||||
- name: Upload App to Release
|
- name: Upload App to Release
|
||||||
id: upload-release-asset-app
|
id: upload-release-asset
|
||||||
uses: actions/upload-release-asset@v1.0.1
|
uses: actions/upload-release-asset@v1.0.1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@ -100,13 +76,13 @@ jobs:
|
|||||||
asset_path: ./Secretive.zip
|
asset_path: ./Secretive.zip
|
||||||
asset_name: Secretive.zip
|
asset_name: Secretive.zip
|
||||||
asset_content_type: application/zip
|
asset_content_type: application/zip
|
||||||
- name: Upload App to Artifacts
|
- name: Upload Archive to Artifacts
|
||||||
|
uses: actions/upload-artifact@v1
|
||||||
|
with:
|
||||||
|
name: Archive.zip
|
||||||
|
path: Archive.zip
|
||||||
|
- name: Upload Archive to Artifacts
|
||||||
uses: actions/upload-artifact@v1
|
uses: actions/upload-artifact@v1
|
||||||
with:
|
with:
|
||||||
name: Secretive.zip
|
name: Secretive.zip
|
||||||
path: Secretive.zip
|
path: Secretive.zip
|
||||||
- name: Upload Archive to Artifacts
|
|
||||||
uses: actions/upload-artifact@v1
|
|
||||||
with:
|
|
||||||
name: Xcode_Archive.zip
|
|
||||||
path: Archive.zip
|
|
||||||
|
20
.github/workflows/test.yml
vendored
@ -1,16 +1,18 @@
|
|||||||
name: Test
|
name: Test
|
||||||
|
|
||||||
on: [push, pull_request]
|
on: push
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: macos-11.0
|
runs-on: macOS-latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v1
|
||||||
- name: Set Environment
|
- name: Setup Signing
|
||||||
run: sudo xcrun xcode-select -s /Applications/Xcode_13.2.1.app
|
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 }}
|
||||||
|
run: ./.github/scripts/signing.sh
|
||||||
- name: Test
|
- name: Test
|
||||||
run: |
|
run: xcrun xcodebuild test -project Secretive.xcodeproj -scheme Secretive
|
||||||
pushd Sources/Packages
|
|
||||||
swift test
|
|
||||||
popd
|
|
||||||
|
2
.gitignore
vendored
@ -91,5 +91,3 @@ iOSInjectionProject/
|
|||||||
|
|
||||||
# Build script products
|
# Build script products
|
||||||
Archive.xcarchive
|
Archive.xcarchive
|
||||||
.DS_Store
|
|
||||||
contents.xcworkspacedata
|
|
||||||
|
@ -1,58 +0,0 @@
|
|||||||
# Setting up Third Party Apps FAQ
|
|
||||||
|
|
||||||
## Tower
|
|
||||||
|
|
||||||
Tower provides [instructions](https://www.git-tower.com/help/mac/integration/environment).
|
|
||||||
|
|
||||||
## GitHub Desktop
|
|
||||||
|
|
||||||
Should just work, no configuration needed
|
|
||||||
|
|
||||||
## Fork
|
|
||||||
|
|
||||||
Add this to your `~/.ssh/config` (the path should match the socket path from the setup flow).
|
|
||||||
|
|
||||||
```
|
|
||||||
Host *
|
|
||||||
IdentityAgent /Users/$YOUR_USERNAME/Library/Containers/com.maxgoedjen.Secretive.SecretAgent/Data/socket.ssh
|
|
||||||
```
|
|
||||||
|
|
||||||
## VS Code
|
|
||||||
|
|
||||||
Add this to your `~/.ssh/config` (the path should match the socket path from the setup flow).
|
|
||||||
|
|
||||||
```
|
|
||||||
Host *
|
|
||||||
IdentityAgent /Users/$YOUR_USERNAME/Library/Containers/com.maxgoedjen.Secretive.SecretAgent/Data/socket.ssh
|
|
||||||
```
|
|
||||||
|
|
||||||
## Cyberduck
|
|
||||||
|
|
||||||
Add this to `~/Library/LaunchAgents/com.maxgoedjen.Secretive.SecretAgent.plist`
|
|
||||||
|
|
||||||
```
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>Label</key>
|
|
||||||
<string>link-ssh-auth-sock</string>
|
|
||||||
<key>ProgramArguments</key>
|
|
||||||
<array>
|
|
||||||
<string>/bin/sh</string>
|
|
||||||
<string>-c</string>
|
|
||||||
<string>/bin/ln -sf $HOME/Library/Containers/com.maxgoedjen.Secretive.SecretAgent/Data/socket.ssh $SSH_AUTH_SOCK</string>
|
|
||||||
</array>
|
|
||||||
<key>RunAtLoad</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
```
|
|
||||||
|
|
||||||
Log out and log in again before launching Cyberduck.
|
|
||||||
|
|
||||||
|
|
||||||
# The app I use isn't listed here!
|
|
||||||
|
|
||||||
If you know how to get it set up, please open a PR for this page and add it! Contributions are very welcome.
|
|
||||||
If you're not able to get it working, please file a [GitHub issue](https://github.com/maxgoedjen/secretive/issues/new) for it. No guarantees we'll be able to get it working, but chances are someone else in the community might be able to.
|
|
19
Brief/Brief.h
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
//
|
||||||
|
// Brief.h
|
||||||
|
// Brief
|
||||||
|
//
|
||||||
|
// Created by Max Goedjen on 3/21/20.
|
||||||
|
// Copyright © 2020 Max Goedjen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
//! Project version number for Brief.
|
||||||
|
FOUNDATION_EXPORT double BriefVersionNumber;
|
||||||
|
|
||||||
|
//! Project version string for Brief.
|
||||||
|
FOUNDATION_EXPORT const unsigned char BriefVersionString[];
|
||||||
|
|
||||||
|
// In this header, you should import all the public headers of your framework using statements like #import <Brief/PublicHeader.h>
|
||||||
|
|
||||||
|
|
24
Brief/Info.plist
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>$(PRODUCT_NAME) is MIT Licensed.</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
140
Brief/Updater.swift
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
public protocol UpdaterProtocol: ObservableObject {
|
||||||
|
|
||||||
|
var update: Release? { get }
|
||||||
|
func ignore(release: Release)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Updater: ObservableObject, UpdaterProtocol {
|
||||||
|
|
||||||
|
@Published public var update: Release?
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
checkForUpdates()
|
||||||
|
let timer = Timer.scheduledTimer(withTimeInterval: 60*60*24, repeats: true) { _ in
|
||||||
|
self.checkForUpdates()
|
||||||
|
}
|
||||||
|
timer.tolerance = 60*60
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func ignore(release: Release) {
|
||||||
|
guard !release.critical else { return }
|
||||||
|
defaults.set(true, forKey: release.name)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.update = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Updater {
|
||||||
|
|
||||||
|
func evaluate(release: Release) {
|
||||||
|
guard !userIgnored(release: release) else { return }
|
||||||
|
let latestVersion = SemVer(release.name)
|
||||||
|
let currentVersion = SemVer(Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String)
|
||||||
|
if latestVersion > currentVersion {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.update = release
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func userIgnored(release: Release) -> Bool {
|
||||||
|
guard !release.critical else { return false }
|
||||||
|
return defaults.bool(forKey: release.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaults: UserDefaults {
|
||||||
|
UserDefaults(suiteName: "com.maxgoedjen.Secretive.updater.ignorelist")!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SemVer {
|
||||||
|
|
||||||
|
let versionNumbers: [Int]
|
||||||
|
|
||||||
|
init(_ version: String) {
|
||||||
|
// Betas have the format 1.2.3_beta1
|
||||||
|
let strippedBeta = version.split(separator: "_").first!
|
||||||
|
var split = strippedBeta.split(separator: ".").compactMap { Int($0) }
|
||||||
|
while split.count < 3 {
|
||||||
|
split.append(0)
|
||||||
|
}
|
||||||
|
versionNumbers = split
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SemVer: Comparable {
|
||||||
|
|
||||||
|
static func < (lhs: SemVer, rhs: SemVer) -> Bool {
|
||||||
|
for (latest, current) in zip(lhs.versionNumbers, rhs.versionNumbers) {
|
||||||
|
if latest < current {
|
||||||
|
return true
|
||||||
|
} else if latest > current {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Updater {
|
||||||
|
|
||||||
|
enum Constants {
|
||||||
|
static let updateURL = URL(string: "https://api.github.com/repos/maxgoedjen/secretive/releases/latest")!
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Release: Codable {
|
||||||
|
|
||||||
|
public let name: String
|
||||||
|
public let html_url: URL
|
||||||
|
public let body: String
|
||||||
|
|
||||||
|
public init(name: String, html_url: URL, body: String) {
|
||||||
|
self.name = name
|
||||||
|
self.html_url = html_url
|
||||||
|
self.body = body
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Release: Identifiable {
|
||||||
|
|
||||||
|
public var id: String {
|
||||||
|
html_url.absoluteString
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Release {
|
||||||
|
|
||||||
|
public var critical: Bool {
|
||||||
|
body.contains(Constants.securityContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Release {
|
||||||
|
|
||||||
|
enum Constants {
|
||||||
|
static let securityContent = "Critical Security Update"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -8,20 +8,12 @@ Security is obviously paramount for a project like Secretive. As such, any contr
|
|||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
Secretive is designed to be easily auditable by people who are considering using it. In keeping with this, Secretive has no third party dependencies, and any contributions which bring in new dependencies will be rejected.
|
Secretive is desigend to be easily auditable by people who are considering using it. In keeping with this, Secretive has no third party dependencies, and any contributions which bring in new dependencies will be rejected.
|
||||||
|
|
||||||
## Code of Conduct
|
## Code of Conduct
|
||||||
|
|
||||||
All contributors must abide by the [Code of Conduct](CODE_OF_CONDUCT.md)
|
All contributors must abide by the [Code of Conduct](CODE_OF_CONDUCT.md)
|
||||||
|
|
||||||
## Credits
|
|
||||||
|
|
||||||
If you make a material contribution to the app, please add yourself to the end of the [credits](https://github.com/maxgoedjen/secretive/blob/main/Secretive/Credits.rtf).
|
|
||||||
|
|
||||||
## Collaborator Status
|
|
||||||
|
|
||||||
I will not grant collaborator access to any contributors for this repository. This is basically just because collaborators [can accesss the secrets Secretive uses for the signing credentials stored in the repository](https://docs.github.com/en/actions/reference/encrypted-secrets#accessing-your-secrets).
|
|
||||||
|
|
||||||
## Secretive is Opinionated
|
## Secretive is Opinionated
|
||||||
|
|
||||||
I'm releasing Secretive as open source so that other people can use it and audit it, feeling comfortable in knowing that the source is available so they can see what it's doing. I have a pretty strong idea of what I'd like this project to look like, and I may respectfully decline contributions that don't line up with that vision. If you'd like to propose a change before implementing, please feel free to [Open an Issue with the proposed tag](https://github.com/maxgoedjen/secretive/issues/new?labels=proposed).
|
I'm releasing Secretive as open source so that other people can use it and audit it, feeling comfortable in knowing that the source is available so they can see what it's doing. I have a pretty strong idea of what I'd like this project to look like, and I may respectfully decline contributions that don't line up with that vision. If you'd like to propose a change before implementing, please feel free to [Open an Issue with the proposed tag](https://github.com/maxgoedjen/secretive/issues/new?labels=proposed).
|
||||||
|
@ -13,7 +13,22 @@
|
|||||||
},
|
},
|
||||||
"testTargets" : [
|
"testTargets" : [
|
||||||
{
|
{
|
||||||
"enabled" : false,
|
"parallelizable" : true,
|
||||||
|
"target" : {
|
||||||
|
"containerPath" : "container:Secretive.xcodeproj",
|
||||||
|
"identifier" : "50617DAF23FCE4AB0099B055",
|
||||||
|
"name" : "SecretKitTests"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parallelizable" : true,
|
||||||
|
"target" : {
|
||||||
|
"containerPath" : "container:Secretive.xcodeproj",
|
||||||
|
"identifier" : "5099A073240242BA0062B6F2",
|
||||||
|
"name" : "SecretAgentKitTests"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
"parallelizable" : true,
|
"parallelizable" : true,
|
||||||
"target" : {
|
"target" : {
|
||||||
"containerPath" : "container:Secretive.xcodeproj",
|
"containerPath" : "container:Secretive.xcodeproj",
|
@ -1,3 +0,0 @@
|
|||||||
# Design
|
|
||||||
|
|
||||||
The art assets for the App Icon and GitHub image are located on [Sketch Cloud](https://www.sketch.com/s/574333cd-8ceb-40e1-a6d9-189da3f1e5dd).
|
|
28
FAQ.md
@ -4,30 +4,18 @@
|
|||||||
|
|
||||||
The secure enclave doesn't allow import or export of private keys. For any new computer, you should just create a new set of keys. If you're using a smart card, you _might_ be able to export your private key from the vendor's software.
|
The secure enclave doesn't allow import or export of private keys. For any new computer, you should just create a new set of keys. If you're using a smart card, you _might_ be able to export your private key from the vendor's software.
|
||||||
|
|
||||||
### Secretive doesn't work with my git client/app
|
### Secretive doesn't work with my git client
|
||||||
|
|
||||||
Secretive relies on the `SSH_AUTH_SOCK` environment variable being respected. The `git` and `ssh` command line tools natively respect this, but third party apps may require some configuration to work. A non-exhaustive list of setup steps is provided in the [App Config FAQ](APP_CONFIG.md).
|
Secretive relies on the `SSH_AUTH_SOCK` environment variable being respected. The `git` and `ssh` command line tools natively respect this, but third party apps may require some configuration to work. A non-exhaustive list of clients is provided here:
|
||||||
|
|
||||||
|
Tower - [Instructions](https://www.git-tower.com/help/mac/integration/environment)
|
||||||
|
|
||||||
|
GitHub Desktop: Should just work, no configuration needed
|
||||||
|
|
||||||
### Secretive isn't working for me
|
### Secretive isn't working for me
|
||||||
|
|
||||||
Please run `ssh -Tv git@github.com` in your terminal and paste the output in a [new GitHub issue](https://github.com/maxgoedjen/secretive/issues/new) with a description of your issue.
|
Please run `ssh -Tv git@github.com` in your terminal and paste the output in a [new GitHub issue](https://github.com/maxgoedjen/secretive/issues/new) with a description of your issue.
|
||||||
|
|
||||||
### Secretive prompts me to type my password instead of using my Apple Watch
|
|
||||||
|
|
||||||
1) Make sure you have enabled "Use your Apple Watch to unlock apps and your Mac" in System Preferences --> Security & Privacy:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
2) Ensure that unlocking your Mac with Apple Watch is working (lock and unlock at least once)
|
|
||||||
3) Now you should get prompted on the watch when your key is accessed. Double click the side button to approve:
|
|
||||||
|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
### How do I tell SSH to use a specific key?
|
|
||||||
|
|
||||||
You can create a `mykey.pub` (where `mykey` is the name of your key) in your `~/.ssh/` directory with the contents of your public key, and specify that you want to use that key in your `~/.ssh/config`. [This ServerFault answer](https://serverfault.com/a/295771) has more details on setting that up
|
|
||||||
|
|
||||||
### Why should I trust you?
|
### Why should I trust you?
|
||||||
|
|
||||||
You shouldn't, for a piece of software like this. Secretive, by design, has an auditable build process. Each build has a fully auditable build log, showing the source it was built from and a SHA of the build product. You can check the SHA of the zip you download against the SHA output in the build log (which is linked in the About window).
|
You shouldn't, for a piece of software like this. Secretive, by design, has an auditable build process. Each build has a fully auditable build log, showing the source it was built from and a SHA of the build product. You can check the SHA of the zip you download against the SHA output in the build log (which is linked in the About window).
|
||||||
@ -36,10 +24,6 @@ You shouldn't, for a piece of software like this. Secretive, by design, has an a
|
|||||||
|
|
||||||
Awesome! Just bear in mind that because an app only has access to the keychain items that it created, if you have secrets that you created with the prebuilt version of Secretive, you'll be unable to access them using your own custom build (since you'll have changed the bundled ID).
|
Awesome! Just bear in mind that because an app only has access to the keychain items that it created, if you have secrets that you created with the prebuilt version of Secretive, you'll be unable to access them using your own custom build (since you'll have changed the bundled ID).
|
||||||
|
|
||||||
### What's this network request to GitHub?
|
|
||||||
|
|
||||||
Secretive checks in with GitHub's releases API to check if there's a new version of Secretive available. You can audit the source code for this feature [here](https://github.com/maxgoedjen/secretive/blob/main/Brief/Updater.swift).
|
|
||||||
|
|
||||||
### I have a security issue
|
### I have a security issue
|
||||||
|
|
||||||
Please contact [max.goedjen@gmail.com](mailto:max.goedjen@gmail.com) with a subject containing "SECRETIVE SECURITY" immediately with details, and I'll address the issue and credit you ASAP.
|
Please contact [max.goedjen@gmail.com](mailto:max.goedjen@gmail.com) with a subject containing "SECRETIVE SECURITY" immediately with details, and I'll address the issue and credit you ASAP.
|
||||||
|
14
README.md
@ -20,7 +20,7 @@ If your Mac has a Secure Enclave, it also has support for strong access controls
|
|||||||
|
|
||||||
### Notifications
|
### Notifications
|
||||||
|
|
||||||
Secretive also notifies you whenever your keys are accessed, so you're never caught off guard.
|
Secretive also notifies you whenever your keys are acceessed, so you're never caught off guard.
|
||||||
|
|
||||||
<img src="/.github/readme/notification.png" alt="Screenshot of Secretive notifying the user">
|
<img src="/.github/readme/notification.png" alt="Screenshot of Secretive notifying the user">
|
||||||
|
|
||||||
@ -30,16 +30,6 @@ For Macs without Secure Enclaves, you can configure a Smart Card (such as a Yubi
|
|||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
#### Direct Download
|
|
||||||
|
|
||||||
You can download the latest release over on the [Releases Page](https://github.com/maxgoedjen/secretive/releases)
|
|
||||||
|
|
||||||
#### Using Homebrew
|
|
||||||
|
|
||||||
brew install secretive
|
|
||||||
|
|
||||||
### FAQ
|
### FAQ
|
||||||
|
|
||||||
There's a [FAQ here](FAQ.md).
|
There's a [FAQ here](FAQ.md).
|
||||||
@ -54,7 +44,7 @@ While Secretive uses the Secure Enclave for key storage, it still relies on Keyc
|
|||||||
|
|
||||||
### Backups and Transfers to New Machines
|
### Backups and Transfers to New Machines
|
||||||
|
|
||||||
Because secrets in the Secure Enclave are not exportable, they are not able to be backed up, and you will not be able to transfer them to a new machine. If you get a new Mac, just create a new set of secrets specific to that Mac.
|
Beacuse secrets in the Secure Enclave are not exportable, they are not able to be backed up, and you will not be able to transfer them to a new machine. If you get a new Mac, just create a new set of secrets specific to that Mac.
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
# Security Policy
|
|
||||||
|
|
||||||
## Supported Versions
|
|
||||||
|
|
||||||
The latest version on the [Releases page](https://github.com/maxgoedjen/secretive/releases) is the only currently supported version.
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
|
||||||
|
|
||||||
If you discover any vulnerabilities in this project, please notify max.goedjen@gmail.com with the subject containing "SECRETIVE SECURITY."
|
|
42
SecretAgent/AppDelegate.swift
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import Cocoa
|
||||||
|
import OSLog
|
||||||
|
import Combine
|
||||||
|
import SecretKit
|
||||||
|
import SecretAgentKit
|
||||||
|
import Brief
|
||||||
|
|
||||||
|
@NSApplicationMain
|
||||||
|
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
|
||||||
|
let storeList: SecretStoreList = {
|
||||||
|
let list = SecretStoreList()
|
||||||
|
list.add(store: SecureEnclave.Store())
|
||||||
|
list.add(store: SmartCard.Store())
|
||||||
|
return list
|
||||||
|
}()
|
||||||
|
let updater = Updater()
|
||||||
|
let notifier = Notifier()
|
||||||
|
lazy var agent: Agent = {
|
||||||
|
Agent(storeList: storeList, witness: notifier)
|
||||||
|
}()
|
||||||
|
lazy var socketController: SocketController = {
|
||||||
|
let path = (NSHomeDirectory() as NSString).appendingPathComponent("socket.ssh") as String
|
||||||
|
return SocketController(path: path)
|
||||||
|
}()
|
||||||
|
fileprivate var updateSink: AnyCancellable?
|
||||||
|
|
||||||
|
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||||
|
os_log(.debug, "SecretAgent finished launching")
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.socketController.handler = self.agent.handle(reader:writer:)
|
||||||
|
}
|
||||||
|
notifier.prompt()
|
||||||
|
updateSink = updater.$update.sink { update in
|
||||||
|
guard let update = update else { return }
|
||||||
|
self.notifier.notify(update: update, ignore: self.updater.ignore(release:))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -31,13 +31,13 @@
|
|||||||
"size" : "128x128"
|
"size" : "128x128"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "Mac Icon.png",
|
"filename" : "Icon 2@1x.png",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "1x",
|
"scale" : "1x",
|
||||||
"size" : "256x256"
|
"size" : "256x256"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "Mac Icon@0.25x.png",
|
"filename" : "Icon 2@2x.png",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "256x256"
|
"size" : "256x256"
|
BIN
SecretAgent/Assets.xcassets/AppIcon.appiconset/Icon 2@1x.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
SecretAgent/Assets.xcassets/AppIcon.appiconset/Icon 2@2x.png
Normal file
After Width: | Height: | Size: 66 KiB |
123
SecretAgent/Notifier.swift
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import Foundation
|
||||||
|
import UserNotifications
|
||||||
|
import AppKit
|
||||||
|
import SecretKit
|
||||||
|
import SecretAgentKit
|
||||||
|
import Brief
|
||||||
|
|
||||||
|
class Notifier {
|
||||||
|
|
||||||
|
fileprivate let notificationDelegate = NotificationDelegate()
|
||||||
|
|
||||||
|
init() {
|
||||||
|
let updateAction = UNNotificationAction(identifier: Constants.updateActionIdentitifier, title: "Update", options: [])
|
||||||
|
let ignoreAction = UNNotificationAction(identifier: Constants.ignoreActionIdentitifier, title: "Ignore", options: [])
|
||||||
|
let updateCategory = UNNotificationCategory(identifier: Constants.updateCategoryIdentitifier, actions: [updateAction, ignoreAction], intentIdentifiers: [], options: [])
|
||||||
|
let criticalUpdateCategory = UNNotificationCategory(identifier: Constants.criticalUpdateCategoryIdentitifier, actions: [updateAction], intentIdentifiers: [], options: [])
|
||||||
|
UNUserNotificationCenter.current().setNotificationCategories([updateCategory, criticalUpdateCategory])
|
||||||
|
UNUserNotificationCenter.current().delegate = notificationDelegate
|
||||||
|
}
|
||||||
|
|
||||||
|
func prompt() {
|
||||||
|
let notificationCenter = UNUserNotificationCenter.current()
|
||||||
|
notificationCenter.requestAuthorization(options: .alert) { _, _ in
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func notify(accessTo secret: AnySecret, by provenance: SigningRequestProvenance) {
|
||||||
|
let notificationCenter = UNUserNotificationCenter.current()
|
||||||
|
let notificationContent = UNMutableNotificationContent()
|
||||||
|
notificationContent.title = "Signed Request from \(provenance.origin.name)"
|
||||||
|
notificationContent.subtitle = "Using secret \"\(secret.name)\""
|
||||||
|
if let iconURL = iconURL(for: provenance), let attachment = try? UNNotificationAttachment(identifier: "icon", url: iconURL, options: nil) {
|
||||||
|
notificationContent.attachments = [attachment]
|
||||||
|
}
|
||||||
|
let request = UNNotificationRequest(identifier: UUID().uuidString, content: notificationContent, trigger: nil)
|
||||||
|
notificationCenter.add(request, withCompletionHandler: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func notify(update: Release, ignore: ((Release) -> Void)?) {
|
||||||
|
notificationDelegate.release = update
|
||||||
|
notificationDelegate.ignore = ignore
|
||||||
|
let notificationCenter = UNUserNotificationCenter.current()
|
||||||
|
let notificationContent = UNMutableNotificationContent()
|
||||||
|
if update.critical {
|
||||||
|
notificationContent.title = "Critical Security Update - \(update.name)"
|
||||||
|
} else {
|
||||||
|
notificationContent.title = "Update Available - \(update.name)"
|
||||||
|
}
|
||||||
|
notificationContent.subtitle = "Click to Update"
|
||||||
|
notificationContent.body = update.body
|
||||||
|
notificationContent.categoryIdentifier = update.critical ? Constants.criticalUpdateCategoryIdentitifier : Constants.updateCategoryIdentitifier
|
||||||
|
let request = UNNotificationRequest(identifier: UUID().uuidString, content: notificationContent, trigger: nil)
|
||||||
|
notificationCenter.add(request, withCompletionHandler: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Notifier {
|
||||||
|
|
||||||
|
func iconURL(for provenance: SigningRequestProvenance) -> URL? {
|
||||||
|
do {
|
||||||
|
if let app = NSRunningApplication(processIdentifier: provenance.origin.pid), let icon = app.icon?.tiffRepresentation {
|
||||||
|
let temporaryURL = URL(fileURLWithPath: (NSTemporaryDirectory() as NSString).appendingPathComponent("\(UUID().uuidString).png"))
|
||||||
|
let bitmap = NSBitmapImageRep(data: icon)
|
||||||
|
try bitmap?.representation(using: .png, properties: [:])?.write(to: temporaryURL)
|
||||||
|
return temporaryURL
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Notifier: SigningWitness {
|
||||||
|
|
||||||
|
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, by provenance: SigningRequestProvenance) throws {
|
||||||
|
}
|
||||||
|
|
||||||
|
func witness(accessTo secret: AnySecret, by provenance: SigningRequestProvenance) throws {
|
||||||
|
notify(accessTo: secret, by: provenance)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Notifier {
|
||||||
|
|
||||||
|
enum Constants {
|
||||||
|
static let updateCategoryIdentitifier = "com.maxgoedjen.Secretive.SecretAgent.update"
|
||||||
|
static let criticalUpdateCategoryIdentitifier = "com.maxgoedjen.Secretive.SecretAgent.update.critical"
|
||||||
|
static let updateActionIdentitifier = "com.maxgoedjen.Secretive.SecretAgent.update.updateaction"
|
||||||
|
static let ignoreActionIdentitifier = "com.maxgoedjen.Secretive.SecretAgent.update.ignoreaction"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
|
||||||
|
|
||||||
|
fileprivate var release: Release?
|
||||||
|
fileprivate var ignore: ((Release) -> Void)?
|
||||||
|
|
||||||
|
func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||||
|
guard let update = release else { return }
|
||||||
|
switch response.actionIdentifier {
|
||||||
|
case Notifier.Constants.updateActionIdentitifier, UNNotificationDefaultActionIdentifier:
|
||||||
|
NSWorkspace.shared.open(update.html_url)
|
||||||
|
case Notifier.Constants.ignoreActionIdentitifier:
|
||||||
|
ignore?(update)
|
||||||
|
default:
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
completionHandler()
|
||||||
|
}
|
||||||
|
|
||||||
|
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||||
|
completionHandler(.alert)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -4,10 +4,6 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.security.app-sandbox</key>
|
<key>com.apple.security.app-sandbox</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.application-groups</key>
|
|
||||||
<array>
|
|
||||||
<string>$(TeamIdentifierPrefix)com.maxgoedjen.secretive</string>
|
|
||||||
</array>
|
|
||||||
<key>com.apple.security.network.client</key>
|
<key>com.apple.security.network.client</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.smartcard</key>
|
<key>com.apple.security.smartcard</key>
|
@ -4,20 +4,15 @@ import OSLog
|
|||||||
import SecretKit
|
import SecretKit
|
||||||
import AppKit
|
import AppKit
|
||||||
|
|
||||||
/// The `Agent` is an implementation of an SSH agent. It manages coordination and access between a socket, traces requests, notifies witnesses and passes requests to stores.
|
|
||||||
public class Agent {
|
public class Agent {
|
||||||
|
|
||||||
private let storeList: SecretStoreList
|
fileprivate let storeList: SecretStoreList
|
||||||
private let witness: SigningWitness?
|
fileprivate let witness: SigningWitness?
|
||||||
private let writer = OpenSSHKeyWriter()
|
fileprivate let writer = OpenSSHKeyWriter()
|
||||||
private let requestTracer = SigningRequestTracer()
|
fileprivate let requestTracer = SigningRequestTracer()
|
||||||
|
|
||||||
/// Initializes an agent with a store list and a witness.
|
|
||||||
/// - Parameters:
|
|
||||||
/// - storeList: The `SecretStoreList` to make available.
|
|
||||||
/// - witness: A witness to notify of requests.
|
|
||||||
public init(storeList: SecretStoreList, witness: SigningWitness? = nil) {
|
public init(storeList: SecretStoreList, witness: SigningWitness? = nil) {
|
||||||
Logger().debug("Agent is running")
|
os_log(.debug, "Agent is running")
|
||||||
self.storeList = storeList
|
self.storeList = storeList
|
||||||
self.witness = witness
|
self.witness = witness
|
||||||
}
|
}
|
||||||
@ -26,21 +21,17 @@ public class Agent {
|
|||||||
|
|
||||||
extension Agent {
|
extension Agent {
|
||||||
|
|
||||||
/// Handles an incoming request.
|
|
||||||
/// - Parameters:
|
|
||||||
/// - reader: A ``FileHandleReader`` to read the content of the request.
|
|
||||||
/// - writer: A ``FileHandleWriter`` to write the response to.
|
|
||||||
public func handle(reader: FileHandleReader, writer: FileHandleWriter) {
|
public func handle(reader: FileHandleReader, writer: FileHandleWriter) {
|
||||||
Logger().debug("Agent handling new data")
|
os_log(.debug, "Agent handling new data")
|
||||||
let data = Data(reader.availableData)
|
let data = reader.availableData
|
||||||
guard data.count > 4 else { return }
|
guard !data.isEmpty else { return }
|
||||||
let requestTypeInt = data[4]
|
let requestTypeInt = data[4]
|
||||||
guard let requestType = SSHAgent.RequestType(rawValue: requestTypeInt) else {
|
guard let requestType = SSHAgent.RequestType(rawValue: requestTypeInt) else {
|
||||||
writer.write(OpenSSHKeyWriter().lengthAndData(of: SSHAgent.ResponseType.agentFailure.data))
|
writer.write(OpenSSHKeyWriter().lengthAndData(of: SSHAgent.ResponseType.agentFailure.data))
|
||||||
Logger().debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)")
|
os_log(.debug, "Agent returned %@", SSHAgent.ResponseType.agentFailure.debugDescription)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Logger().debug("Agent handling request of type \(requestType.debugDescription)")
|
os_log(.debug, "Agent handling request of type %@", requestType.debugDescription)
|
||||||
let subData = Data(data[5...])
|
let subData = Data(data[5...])
|
||||||
let response = handle(requestType: requestType, data: subData, reader: reader)
|
let response = handle(requestType: requestType, data: subData, reader: reader)
|
||||||
writer.write(response)
|
writer.write(response)
|
||||||
@ -53,17 +44,17 @@ extension Agent {
|
|||||||
case .requestIdentities:
|
case .requestIdentities:
|
||||||
response.append(SSHAgent.ResponseType.agentIdentitiesAnswer.data)
|
response.append(SSHAgent.ResponseType.agentIdentitiesAnswer.data)
|
||||||
response.append(identities())
|
response.append(identities())
|
||||||
Logger().debug("Agent returned \(SSHAgent.ResponseType.agentIdentitiesAnswer.debugDescription)")
|
os_log(.debug, "Agent returned %@", SSHAgent.ResponseType.agentIdentitiesAnswer.debugDescription)
|
||||||
case .signRequest:
|
case .signRequest:
|
||||||
let provenance = requestTracer.provenance(from: reader)
|
let provenance = requestTracer.provenance(from: reader)
|
||||||
response.append(SSHAgent.ResponseType.agentSignResponse.data)
|
response.append(SSHAgent.ResponseType.agentSignResponse.data)
|
||||||
response.append(try sign(data: data, provenance: provenance))
|
response.append(try sign(data: data, provenance: provenance))
|
||||||
Logger().debug("Agent returned \(SSHAgent.ResponseType.agentSignResponse.debugDescription)")
|
os_log(.debug, "Agent returned %@", SSHAgent.ResponseType.agentSignResponse.debugDescription)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
response.removeAll()
|
response.removeAll()
|
||||||
response.append(SSHAgent.ResponseType.agentFailure.data)
|
response.append(SSHAgent.ResponseType.agentFailure.data)
|
||||||
Logger().debug("Agent returned \(SSHAgent.ResponseType.agentFailure.debugDescription)")
|
os_log(.debug, "Agent returned %@", SSHAgent.ResponseType.agentFailure.debugDescription)
|
||||||
}
|
}
|
||||||
let full = OpenSSHKeyWriter().lengthAndData(of: response)
|
let full = OpenSSHKeyWriter().lengthAndData(of: response)
|
||||||
return full
|
return full
|
||||||
@ -73,10 +64,10 @@ extension Agent {
|
|||||||
|
|
||||||
extension Agent {
|
extension Agent {
|
||||||
|
|
||||||
/// Lists the identities available for signing operations
|
|
||||||
/// - Returns: An OpenSSH formatted Data payload listing the identities available for signing operations.
|
|
||||||
func identities() -> Data {
|
func identities() -> Data {
|
||||||
let secrets = storeList.stores.flatMap(\.secrets)
|
// TODO: RESTORE ONCE XCODE 11.4 IS GM
|
||||||
|
let secrets = storeList.stores.flatMap { $0.secrets }
|
||||||
|
// let secrets = storeList.stores.flatMap(\.secrets)
|
||||||
var count = UInt32(secrets.count).bigEndian
|
var count = UInt32(secrets.count).bigEndian
|
||||||
let countData = Data(bytes: &count, count: UInt32.bitWidth/8)
|
let countData = Data(bytes: &count, count: UInt32.bitWidth/8)
|
||||||
var keyData = Data()
|
var keyData = Data()
|
||||||
@ -87,30 +78,24 @@ extension Agent {
|
|||||||
let curveData = writer.curveType(for: secret.algorithm, length: secret.keySize).data(using: .utf8)!
|
let curveData = writer.curveType(for: secret.algorithm, length: secret.keySize).data(using: .utf8)!
|
||||||
keyData.append(writer.lengthAndData(of: curveData))
|
keyData.append(writer.lengthAndData(of: curveData))
|
||||||
}
|
}
|
||||||
Logger().debug("Agent enumerated \(secrets.count) identities")
|
os_log(.debug, "Agent enumerated %@ identities", secrets.count as NSNumber)
|
||||||
return countData + keyData
|
return countData + keyData
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Notifies witnesses of a pending signature request, and performs the signing operation if none object.
|
|
||||||
/// - Parameters:
|
|
||||||
/// - data: The data to sign.
|
|
||||||
/// - provenance: A ``SecretKit.SigningRequestProvenance`` object describing the origin of the request.
|
|
||||||
/// - Returns: An OpenSSH formatted Data payload containing the signed data response.
|
|
||||||
func sign(data: Data, provenance: SigningRequestProvenance) throws -> Data {
|
func sign(data: Data, provenance: SigningRequestProvenance) throws -> Data {
|
||||||
let reader = OpenSSHReader(data: data)
|
let reader = OpenSSHReader(data: data)
|
||||||
let hash = reader.readNextChunk()
|
let hash = reader.readNextChunk()
|
||||||
guard let (store, secret) = secret(matching: hash) else {
|
guard let (store, secret) = secret(matching: hash) else {
|
||||||
Logger().debug("Agent did not have a key matching \(hash as NSData)")
|
os_log(.debug, "Agent did not have a key matching %@", hash as NSData)
|
||||||
throw AgentError.noMatchingKey
|
throw AgentError.noMatchingKey
|
||||||
}
|
}
|
||||||
|
|
||||||
if let witness = witness {
|
if let witness = witness {
|
||||||
try witness.speakNowOrForeverHoldYourPeace(forAccessTo: secret, from: store, by: provenance)
|
try witness.speakNowOrForeverHoldYourPeace(forAccessTo: secret, by: provenance)
|
||||||
}
|
}
|
||||||
|
|
||||||
let dataToSign = reader.readNextChunk()
|
let dataToSign = reader.readNextChunk()
|
||||||
let signed = try store.sign(data: dataToSign, with: secret, for: provenance)
|
let derSignature = try store.sign(data: dataToSign, with: secret)
|
||||||
let derSignature = signed.data
|
|
||||||
|
|
||||||
let curveData = writer.curveType(for: secret.algorithm, length: secret.keySize).data(using: .utf8)!
|
let curveData = writer.curveType(for: secret.algorithm, length: secret.keySize).data(using: .utf8)!
|
||||||
|
|
||||||
@ -151,10 +136,10 @@ extension Agent {
|
|||||||
signedData.append(writer.lengthAndData(of: sub))
|
signedData.append(writer.lengthAndData(of: sub))
|
||||||
|
|
||||||
if let witness = witness {
|
if let witness = witness {
|
||||||
try witness.witness(accessTo: secret, from: store, by: provenance, requiredAuthentication: signed.requiredAuthentication)
|
try witness.witness(accessTo: secret, by: provenance)
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger().debug("Agent signed request")
|
os_log(.debug, "Agent signed request")
|
||||||
|
|
||||||
return signedData
|
return signedData
|
||||||
}
|
}
|
||||||
@ -163,9 +148,6 @@ extension Agent {
|
|||||||
|
|
||||||
extension Agent {
|
extension Agent {
|
||||||
|
|
||||||
/// Finds a ``Secret`` matching a specified hash whos signature was requested.
|
|
||||||
/// - Parameter hash: The hash to match against.
|
|
||||||
/// - Returns: A ``Secret`` and the ``SecretStore`` containing it, if a match is found.
|
|
||||||
func secret(matching hash: Data) -> (AnySecretStore, AnySecret)? {
|
func secret(matching hash: Data) -> (AnySecretStore, AnySecret)? {
|
||||||
storeList.stores.compactMap { store -> (AnySecretStore, AnySecret)? in
|
storeList.stores.compactMap { store -> (AnySecretStore, AnySecret)? in
|
||||||
let allMatching = store.secrets.filter { secret in
|
let allMatching = store.secrets.filter { secret in
|
||||||
@ -183,7 +165,6 @@ extension Agent {
|
|||||||
|
|
||||||
extension Agent {
|
extension Agent {
|
||||||
|
|
||||||
/// An error involving agent operations..
|
|
||||||
enum AgentError: Error {
|
enum AgentError: Error {
|
||||||
case unhandledType
|
case unhandledType
|
||||||
case noMatchingKey
|
case noMatchingKey
|
@ -1,21 +1,15 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Protocol abstraction of the reading aspects of FileHandle.
|
|
||||||
public protocol FileHandleReader {
|
public protocol FileHandleReader {
|
||||||
|
|
||||||
/// Gets data that is available for reading.
|
|
||||||
var availableData: Data { get }
|
var availableData: Data { get }
|
||||||
/// A file descriptor of the handle.
|
|
||||||
var fileDescriptor: Int32 { get }
|
var fileDescriptor: Int32 { get }
|
||||||
/// The process ID of the process coonnected to the other end of the FileHandle.
|
|
||||||
var pidOfConnectedProcess: Int32 { get }
|
var pidOfConnectedProcess: Int32 { get }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Protocol abstraction of the writing aspects of FileHandle.
|
|
||||||
public protocol FileHandleWriter {
|
public protocol FileHandleWriter {
|
||||||
|
|
||||||
/// Writes data to the handle.
|
|
||||||
func write(_ data: Data)
|
func write(_ data: Data)
|
||||||
|
|
||||||
}
|
}
|
24
SecretAgentKit/Info.plist
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(CI_VERSION)</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>$(PRODUCT_NAME) is MIT Licensed.</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
@ -1,13 +1,10 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// A namespace for the SSH Agent Protocol, as described in https://tools.ietf.org/id/draft-miller-ssh-agent-01.html
|
|
||||||
public enum SSHAgent {}
|
public enum SSHAgent {}
|
||||||
|
|
||||||
extension SSHAgent {
|
extension SSHAgent {
|
||||||
|
|
||||||
/// The type of the SSH Agent Request, as described in https://tools.ietf.org/id/draft-miller-ssh-agent-01.html#rfc.section.5.1
|
|
||||||
public enum RequestType: UInt8, CustomDebugStringConvertible {
|
public enum RequestType: UInt8, CustomDebugStringConvertible {
|
||||||
|
|
||||||
case requestIdentities = 11
|
case requestIdentities = 11
|
||||||
case signRequest = 13
|
case signRequest = 13
|
||||||
|
|
||||||
@ -21,9 +18,7 @@ extension SSHAgent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The type of the SSH Agent Response, as described in https://tools.ietf.org/id/draft-miller-ssh-agent-01.html#rfc.section.5.1
|
|
||||||
public enum ResponseType: UInt8, CustomDebugStringConvertible {
|
public enum ResponseType: UInt8, CustomDebugStringConvertible {
|
||||||
|
|
||||||
case agentFailure = 5
|
case agentFailure = 5
|
||||||
case agentIdentitiesAnswer = 12
|
case agentIdentitiesAnswer = 12
|
||||||
case agentSignResponse = 14
|
case agentSignResponse = 14
|
45
SecretAgentKit/SigningRequestProvenance.swift
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import Foundation
|
||||||
|
import AppKit
|
||||||
|
|
||||||
|
public struct SigningRequestProvenance: Equatable {
|
||||||
|
|
||||||
|
public var chain: [Process]
|
||||||
|
public init(root: Process) {
|
||||||
|
self.chain = [root]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SigningRequestProvenance {
|
||||||
|
|
||||||
|
public var origin: Process {
|
||||||
|
chain.last!
|
||||||
|
}
|
||||||
|
|
||||||
|
public var intact: Bool {
|
||||||
|
return chain.reduce(true) { $0 && $1.validSignature }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SigningRequestProvenance {
|
||||||
|
|
||||||
|
public struct Process: Equatable {
|
||||||
|
|
||||||
|
public let pid: Int32
|
||||||
|
public let name: String
|
||||||
|
public let path: String
|
||||||
|
public let validSignature: Bool
|
||||||
|
let parentPID: Int32?
|
||||||
|
|
||||||
|
init(pid: Int32, name: String, path: String, validSignature: Bool, parentPID: Int32?) {
|
||||||
|
self.pid = pid
|
||||||
|
self.name = name
|
||||||
|
self.path = path
|
||||||
|
self.validSignature = validSignature
|
||||||
|
self.parentPID = parentPID
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
39
SecretAgentKit/SigningRequestTracer.swift
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import Foundation
|
||||||
|
import AppKit
|
||||||
|
import Security
|
||||||
|
|
||||||
|
struct SigningRequestTracer {
|
||||||
|
|
||||||
|
func provenance(from fileHandleReader: FileHandleReader) -> SigningRequestProvenance {
|
||||||
|
let firstInfo = process(from: fileHandleReader.pidOfConnectedProcess)
|
||||||
|
|
||||||
|
var provenance = SigningRequestProvenance(root: firstInfo)
|
||||||
|
while NSRunningApplication(processIdentifier: provenance.origin.pid) == nil && provenance.origin.parentPID != nil {
|
||||||
|
provenance.chain.append(process(from: provenance.origin.parentPID!))
|
||||||
|
}
|
||||||
|
return provenance
|
||||||
|
}
|
||||||
|
|
||||||
|
func pidAndNameInfo(from pid: Int32) -> kinfo_proc {
|
||||||
|
var len = MemoryLayout<kinfo_proc>.size
|
||||||
|
let infoPointer = UnsafeMutableRawPointer.allocate(byteCount: len, alignment: 1)
|
||||||
|
var name: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, pid]
|
||||||
|
sysctl(&name, UInt32(name.count), infoPointer, &len, nil, 0)
|
||||||
|
return infoPointer.load(as: kinfo_proc.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func process(from pid: Int32) -> SigningRequestProvenance.Process {
|
||||||
|
var pidAndNameInfo = self.pidAndNameInfo(from: pid)
|
||||||
|
let ppid = pidAndNameInfo.kp_eproc.e_ppid != 0 ? pidAndNameInfo.kp_eproc.e_ppid : nil
|
||||||
|
let procName = String(cString: &pidAndNameInfo.kp_proc.p_comm.0)
|
||||||
|
let pathPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: Int(MAXPATHLEN))
|
||||||
|
_ = proc_pidpath(pid, pathPointer, UInt32(MAXPATHLEN))
|
||||||
|
let path = String(cString: pathPointer)
|
||||||
|
var secCode: Unmanaged<SecCode>!
|
||||||
|
let flags: SecCSFlags = [.considerExpiration, .enforceRevocationChecks]
|
||||||
|
SecCodeCreateWithPID(pid, SecCSFlags(), &secCode)
|
||||||
|
let valid = SecCodeCheckValidity(secCode.takeRetainedValue(), flags, nil) == errSecSuccess
|
||||||
|
return SigningRequestProvenance.Process(pid: pid, name: procName, path: path, validSignature: valid, parentPID: ppid)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
9
SecretAgentKit/SigningWitness.swift
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import Foundation
|
||||||
|
import SecretKit
|
||||||
|
|
||||||
|
public protocol SigningWitness {
|
||||||
|
|
||||||
|
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, by provenance: SigningRequestProvenance) throws
|
||||||
|
func witness(accessTo secret: AnySecret, by provenance: SigningRequestProvenance) throws
|
||||||
|
|
||||||
|
}
|
@ -1,47 +1,39 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import OSLog
|
import OSLog
|
||||||
|
|
||||||
/// A controller that manages socket configuration and request dispatching.
|
|
||||||
public class SocketController {
|
public class SocketController {
|
||||||
|
|
||||||
/// The active FileHandle.
|
fileprivate var fileHandle: FileHandle?
|
||||||
private var fileHandle: FileHandle?
|
fileprivate var port: SocketPort?
|
||||||
/// A handler that will be notified when a new read/write handle is available.
|
|
||||||
public var handler: ((FileHandleReader, FileHandleWriter) -> Void)?
|
public var handler: ((FileHandleReader, FileHandleWriter) -> Void)?
|
||||||
|
|
||||||
/// Initializes a socket controller with a specified path.
|
|
||||||
/// - Parameter path: The path to use as a socket.
|
|
||||||
public init(path: String) {
|
public init(path: String) {
|
||||||
Logger().debug("Socket controller setting up at \(path)")
|
os_log(.debug, "Socket controller setting up at %@", path)
|
||||||
if let _ = try? FileManager.default.removeItem(atPath: path) {
|
if let _ = try? FileManager.default.removeItem(atPath: path) {
|
||||||
Logger().debug("Socket controller removed existing socket")
|
os_log(.debug, "Socket controller removed existing socket")
|
||||||
}
|
}
|
||||||
let exists = FileManager.default.fileExists(atPath: path)
|
let exists = FileManager.default.fileExists(atPath: path)
|
||||||
assert(!exists)
|
assert(!exists)
|
||||||
Logger().debug("Socket controller path is clear")
|
os_log(.debug, "Socket controller path is clear")
|
||||||
|
port = socketPort(at: path)
|
||||||
configureSocket(at: path)
|
configureSocket(at: path)
|
||||||
Logger().debug("Socket listening at \(path)")
|
os_log(.debug, "Socket listening at %@", path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configures the socket and a corresponding FileHandle.
|
|
||||||
/// - Parameter path: The path to use as a socket.
|
|
||||||
func configureSocket(at path: String) {
|
func configureSocket(at path: String) {
|
||||||
let port = socketPort(at: path)
|
guard let port = port else { return }
|
||||||
fileHandle = FileHandle(fileDescriptor: port.socket, closeOnDealloc: true)
|
fileHandle = FileHandle(fileDescriptor: port.socket, closeOnDealloc: true)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(handleConnectionAccept(notification:)), name: .NSFileHandleConnectionAccepted, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(handleConnectionAccept(notification:)), name: .NSFileHandleConnectionAccepted, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(handleConnectionDataAvailable(notification:)), name: .NSFileHandleDataAvailable, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(handleConnectionDataAvailable(notification:)), name: .NSFileHandleDataAvailable, object: nil)
|
||||||
fileHandle?.acceptConnectionInBackgroundAndNotify(forModes: [RunLoop.current.currentMode!])
|
fileHandle?.acceptConnectionInBackgroundAndNotify(forModes: [RunLoop.current.currentMode!])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a SocketPort for a path.
|
|
||||||
/// - Parameter path: The path to use as a socket.
|
|
||||||
/// - Returns: A configured SocketPort.
|
|
||||||
func socketPort(at path: String) -> SocketPort {
|
func socketPort(at path: String) -> SocketPort {
|
||||||
var addr = sockaddr_un()
|
var addr = sockaddr_un()
|
||||||
addr.sun_family = sa_family_t(AF_UNIX)
|
addr.sun_family = sa_family_t(AF_UNIX)
|
||||||
|
|
||||||
var len: Int = 0
|
var len: Int = 0
|
||||||
withUnsafeMutablePointer(to: &addr.sun_path.0) { pointer in
|
_ = withUnsafeMutablePointer(to: &addr.sun_path.0) { pointer in
|
||||||
path.withCString { cstring in
|
path.withCString { cstring in
|
||||||
len = strlen(cstring)
|
len = strlen(cstring)
|
||||||
strncpy(pointer, cstring, len)
|
strncpy(pointer, cstring, len)
|
||||||
@ -50,29 +42,25 @@ public class SocketController {
|
|||||||
addr.sun_len = UInt8(len+2)
|
addr.sun_len = UInt8(len+2)
|
||||||
|
|
||||||
var data: Data!
|
var data: Data!
|
||||||
withUnsafePointer(to: &addr) { pointer in
|
_ = withUnsafePointer(to: &addr) { pointer in
|
||||||
data = Data(bytes: pointer, count: MemoryLayout<sockaddr_un>.size)
|
data = Data(bytes: pointer, count: MemoryLayout<sockaddr_un>.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
return SocketPort(protocolFamily: AF_UNIX, socketType: SOCK_STREAM, protocol: 0, address: data)!
|
return SocketPort(protocolFamily: AF_UNIX, socketType: SOCK_STREAM, protocol: 0, address: data)!
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles a new connection being accepted, invokes the handler, and prepares to accept new connections.
|
|
||||||
/// - Parameter notification: A `Notification` that triggered the call.
|
|
||||||
@objc func handleConnectionAccept(notification: Notification) {
|
@objc func handleConnectionAccept(notification: Notification) {
|
||||||
Logger().debug("Socket controller accepted connection")
|
os_log(.debug, "Socket controller accepted connection")
|
||||||
guard let new = notification.userInfo?[NSFileHandleNotificationFileHandleItem] as? FileHandle else { return }
|
guard let new = notification.userInfo?[NSFileHandleNotificationFileHandleItem] as? FileHandle else { return }
|
||||||
handler?(new, new)
|
handler?(new, new)
|
||||||
new.waitForDataInBackgroundAndNotify()
|
new.waitForDataInBackgroundAndNotify()
|
||||||
fileHandle?.acceptConnectionInBackgroundAndNotify(forModes: [RunLoop.current.currentMode!])
|
fileHandle?.acceptConnectionInBackgroundAndNotify(forModes: [RunLoop.current.currentMode!])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles a new connection providing data and invokes the handler callback.
|
|
||||||
/// - Parameter notification: A `Notification` that triggered the call.
|
|
||||||
@objc func handleConnectionDataAvailable(notification: Notification) {
|
@objc func handleConnectionDataAvailable(notification: Notification) {
|
||||||
Logger().debug("Socket controller has new data available")
|
os_log(.debug, "Socket controller has new data available")
|
||||||
guard let new = notification.object as? FileHandle else { return }
|
guard let new = notification.object as? FileHandle else { return }
|
||||||
Logger().debug("Socket controller received new file handle")
|
os_log(.debug, "Socket controller received new file handle")
|
||||||
handler?(new, new)
|
handler?(new, new)
|
||||||
}
|
}
|
||||||
|
|
@ -106,7 +106,7 @@ class AgentTests: XCTestCase {
|
|||||||
let agent = Agent(storeList: list, witness: witness)
|
let agent = Agent(storeList: list, witness: witness)
|
||||||
agent.handle(reader: stubReader, writer: stubWriter)
|
agent.handle(reader: stubReader, writer: stubWriter)
|
||||||
XCTAssertEqual(witnessTrace, speakNowTrace)
|
XCTAssertEqual(witnessTrace, speakNowTrace)
|
||||||
XCTAssertEqual(witnessTrace.origin.displayName, "Finder")
|
XCTAssertEqual(witnessTrace.origin.name, "Finder")
|
||||||
XCTAssertEqual(witnessTrace.origin.validSignature, true)
|
XCTAssertEqual(witnessTrace.origin.validSignature, true)
|
||||||
XCTAssertEqual(witnessTrace.origin.parentPID, 1)
|
XCTAssertEqual(witnessTrace.origin.parentPID, 1)
|
||||||
}
|
}
|
@ -1,4 +1,3 @@
|
|||||||
import Foundation
|
|
||||||
import SecretAgentKit
|
import SecretAgentKit
|
||||||
|
|
||||||
class StubFileHandleWriter: FileHandleWriter {
|
class StubFileHandleWriter: FileHandleWriter {
|
@ -1,4 +1,3 @@
|
|||||||
import Foundation
|
|
||||||
import SecretKit
|
import SecretKit
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
|
|
||||||
@ -49,9 +48,9 @@ extension Stub {
|
|||||||
print("Public Key OpenSSH: \(OpenSSHKeyWriter().openSSHString(secret: secret))")
|
print("Public Key OpenSSH: \(OpenSSHKeyWriter().openSSHString(secret: secret))")
|
||||||
}
|
}
|
||||||
|
|
||||||
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) throws -> SignedData {
|
public func sign(data: Data, with secret: Secret) throws -> Data {
|
||||||
guard !shouldThrow else {
|
guard !shouldThrow else {
|
||||||
throw NSError(domain: "test", code: 0, userInfo: nil)
|
throw NSError()
|
||||||
}
|
}
|
||||||
let privateKey = SecKeyCreateWithData(secret.privateKey as CFData, [
|
let privateKey = SecKeyCreateWithData(secret.privateKey as CFData, [
|
||||||
kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
|
kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
|
||||||
@ -68,10 +67,7 @@ extension Stub {
|
|||||||
default:
|
default:
|
||||||
fatalError()
|
fatalError()
|
||||||
}
|
}
|
||||||
return SignedData(data: SecKeyCreateSignature(privateKey, signatureAlgorithm, data as CFData, nil)! as Data, requiredAuthentication: false)
|
return SecKeyCreateSignature(privateKey, signatureAlgorithm, data as CFData, nil)! as Data
|
||||||
}
|
|
||||||
|
|
||||||
public func persistAuthentication(secret: Stub.Secret, forDuration duration: TimeInterval) throws {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -10,14 +10,14 @@ struct StubWitness {
|
|||||||
|
|
||||||
extension StubWitness: SigningWitness {
|
extension StubWitness: SigningWitness {
|
||||||
|
|
||||||
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance) throws {
|
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, by provenance: SigningRequestProvenance) throws {
|
||||||
let objection = speakNow(secret, provenance)
|
let objection = speakNow(secret, provenance)
|
||||||
if objection {
|
if objection {
|
||||||
throw TheresMyChance()
|
throw TheresMyChance()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, requiredAuthentication: Bool) throws {
|
func witness(accessTo secret: AnySecret, by provenance: SigningRequestProvenance) throws {
|
||||||
witness(secret, provenance)
|
witness(secret, provenance)
|
||||||
}
|
}
|
||||||
|
|
@ -1,15 +1,14 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Type eraser for Secret.
|
|
||||||
public struct AnySecret: Secret {
|
public struct AnySecret: Secret {
|
||||||
|
|
||||||
let base: Any
|
let base: Any
|
||||||
private let hashable: AnyHashable
|
fileprivate let hashable: AnyHashable
|
||||||
private let _id: () -> AnyHashable
|
fileprivate let _id: () -> AnyHashable
|
||||||
private let _name: () -> String
|
fileprivate let _name: () -> String
|
||||||
private let _algorithm: () -> Algorithm
|
fileprivate let _algorithm: () -> Algorithm
|
||||||
private let _keySize: () -> Int
|
fileprivate let _keySize: () -> Int
|
||||||
private let _publicKey: () -> Data
|
fileprivate let _publicKey: () -> Data
|
||||||
|
|
||||||
public init<T>(_ secret: T) where T: Secret {
|
public init<T>(_ secret: T) where T: Secret {
|
||||||
if let secret = secret as? AnySecret {
|
if let secret = secret as? AnySecret {
|
@ -1,18 +1,15 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
/// Type eraser for SecretStore.
|
|
||||||
public class AnySecretStore: SecretStore {
|
public class AnySecretStore: SecretStore {
|
||||||
|
|
||||||
let base: Any
|
let base: Any
|
||||||
private let _isAvailable: () -> Bool
|
fileprivate let _isAvailable: () -> Bool
|
||||||
private let _id: () -> UUID
|
fileprivate let _id: () -> UUID
|
||||||
private let _name: () -> String
|
fileprivate let _name: () -> String
|
||||||
private let _secrets: () -> [AnySecret]
|
fileprivate let _secrets: () -> [AnySecret]
|
||||||
private let _sign: (Data, AnySecret, SigningRequestProvenance) throws -> SignedData
|
fileprivate let _sign: (Data, AnySecret) throws -> Data
|
||||||
private let _persistAuthentication: (AnySecret, TimeInterval) throws -> Void
|
fileprivate var sink: AnyCancellable?
|
||||||
|
|
||||||
private var sink: AnyCancellable?
|
|
||||||
|
|
||||||
public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStore {
|
public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStore {
|
||||||
base = secretStore
|
base = secretStore
|
||||||
@ -20,8 +17,7 @@ public class AnySecretStore: SecretStore {
|
|||||||
_name = { secretStore.name }
|
_name = { secretStore.name }
|
||||||
_id = { secretStore.id }
|
_id = { secretStore.id }
|
||||||
_secrets = { secretStore.secrets.map { AnySecret($0) } }
|
_secrets = { secretStore.secrets.map { AnySecret($0) } }
|
||||||
_sign = { try secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType, for: $2) }
|
_sign = { try secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType) }
|
||||||
_persistAuthentication = { try secretStore.persistAuthentication(secret: $0.base as! SecretStoreType.SecretType, forDuration: $1) }
|
|
||||||
sink = secretStore.objectWillChange.sink { _ in
|
sink = secretStore.objectWillChange.sink { _ in
|
||||||
self.objectWillChange.send()
|
self.objectWillChange.send()
|
||||||
}
|
}
|
||||||
@ -43,28 +39,20 @@ public class AnySecretStore: SecretStore {
|
|||||||
return _secrets()
|
return _secrets()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func sign(data: Data, with secret: AnySecret, for provenance: SigningRequestProvenance) throws -> SignedData {
|
public func sign(data: Data, with secret: AnySecret) throws -> Data {
|
||||||
try _sign(data, secret, provenance)
|
try _sign(data, secret)
|
||||||
}
|
|
||||||
|
|
||||||
public func persistAuthentication(secret: AnySecret, forDuration duration: TimeInterval) throws {
|
|
||||||
try _persistAuthentication(secret, duration)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable {
|
public class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable {
|
||||||
|
|
||||||
private let _create: (String, Bool) throws -> Void
|
fileprivate let _create: (String, Bool) throws -> Void
|
||||||
private let _delete: (AnySecret) throws -> Void
|
fileprivate let _delete: (AnySecret) throws -> Void
|
||||||
private let _update: (AnySecret, String) throws -> Void
|
|
||||||
private let _reload: () throws -> Void
|
|
||||||
|
|
||||||
public init<SecretStoreType>(modifiable secretStore: SecretStoreType) where SecretStoreType: SecretStoreModifiable {
|
public init<SecretStoreType>(modifiable secretStore: SecretStoreType) where SecretStoreType: SecretStoreModifiable {
|
||||||
_create = { try secretStore.create(name: $0, requiresAuthentication: $1) }
|
_create = { try secretStore.create(name: $0, requiresAuthentication: $1) }
|
||||||
_delete = { try secretStore.delete(secret: $0.base as! SecretStoreType.SecretType) }
|
_delete = { try secretStore.delete(secret: $0.base as! SecretStoreType.SecretType) }
|
||||||
_update = { try secretStore.update(secret: $0.base as! SecretStoreType.SecretType, name: $1) }
|
|
||||||
_reload = { try secretStore.reload() }
|
|
||||||
super.init(secretStore)
|
super.init(secretStore)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,12 +64,4 @@ public class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable {
|
|||||||
try _delete(secret)
|
try _delete(secret)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func update(secret: AnySecret, name: String) throws {
|
|
||||||
try _update(secret, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func reload() throws {
|
|
||||||
try _reload()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
49
SecretKit/Common/OpenSSH/OpenSSHKeyWriter.swift
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import Foundation
|
||||||
|
import CryptoKit
|
||||||
|
|
||||||
|
// For the moment, only supports ecdsa-sha2-nistp256 and ecdsa-sha2-nistp386 keys
|
||||||
|
public struct OpenSSHKeyWriter {
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public func data<SecretType: Secret>(secret: SecretType) -> Data {
|
||||||
|
lengthAndData(of: curveType(for: secret.algorithm, length: secret.keySize).data(using: .utf8)!) +
|
||||||
|
lengthAndData(of: curveIdentifier(for: secret.algorithm, length: secret.keySize).data(using: .utf8)!) +
|
||||||
|
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 openSSHFingerprint<SecretType: Secret>(secret: SecretType) -> String {
|
||||||
|
Insecure.MD5.hash(data: data(secret: secret))
|
||||||
|
.compactMap { ("0" + String($0, radix: 16, uppercase: false)).suffix(2) }
|
||||||
|
.joined(separator: ":")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension OpenSSHKeyWriter {
|
||||||
|
|
||||||
|
public func lengthAndData(of data: Data) -> Data {
|
||||||
|
let rawLength = UInt32(data.count)
|
||||||
|
var endian = rawLength.bigEndian
|
||||||
|
return Data(bytes: &endian, count: UInt32.bitWidth/8) + data
|
||||||
|
}
|
||||||
|
|
||||||
|
public func curveIdentifier(for algorithm: Algorithm, length: Int) -> String {
|
||||||
|
switch algorithm {
|
||||||
|
case .ellipticCurve:
|
||||||
|
return "nistp" + String(describing: length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func curveType(for algorithm: Algorithm, length: Int) -> String {
|
||||||
|
switch algorithm {
|
||||||
|
case .ellipticCurve:
|
||||||
|
return "ecdsa-sha2-nistp" + String(describing: length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,18 +1,13 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Reads OpenSSH protocol data.
|
|
||||||
public class OpenSSHReader {
|
public class OpenSSHReader {
|
||||||
|
|
||||||
var remaining: Data
|
var remaining: Data
|
||||||
|
|
||||||
/// Initialize the reader with an OpenSSH data payload.
|
|
||||||
/// - Parameter data: The data to read.
|
|
||||||
public init(data: Data) {
|
public init(data: Data) {
|
||||||
remaining = Data(data)
|
remaining = Data(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads the next chunk of data from the playload.
|
|
||||||
/// - Returns: The next chunk of data.
|
|
||||||
public func readNextChunk() -> Data {
|
public func readNextChunk() -> Data {
|
||||||
let lengthRange = 0..<(UInt32.bitWidth/8)
|
let lengthRange = 0..<(UInt32.bitWidth/8)
|
||||||
let lengthChunk = remaining[lengthRange]
|
let lengthChunk = remaining[lengthRange]
|
@ -1,32 +1,25 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
/// A "Store Store," which holds a list of type-erased stores.
|
|
||||||
public class SecretStoreList: ObservableObject {
|
public class SecretStoreList: ObservableObject {
|
||||||
|
|
||||||
/// The Stores managed by the SecretStoreList.
|
|
||||||
@Published public var stores: [AnySecretStore] = []
|
@Published public var stores: [AnySecretStore] = []
|
||||||
/// A modifiable store, if one is available.
|
|
||||||
@Published public var modifiableStore: AnySecretStoreModifiable?
|
@Published public var modifiableStore: AnySecretStoreModifiable?
|
||||||
private var sinks: [AnyCancellable] = []
|
fileprivate var sinks: [AnyCancellable] = []
|
||||||
|
|
||||||
/// Initializes a SecretStoreList.
|
|
||||||
public init() {
|
public init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a non-type-erased SecretStore to the list.
|
|
||||||
public func add<SecretStoreType: SecretStore>(store: SecretStoreType) {
|
public func add<SecretStoreType: SecretStore>(store: SecretStoreType) {
|
||||||
addInternal(store: AnySecretStore(store))
|
addInternal(store: AnySecretStore(store))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a non-type-erased modifiable SecretStore.
|
|
||||||
public func add<SecretStoreType: SecretStoreModifiable>(store: SecretStoreType) {
|
public func add<SecretStoreType: SecretStoreModifiable>(store: SecretStoreType) {
|
||||||
let modifiable = AnySecretStoreModifiable(modifiable: store)
|
let modifiable = AnySecretStoreModifiable(modifiable: store)
|
||||||
modifiableStore = modifiable
|
modifiableStore = modifiable
|
||||||
addInternal(store: modifiable)
|
addInternal(store: modifiable)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A boolean describing whether there are any Stores available.
|
|
||||||
public var anyAvailable: Bool {
|
public var anyAvailable: Bool {
|
||||||
stores.reduce(false, { $0 || $1.isAvailable })
|
stores.reduce(false, { $0 || $1.isAvailable })
|
||||||
}
|
}
|
||||||
@ -35,7 +28,7 @@ public class SecretStoreList: ObservableObject {
|
|||||||
|
|
||||||
extension SecretStoreList {
|
extension SecretStoreList {
|
||||||
|
|
||||||
private func addInternal(store: AnySecretStore) {
|
fileprivate func addInternal(store: AnySecretStore) {
|
||||||
stores.append(store)
|
stores.append(store)
|
||||||
let sink = store.objectWillChange.sink {
|
let sink = store.objectWillChange.sink {
|
||||||
self.objectWillChange.send()
|
self.objectWillChange.send()
|
@ -1,26 +1,14 @@
|
|||||||
import Foundation
|
|
||||||
|
|
||||||
/// The base protocol for describing a Secret
|
|
||||||
public protocol Secret: Identifiable, Hashable {
|
public protocol Secret: Identifiable, Hashable {
|
||||||
|
|
||||||
/// A user-facing string identifying the Secret.
|
|
||||||
var name: String { get }
|
var name: String { get }
|
||||||
/// The algorithm this secret uses.
|
|
||||||
var algorithm: Algorithm { get }
|
var algorithm: Algorithm { get }
|
||||||
/// The key size for the secret.
|
|
||||||
var keySize: Int { get }
|
var keySize: Int { get }
|
||||||
/// The public key data for the secret.
|
|
||||||
var publicKey: Data { get }
|
var publicKey: Data { get }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The type of algorithm the Secret uses. Currently, only elliptic curve algorithms are supported.
|
|
||||||
public enum Algorithm: Hashable {
|
public enum Algorithm: Hashable {
|
||||||
|
|
||||||
case ellipticCurve
|
case ellipticCurve
|
||||||
|
|
||||||
/// Initializes the Algorithm with a secAttr representation of an algorithm.
|
|
||||||
/// - Parameter secAttr: the secAttr, represented as an NSNumber.
|
|
||||||
public init(secAttr: NSNumber) {
|
public init(secAttr: NSNumber) {
|
||||||
let secAttrString = secAttr.stringValue as CFString
|
let secAttrString = secAttr.stringValue as CFString
|
||||||
switch secAttrString {
|
switch secAttrString {
|
27
SecretKit/Common/Types/SecretStore.swift
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import Combine
|
||||||
|
|
||||||
|
public protocol SecretStore: ObservableObject, Identifiable {
|
||||||
|
|
||||||
|
associatedtype SecretType: Secret
|
||||||
|
|
||||||
|
var isAvailable: Bool { get }
|
||||||
|
var id: UUID { get }
|
||||||
|
var name: String { get }
|
||||||
|
var secrets: [SecretType] { get }
|
||||||
|
|
||||||
|
func sign(data: Data, with secret: SecretType) throws -> Data
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public protocol SecretStoreModifiable: SecretStore {
|
||||||
|
|
||||||
|
func create(name: String, requiresAuthentication: Bool) throws
|
||||||
|
func delete(secret: SecretType) throws
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NSNotification.Name {
|
||||||
|
|
||||||
|
static let secretStoreUpdated = NSNotification.Name("com.maxgoedjen.Secretive.secretStore.updated")
|
||||||
|
|
||||||
|
}
|
24
SecretKit/Info.plist
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(CI_VERSION)</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>$(PRODUCT_NAME) is MIT Licensed.</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
11
SecretKit/SecretKit.h
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
//! Project version number for SecretKit.
|
||||||
|
FOUNDATION_EXPORT double SecretKitVersionNumber;
|
||||||
|
|
||||||
|
//! Project version string for SecretKit.
|
||||||
|
FOUNDATION_EXPORT const unsigned char SecretKitVersionString[];
|
||||||
|
|
||||||
|
// In this header, you should import all the public headers of your framework using statements like #import <SecretKit/PublicHeader.h>
|
||||||
|
|
||||||
|
|
1
SecretKit/SecureEnclave/SecureEnclave.swift
Normal file
@ -0,0 +1 @@
|
|||||||
|
public enum SecureEnclave {}
|
@ -1,10 +1,8 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
import SecretKit
|
|
||||||
|
|
||||||
extension SecureEnclave {
|
extension SecureEnclave {
|
||||||
|
|
||||||
/// An implementation of Secret backed by the Secure Enclave.
|
|
||||||
public struct Secret: SecretKit.Secret {
|
public struct Secret: SecretKit.Secret {
|
||||||
|
|
||||||
public let id: Data
|
public let id: Data
|
185
SecretKit/SecureEnclave/SecureEnclaveStore.swift
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
import Foundation
|
||||||
|
import Security
|
||||||
|
import CryptoTokenKit
|
||||||
|
|
||||||
|
extension SecureEnclave {
|
||||||
|
|
||||||
|
public class Store: SecretStoreModifiable {
|
||||||
|
|
||||||
|
public var isAvailable: Bool {
|
||||||
|
// For some reason, as of build time, CryptoKit.SecureEnclave.isAvailable always returns false
|
||||||
|
// error msg "Received error sending GET UNIQUE DEVICE command"
|
||||||
|
// Verify it with TKTokenWatcher manually.
|
||||||
|
TKTokenWatcher().tokenIDs.contains("com.apple.setoken")
|
||||||
|
}
|
||||||
|
public let id = UUID()
|
||||||
|
public let name = NSLocalizedString("Secure Enclave", comment: "Secure Enclave")
|
||||||
|
@Published public fileprivate(set) var secrets: [Secret] = []
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
DistributedNotificationCenter.default().addObserver(forName: .secretStoreUpdated, object: nil, queue: .main) { _ in
|
||||||
|
self.reloadSecrets(notify: false)
|
||||||
|
}
|
||||||
|
loadSecrets()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Public API
|
||||||
|
|
||||||
|
public func create(name: String, requiresAuthentication: Bool) throws {
|
||||||
|
var accessError: SecurityError?
|
||||||
|
let flags: SecAccessControlCreateFlags
|
||||||
|
if requiresAuthentication {
|
||||||
|
flags = [.privateKeyUsage, .userPresence]
|
||||||
|
} else {
|
||||||
|
flags = .privateKeyUsage
|
||||||
|
}
|
||||||
|
let access =
|
||||||
|
SecAccessControlCreateWithFlags(kCFAllocatorDefault,
|
||||||
|
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
||||||
|
flags,
|
||||||
|
&accessError) as Any
|
||||||
|
if let error = accessError {
|
||||||
|
throw error.takeRetainedValue() as Error
|
||||||
|
}
|
||||||
|
|
||||||
|
let attributes = [
|
||||||
|
kSecAttrLabel: name,
|
||||||
|
kSecAttrKeyType: Constants.keyType,
|
||||||
|
kSecAttrTokenID: kSecAttrTokenIDSecureEnclave,
|
||||||
|
kSecAttrApplicationTag: Constants.keyTag,
|
||||||
|
kSecPrivateKeyAttrs: [
|
||||||
|
kSecAttrIsPermanent: true,
|
||||||
|
kSecAttrAccessControl: access
|
||||||
|
]
|
||||||
|
] as CFDictionary
|
||||||
|
|
||||||
|
var privateKey: SecKey? = nil
|
||||||
|
var publicKey: SecKey? = nil
|
||||||
|
let status = SecKeyGeneratePair(attributes, &publicKey, &privateKey)
|
||||||
|
guard privateKey != nil, let pk = publicKey else {
|
||||||
|
throw KeychainError(statusCode: status)
|
||||||
|
}
|
||||||
|
try savePublicKey(pk, name: name)
|
||||||
|
reloadSecrets()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func delete(secret: Secret) throws {
|
||||||
|
let deleteAttributes = [
|
||||||
|
kSecClass: kSecClassKey,
|
||||||
|
kSecAttrApplicationLabel: secret.id as CFData
|
||||||
|
] as CFDictionary
|
||||||
|
let status = SecItemDelete(deleteAttributes)
|
||||||
|
if status != errSecSuccess {
|
||||||
|
throw KeychainError(statusCode: status)
|
||||||
|
}
|
||||||
|
reloadSecrets()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func sign(data: Data, with secret: SecretType) throws -> Data {
|
||||||
|
let attributes = [
|
||||||
|
kSecClass: kSecClassKey,
|
||||||
|
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
|
||||||
|
kSecAttrApplicationLabel: secret.id as CFData,
|
||||||
|
kSecAttrKeyType: Constants.keyType,
|
||||||
|
kSecAttrTokenID: kSecAttrTokenIDSecureEnclave,
|
||||||
|
kSecAttrApplicationTag: Constants.keyTag,
|
||||||
|
kSecReturnRef: true
|
||||||
|
] as CFDictionary
|
||||||
|
var untyped: CFTypeRef?
|
||||||
|
let status = SecItemCopyMatching(attributes, &untyped)
|
||||||
|
if status != errSecSuccess {
|
||||||
|
throw KeychainError(statusCode: status)
|
||||||
|
}
|
||||||
|
guard let untypedSafe = untyped else {
|
||||||
|
throw KeychainError(statusCode: errSecSuccess)
|
||||||
|
}
|
||||||
|
let key = untypedSafe as! SecKey
|
||||||
|
var signError: SecurityError?
|
||||||
|
guard let signature = SecKeyCreateSignature(key, .ecdsaSignatureMessageX962SHA256, data as CFData, &signError) else {
|
||||||
|
throw SigningError(error: signError)
|
||||||
|
}
|
||||||
|
return signature as Data
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SecureEnclave.Store {
|
||||||
|
|
||||||
|
fileprivate func reloadSecrets(notify: Bool = true) {
|
||||||
|
secrets.removeAll()
|
||||||
|
loadSecrets()
|
||||||
|
if notify {
|
||||||
|
DistributedNotificationCenter.default().post(name: .secretStoreUpdated, object: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func loadSecrets() {
|
||||||
|
let attributes = [
|
||||||
|
kSecClass: kSecClassKey,
|
||||||
|
kSecAttrKeyType: SecureEnclave.Constants.keyType,
|
||||||
|
kSecAttrApplicationTag: SecureEnclave.Constants.keyTag,
|
||||||
|
kSecAttrKeyClass: kSecAttrKeyClassPublic,
|
||||||
|
kSecReturnRef: true,
|
||||||
|
kSecMatchLimit: kSecMatchLimitAll,
|
||||||
|
kSecReturnAttributes: true
|
||||||
|
] as CFDictionary
|
||||||
|
var untyped: CFTypeRef?
|
||||||
|
SecItemCopyMatching(attributes, &untyped)
|
||||||
|
guard let typed = untyped as? [[CFString: Any]] else { return }
|
||||||
|
let wrapped: [SecureEnclave.Secret] = typed.map {
|
||||||
|
let name = $0[kSecAttrLabel] as? String ?? "Unnamed"
|
||||||
|
let id = $0[kSecAttrApplicationLabel] as! Data
|
||||||
|
let publicKeyRef = $0[kSecValueRef] as! SecKey
|
||||||
|
let publicKeyAttributes = SecKeyCopyAttributes(publicKeyRef) as! [CFString: Any]
|
||||||
|
let publicKey = publicKeyAttributes[kSecValueData] as! Data
|
||||||
|
return SecureEnclave.Secret(id: id, name: name, publicKey: publicKey)
|
||||||
|
}
|
||||||
|
secrets.append(contentsOf: wrapped)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func savePublicKey(_ publicKey: SecKey, name: String) throws {
|
||||||
|
let attributes = [
|
||||||
|
kSecClass: kSecClassKey,
|
||||||
|
kSecAttrKeyType: SecureEnclave.Constants.keyType,
|
||||||
|
kSecAttrKeyClass: kSecAttrKeyClassPublic,
|
||||||
|
kSecAttrApplicationTag: SecureEnclave.Constants.keyTag,
|
||||||
|
kSecValueRef: publicKey,
|
||||||
|
kSecAttrIsPermanent: true,
|
||||||
|
kSecReturnData: true,
|
||||||
|
kSecAttrLabel: name
|
||||||
|
] as CFDictionary
|
||||||
|
let status = SecItemAdd(attributes, nil)
|
||||||
|
if status != errSecSuccess {
|
||||||
|
throw SecureEnclave.KeychainError(statusCode: status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SecureEnclave {
|
||||||
|
|
||||||
|
public struct KeychainError: Error {
|
||||||
|
public let statusCode: OSStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct SigningError: Error {
|
||||||
|
public let error: SecurityError?
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SecureEnclave {
|
||||||
|
|
||||||
|
public typealias SecurityError = Unmanaged<CFError>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SecureEnclave {
|
||||||
|
|
||||||
|
enum Constants {
|
||||||
|
fileprivate static let keyTag = "com.maxgoedjen.secretive.secureenclave.key".data(using: .utf8)! as CFData
|
||||||
|
fileprivate static let keyType = kSecAttrKeyTypeECSECPrimeRandom
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
1
SecretKit/SmartCard/SmartCard.swift
Normal file
@ -0,0 +1 @@
|
|||||||
|
public enum SmartCard {}
|
@ -1,10 +1,8 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
import SecretKit
|
|
||||||
|
|
||||||
extension SmartCard {
|
extension SmartCard {
|
||||||
|
|
||||||
/// An implementation of Secret backed by a Smart Card.
|
|
||||||
public struct Secret: SecretKit.Secret {
|
public struct Secret: SecretKit.Secret {
|
||||||
|
|
||||||
public let id: Data
|
public let id: Data
|
@ -1,22 +1,21 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Security
|
import Security
|
||||||
import CryptoTokenKit
|
import CryptoTokenKit
|
||||||
import LocalAuthentication
|
|
||||||
import SecretKit
|
|
||||||
|
|
||||||
|
// TODO: Might need to split this up into "sub-stores?"
|
||||||
|
// ie, each token has its own Store.
|
||||||
extension SmartCard {
|
extension SmartCard {
|
||||||
|
|
||||||
/// An implementation of Store backed by a Smart Card.
|
|
||||||
public class Store: SecretStore {
|
public class Store: SecretStore {
|
||||||
|
|
||||||
|
// TODO: Read actual smart card name, eg "YubiKey 5c"
|
||||||
@Published public var isAvailable: Bool = false
|
@Published public var isAvailable: Bool = false
|
||||||
public let id = UUID()
|
public let id = UUID()
|
||||||
public private(set) var name = NSLocalizedString("Smart Card", comment: "Smart Card")
|
public fileprivate(set) var name = NSLocalizedString("Smart Card", comment: "Smart Card")
|
||||||
@Published public private(set) var secrets: [Secret] = []
|
@Published public fileprivate(set) var secrets: [Secret] = []
|
||||||
private let watcher = TKTokenWatcher()
|
fileprivate let watcher = TKTokenWatcher()
|
||||||
private var tokenID: String?
|
fileprivate var tokenID: String?
|
||||||
|
|
||||||
/// Initializes a Store.
|
|
||||||
public init() {
|
public init() {
|
||||||
tokenID = watcher.nonSecureEnclaveTokens.first
|
tokenID = watcher.nonSecureEnclaveTokens.first
|
||||||
watcher.setInsertionHandler { string in
|
watcher.setInsertionHandler { string in
|
||||||
@ -44,19 +43,15 @@ extension SmartCard {
|
|||||||
fatalError("Keys must be deleted on the smart card.")
|
fatalError("Keys must be deleted on the smart card.")
|
||||||
}
|
}
|
||||||
|
|
||||||
public func sign(data: Data, with secret: SecretType, for provenance: SigningRequestProvenance) throws -> SignedData {
|
public func sign(data: Data, with secret: SecretType) throws -> Data {
|
||||||
guard let tokenID = tokenID else { fatalError() }
|
guard let tokenID = tokenID else { fatalError() }
|
||||||
let context = LAContext()
|
|
||||||
context.localizedReason = "sign a request from \"\(provenance.origin.displayName)\" using secret \"\(secret.name)\""
|
|
||||||
context.localizedCancelTitle = "Deny"
|
|
||||||
let attributes = [
|
let attributes = [
|
||||||
kSecClass: kSecClassKey,
|
kSecClass: kSecClassKey,
|
||||||
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
|
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
|
||||||
kSecAttrApplicationLabel: secret.id as CFData,
|
kSecAttrApplicationLabel: secret.id as CFData,
|
||||||
kSecAttrTokenID: tokenID,
|
kSecAttrTokenID: tokenID,
|
||||||
kSecUseAuthenticationContext: context,
|
|
||||||
kSecReturnRef: true
|
kSecReturnRef: true
|
||||||
] as CFDictionary
|
] as CFDictionary
|
||||||
var untyped: CFTypeRef?
|
var untyped: CFTypeRef?
|
||||||
let status = SecItemCopyMatching(attributes, &untyped)
|
let status = SecItemCopyMatching(attributes, &untyped)
|
||||||
if status != errSecSuccess {
|
if status != errSecSuccess {
|
||||||
@ -79,10 +74,7 @@ extension SmartCard {
|
|||||||
guard let signature = SecKeyCreateSignature(key, signatureAlgorithm, data as CFData, &signError) else {
|
guard let signature = SecKeyCreateSignature(key, signatureAlgorithm, data as CFData, &signError) else {
|
||||||
throw SigningError(error: signError)
|
throw SigningError(error: signError)
|
||||||
}
|
}
|
||||||
return SignedData(data: signature as Data, requiredAuthentication: false)
|
return signature as Data
|
||||||
}
|
|
||||||
|
|
||||||
public func persistAuthentication(secret: SmartCard.Secret, forDuration: TimeInterval) throws {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -91,15 +83,12 @@ extension SmartCard {
|
|||||||
|
|
||||||
extension SmartCard.Store {
|
extension SmartCard.Store {
|
||||||
|
|
||||||
/// Resets the token ID and reloads secrets.
|
fileprivate func smartcardRemoved(for tokenID: String? = nil) {
|
||||||
/// - Parameter tokenID: The ID of the token that was removed.
|
|
||||||
private func smartcardRemoved(for tokenID: String? = nil) {
|
|
||||||
self.tokenID = nil
|
self.tokenID = nil
|
||||||
reloadSecrets()
|
reloadSecrets()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reloads all secrets from the store.
|
fileprivate func reloadSecrets() {
|
||||||
private func reloadSecrets() {
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.isAvailable = self.tokenID != nil
|
self.isAvailable = self.tokenID != nil
|
||||||
self.secrets.removeAll()
|
self.secrets.removeAll()
|
||||||
@ -107,25 +96,14 @@ extension SmartCard.Store {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads all secrets from the store.
|
fileprivate func loadSecrets() {
|
||||||
private func loadSecrets() {
|
|
||||||
guard let tokenID = tokenID else { return }
|
guard let tokenID = tokenID else { return }
|
||||||
|
// Hack to read name if there's only one smart card
|
||||||
let fallbackName = NSLocalizedString("Smart Card", comment: "Smart Card")
|
let slotNames = TKSmartCardSlotManager().slotNames
|
||||||
if #available(macOS 12.0, *) {
|
if watcher.nonSecureEnclaveTokens.count == 1 && slotNames.count == 1 {
|
||||||
if let driverName = watcher.tokenInfo(forTokenID: tokenID)?.driverName {
|
name = slotNames.first!
|
||||||
name = driverName
|
|
||||||
} else {
|
|
||||||
name = fallbackName
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Hack to read name if there's only one smart card
|
name = NSLocalizedString("Smart Card", comment: "Smart Card")
|
||||||
let slotNames = TKSmartCardSlotManager().slotNames
|
|
||||||
if watcher.nonSecureEnclaveTokens.count == 1 && slotNames.count == 1 {
|
|
||||||
name = slotNames.first!
|
|
||||||
} else {
|
|
||||||
name = fallbackName
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let attributes = [
|
let attributes = [
|
||||||
@ -135,7 +113,7 @@ extension SmartCard.Store {
|
|||||||
kSecReturnRef: true,
|
kSecReturnRef: true,
|
||||||
kSecMatchLimit: kSecMatchLimitAll,
|
kSecMatchLimit: kSecMatchLimitAll,
|
||||||
kSecReturnAttributes: true
|
kSecReturnAttributes: true
|
||||||
] as CFDictionary
|
] as CFDictionary
|
||||||
var untyped: CFTypeRef?
|
var untyped: CFTypeRef?
|
||||||
SecItemCopyMatching(attributes, &untyped)
|
SecItemCopyMatching(attributes, &untyped)
|
||||||
guard let typed = untyped as? [[CFString: Any]] else { return }
|
guard let typed = untyped as? [[CFString: Any]] else { return }
|
||||||
@ -157,7 +135,6 @@ extension SmartCard.Store {
|
|||||||
|
|
||||||
extension TKTokenWatcher {
|
extension TKTokenWatcher {
|
||||||
|
|
||||||
/// All available tokens, excluding the Secure Enclave.
|
|
||||||
fileprivate var nonSecureEnclaveTokens: [String] {
|
fileprivate var nonSecureEnclaveTokens: [String] {
|
||||||
tokenIDs.filter { !$0.contains("setoken") }
|
tokenIDs.filter { !$0.contains("setoken") }
|
||||||
}
|
}
|
||||||
@ -166,15 +143,11 @@ extension TKTokenWatcher {
|
|||||||
|
|
||||||
extension SmartCard {
|
extension SmartCard {
|
||||||
|
|
||||||
/// A wrapper around an error code reported by a Keychain API.
|
|
||||||
public struct KeychainError: Error {
|
public struct KeychainError: Error {
|
||||||
/// The status code involved.
|
|
||||||
public let statusCode: OSStatus
|
public let statusCode: OSStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A signing-related error.
|
|
||||||
public struct SigningError: Error {
|
public struct SigningError: Error {
|
||||||
/// The underlying error reported by the API, if one was returned.
|
|
||||||
public let error: SecurityError?
|
public let error: SecurityError?
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import XCTest
|
import XCTest
|
||||||
@testable import SecretKit
|
@testable import SecretKit
|
||||||
@testable import SecureEnclaveSecretKit
|
|
||||||
@testable import SmartCardSecretKit
|
|
||||||
|
|
||||||
class AnySecretTests: XCTestCase {
|
class AnySecretTests: XCTestCase {
|
||||||
|
|
22
SecretKitTests/Info.plist
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
@ -1,8 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import XCTest
|
import XCTest
|
||||||
@testable import SecretKit
|
@testable import SecretKit
|
||||||
@testable import SecureEnclaveSecretKit
|
|
||||||
@testable import SmartCardSecretKit
|
|
||||||
|
|
||||||
class OpenSSHReaderTests: XCTestCase {
|
class OpenSSHReaderTests: XCTestCase {
|
||||||
|
|
@ -1,19 +1,13 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import XCTest
|
import XCTest
|
||||||
@testable import SecretKit
|
@testable import SecretKit
|
||||||
@testable import SecureEnclaveSecretKit
|
|
||||||
@testable import SmartCardSecretKit
|
|
||||||
|
|
||||||
class OpenSSHWriterTests: XCTestCase {
|
class OpenSSHWriterTests: XCTestCase {
|
||||||
|
|
||||||
let writer = OpenSSHKeyWriter()
|
let writer = OpenSSHKeyWriter()
|
||||||
|
|
||||||
func testECDSA256MD5Fingerprint() {
|
func testECDSA256Fingerprint() {
|
||||||
XCTAssertEqual(writer.openSSHMD5Fingerprint(secret: Constants.ecdsa256Secret), "dc:60:4d:ff:c2:d9:18:8b:2f:24:40:b5:7f:43:47:e5")
|
XCTAssertEqual(writer.openSSHFingerprint(secret: Constants.ecdsa256Secret), "dc:60:4d:ff:c2:d9:18:8b:2f:24:40:b5:7f:43:47:e5")
|
||||||
}
|
|
||||||
|
|
||||||
func testECDSA256SHA256Fingerprint() {
|
|
||||||
XCTAssertEqual(writer.openSSHSHA256Fingerprint(secret: Constants.ecdsa256Secret), "SHA256:/VQFeGyM8qKA8rB6WGMuZZxZLJln2UgXLk3F0uTF650")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testECDSA256PublicKey() {
|
func testECDSA256PublicKey() {
|
||||||
@ -25,12 +19,8 @@ class OpenSSHWriterTests: XCTestCase {
|
|||||||
XCTAssertEqual(writer.data(secret: Constants.ecdsa256Secret), Data(base64Encoded: "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOVEjgAA5PHqRgwykjN5qM21uWCHFSY/Sqo5gkHAkn+e1MMQKHOLga7ucB9b3mif33MBid59GRK9GEPVlMiSQwo="))
|
XCTAssertEqual(writer.data(secret: Constants.ecdsa256Secret), Data(base64Encoded: "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOVEjgAA5PHqRgwykjN5qM21uWCHFSY/Sqo5gkHAkn+e1MMQKHOLga7ucB9b3mif33MBid59GRK9GEPVlMiSQwo="))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testECDSA384MD5Fingerprint() {
|
func testECDSA384Fingerprint() {
|
||||||
XCTAssertEqual(writer.openSSHMD5Fingerprint(secret: Constants.ecdsa384Secret), "66:e0:66:d7:41:ed:19:8e:e2:20:df:ce:ac:7e:2b:6e")
|
XCTAssertEqual(writer.openSSHFingerprint(secret: Constants.ecdsa384Secret), "66:e0:66:d7:41:ed:19:8e:e2:20:df:ce:ac:7e:2b:6e")
|
||||||
}
|
|
||||||
|
|
||||||
func testECDSA384SHA256Fingerprint() {
|
|
||||||
XCTAssertEqual(writer.openSSHSHA256Fingerprint(secret: Constants.ecdsa384Secret), "SHA256:GJUEymQNL9ymaMRRJCMGY4rWIJHu/Lm8Yhao/PAiz1I")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testECDSA384PublicKey() {
|
func testECDSA384PublicKey() {
|
1977
Secretive.xcodeproj/project.pbxproj
Normal file
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1320"
|
LastUpgradeVersion = "1140"
|
||||||
version = "1.7">
|
version = "1.7">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
@ -0,0 +1,85 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1140"
|
||||||
|
version = "1.7">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "5099A06B240242BA0062B6F2"
|
||||||
|
BuildableName = "SecretAgentKit.framework"
|
||||||
|
BlueprintName = "SecretAgentKit"
|
||||||
|
ReferencedContainer = "container:Secretive.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Test"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<TestPlans>
|
||||||
|
<TestPlanReference
|
||||||
|
reference = "container:Config/Secretive.xctestplan"
|
||||||
|
default = "YES">
|
||||||
|
</TestPlanReference>
|
||||||
|
</TestPlans>
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO"
|
||||||
|
parallelizable = "YES"
|
||||||
|
testExecutionOrdering = "random">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "5099A073240242BA0062B6F2"
|
||||||
|
BuildableName = "SecretAgentKitTests.xctest"
|
||||||
|
BlueprintName = "SecretAgentKitTests"
|
||||||
|
ReferencedContainer = "container:Secretive.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "5099A06B240242BA0062B6F2"
|
||||||
|
BuildableName = "SecretAgentKit.framework"
|
||||||
|
BlueprintName = "SecretAgentKit"
|
||||||
|
ReferencedContainer = "container:Secretive.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1140"
|
||||||
|
version = "1.7">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "50617DA723FCE4AB0099B055"
|
||||||
|
BuildableName = "SecretKit.framework"
|
||||||
|
BlueprintName = "SecretKit"
|
||||||
|
ReferencedContainer = "container:Secretive.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Test"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<TestPlans>
|
||||||
|
<TestPlanReference
|
||||||
|
reference = "container:Config/Secretive.xctestplan"
|
||||||
|
default = "YES">
|
||||||
|
</TestPlanReference>
|
||||||
|
</TestPlans>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "50617DA723FCE4AB0099B055"
|
||||||
|
BuildableName = "SecretKit.framework"
|
||||||
|
BlueprintName = "SecretKit"
|
||||||
|
ReferencedContainer = "container:Secretive.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1320"
|
LastUpgradeVersion = "1140"
|
||||||
version = "1.7">
|
version = "1.7">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
108
Secretive/AppDelegate.swift
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import Cocoa
|
||||||
|
import SwiftUI
|
||||||
|
import SecretKit
|
||||||
|
import Brief
|
||||||
|
|
||||||
|
@NSApplicationMain
|
||||||
|
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
|
||||||
|
var window: NSWindow!
|
||||||
|
@IBOutlet var newMenuItem: NSMenuItem!
|
||||||
|
@IBOutlet var toolbar: NSToolbar!
|
||||||
|
let storeList: SecretStoreList = {
|
||||||
|
let list = SecretStoreList()
|
||||||
|
list.add(store: SecureEnclave.Store())
|
||||||
|
list.add(store: SmartCard.Store())
|
||||||
|
return list
|
||||||
|
}()
|
||||||
|
let updater = Updater()
|
||||||
|
let agentStatusChecker = AgentStatusChecker()
|
||||||
|
let justUpdatedChecker = JustUpdatedChecker()
|
||||||
|
|
||||||
|
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||||
|
let contentView = ContentView(storeList: storeList, updater: updater, agentStatusChecker: agentStatusChecker, runSetupBlock: { self.runSetup(sender: nil) })
|
||||||
|
// Create the window and set the content view.
|
||||||
|
window = NSWindow(
|
||||||
|
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
|
||||||
|
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
|
||||||
|
backing: .buffered, defer: false)
|
||||||
|
window.center()
|
||||||
|
window.setFrameAutosaveName("Main Window")
|
||||||
|
window.contentView = NSHostingView(rootView: contentView)
|
||||||
|
window.makeKeyAndOrderFront(nil)
|
||||||
|
window.titleVisibility = .hidden
|
||||||
|
window.toolbar = toolbar
|
||||||
|
window.isReleasedWhenClosed = false
|
||||||
|
if storeList.modifiableStore?.isAvailable ?? false {
|
||||||
|
let plus = NSTitlebarAccessoryViewController()
|
||||||
|
plus.view = NSButton(image: NSImage(named: NSImage.addTemplateName)!, target: self, action: #selector(add(sender:)))
|
||||||
|
plus.layoutAttribute = .right
|
||||||
|
window.addTitlebarAccessoryViewController(plus)
|
||||||
|
newMenuItem.isEnabled = true
|
||||||
|
}
|
||||||
|
runSetupIfNeeded()
|
||||||
|
relaunchAgentIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
func applicationDidBecomeActive(_ notification: Notification) {
|
||||||
|
agentStatusChecker.check()
|
||||||
|
}
|
||||||
|
|
||||||
|
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
|
||||||
|
guard !flag else { return false }
|
||||||
|
window.makeKeyAndOrderFront(self)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func add(sender: AnyObject?) {
|
||||||
|
var addWindow: NSWindow!
|
||||||
|
let addView = CreateSecretView(store: storeList.modifiableStore!) {
|
||||||
|
self.window.endSheet(addWindow)
|
||||||
|
}
|
||||||
|
addWindow = NSWindow(
|
||||||
|
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
|
||||||
|
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
|
||||||
|
backing: .buffered, defer: false)
|
||||||
|
addWindow.contentView = NSHostingView(rootView: addView)
|
||||||
|
window.beginSheet(addWindow, completionHandler: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func runSetup(sender: AnyObject?) {
|
||||||
|
let setupWindow = NSWindow(
|
||||||
|
contentRect: NSRect(x: 0, y: 0, width: 0, height: 0),
|
||||||
|
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
|
||||||
|
backing: .buffered, defer: false)
|
||||||
|
let setupView = SetupView() { success in
|
||||||
|
self.window.endSheet(setupWindow)
|
||||||
|
self.agentStatusChecker.check()
|
||||||
|
}
|
||||||
|
setupWindow.contentView = NSHostingView(rootView: setupView)
|
||||||
|
window.beginSheet(setupWindow, completionHandler: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AppDelegate {
|
||||||
|
|
||||||
|
func runSetupIfNeeded() {
|
||||||
|
if !UserDefaults.standard.bool(forKey: Constants.defaultsHasRunSetup) {
|
||||||
|
UserDefaults.standard.set(true, forKey: Constants.defaultsHasRunSetup)
|
||||||
|
runSetup(sender: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func relaunchAgentIfNeeded() {
|
||||||
|
if agentStatusChecker.running && justUpdatedChecker.justUpdated {
|
||||||
|
LaunchAgentController().relaunch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AppDelegate {
|
||||||
|
|
||||||
|
enum Constants {
|
||||||
|
static let defaultsHasRunSetup = "defaultsHasRunSetup"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -31,13 +31,13 @@
|
|||||||
"size" : "128x128"
|
"size" : "128x128"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "Mac Icon.png",
|
"filename" : "Icon 2@1x.png",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "1x",
|
"scale" : "1x",
|
||||||
"size" : "256x256"
|
"size" : "256x256"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "Mac Icon@0.25x.png",
|
"filename" : "Icon 2@2x.png",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "256x256"
|
"size" : "256x256"
|
BIN
Secretive/Assets.xcassets/AppIcon.appiconset/Icon 2@1x.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
Secretive/Assets.xcassets/AppIcon.appiconset/Icon 2@2x.png
Normal file
After Width: | Height: | Size: 66 KiB |
160
Secretive/Base.lproj/Main.storyboard
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="16085" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||||
|
<dependencies>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="16085"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--Application-->
|
||||||
|
<scene sceneID="JPo-4y-FX3">
|
||||||
|
<objects>
|
||||||
|
<application id="hnw-xV-0zn" sceneMemberID="viewController">
|
||||||
|
<menu key="mainMenu" title="Main Menu" systemMenu="main" autoenablesItems="NO" id="AYu-sK-qS6">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Secretive" id="1Xt-HY-uBw">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Secretive" systemMenu="apple" id="uQy-DD-JDr">
|
||||||
|
<items>
|
||||||
|
<menuItem title="About Secretive" id="5kV-Vb-QxS">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="orderFrontStandardAboutPanel:" target="Ady-hI-5gd" id="Exp-CZ-Vem"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
|
||||||
|
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
|
||||||
|
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
|
||||||
|
<menuItem title="Services" id="NMo-om-nkz">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
|
||||||
|
<menuItem title="Hide Secretive" keyEquivalent="h" id="Olw-nP-bQN">
|
||||||
|
<connections>
|
||||||
|
<action selector="hide:" target="Ady-hI-5gd" id="PnN-Uc-m68"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="hideOtherApplications:" target="Ady-hI-5gd" id="VT4-aY-XCT"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Show All" id="Kd2-mp-pUS">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="unhideAllApplications:" target="Ady-hI-5gd" id="Dhg-Le-xox"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
|
||||||
|
<menuItem title="Quit Secretive" keyEquivalent="q" id="4sb-4s-VLi">
|
||||||
|
<connections>
|
||||||
|
<action selector="terminate:" target="Ady-hI-5gd" id="Te7-pn-YzF"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="File" id="dMs-cI-mzQ">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="File" autoenablesItems="NO" id="bib-Uj-vzu">
|
||||||
|
<items>
|
||||||
|
<menuItem title="New" enabled="NO" keyEquivalent="n" id="Was-JA-tGl">
|
||||||
|
<connections>
|
||||||
|
<action selector="addWithSender:" target="Voe-Tx-rLC" id="U1t-YZ-Hn5"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="m54-Is-iLE"/>
|
||||||
|
<menuItem title="Close" keyEquivalent="w" id="DVo-aG-piG">
|
||||||
|
<connections>
|
||||||
|
<action selector="performClose:" target="Ady-hI-5gd" id="HmO-Ls-i7Q"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Window" id="aUF-d1-5bR">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
|
||||||
|
<connections>
|
||||||
|
<action selector="performMiniaturize:" target="Ady-hI-5gd" id="VwT-WD-YPe"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Zoom" id="R4o-n2-Eq4">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="performZoom:" target="Ady-hI-5gd" id="DIl-cC-cCs"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
|
||||||
|
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="arrangeInFront:" target="Ady-hI-5gd" id="DRN-fu-gQh"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Help" id="wpr-3q-Mcd">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Setup Helper App" id="04y-R6-7bF">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="runSetupWithSender:" target="Voe-Tx-rLC" id="Fty-2m-eng"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="Ddf-5M-Bmf"/>
|
||||||
|
<menuItem title="Secretive Help" keyEquivalent="?" id="FKE-Sm-Kum">
|
||||||
|
<connections>
|
||||||
|
<action selector="showHelp:" target="Ady-hI-5gd" id="y7X-2Q-9no"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
<connections>
|
||||||
|
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
|
||||||
|
</connections>
|
||||||
|
</application>
|
||||||
|
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Secretive" customModuleProvider="target">
|
||||||
|
<connections>
|
||||||
|
<outlet property="newMenuItem" destination="Was-JA-tGl" id="C8s-uk-gMA"/>
|
||||||
|
<outlet property="toolbar" destination="bvo-mt-QR4" id="XSF-g2-znt"/>
|
||||||
|
</connections>
|
||||||
|
</customObject>
|
||||||
|
<toolbar implicitIdentifier="09D11707-F4A3-4FD5-970E-AC5832E91C2B" autosavesConfiguration="NO" displayMode="iconAndLabel" sizeMode="regular" id="bvo-mt-QR4">
|
||||||
|
<allowedToolbarItems>
|
||||||
|
<toolbarItem implicitItemIdentifier="NSToolbarFlexibleSpaceItem" id="9Xm-OQ-a7h"/>
|
||||||
|
<toolbarItem implicitItemIdentifier="728E7E6E-F692-41A1-9439-C6EF9BE96CBA" label="Secretive" paletteLabel="" sizingBehavior="auto" id="xbD-W8-Ypr">
|
||||||
|
<nil key="toolTip"/>
|
||||||
|
<textField key="view" horizontalHuggingPriority="251" verticalHuggingPriority="750" id="Mg0-Hm-7bW">
|
||||||
|
<rect key="frame" x="0.0" y="14" width="65" height="16"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
|
<textFieldCell key="cell" lineBreakMode="clipping" title="Secretive" id="EXw-BM-zF7">
|
||||||
|
<font key="font" usesAppearanceFont="YES"/>
|
||||||
|
<color key="textColor" name="windowFrameTextColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||||
|
</textFieldCell>
|
||||||
|
</textField>
|
||||||
|
</toolbarItem>
|
||||||
|
</allowedToolbarItems>
|
||||||
|
<defaultToolbarItems>
|
||||||
|
<toolbarItem reference="9Xm-OQ-a7h"/>
|
||||||
|
<toolbarItem reference="xbD-W8-Ypr"/>
|
||||||
|
<toolbarItem reference="9Xm-OQ-a7h"/>
|
||||||
|
</defaultToolbarItems>
|
||||||
|
</toolbar>
|
||||||
|
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||||
|
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="75" y="0.0"/>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
</document>
|
33
Secretive/Controllers/AgentStatusChecker.swift
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
import AppKit
|
||||||
|
|
||||||
|
protocol AgentStatusCheckerProtocol: ObservableObject {
|
||||||
|
var running: Bool { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
class AgentStatusChecker: ObservableObject, AgentStatusCheckerProtocol {
|
||||||
|
|
||||||
|
@Published var running: Bool = false
|
||||||
|
|
||||||
|
init() {
|
||||||
|
check()
|
||||||
|
}
|
||||||
|
|
||||||
|
func check() {
|
||||||
|
running = secretAgentProcess != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var secretAgentProcess: NSRunningApplication? {
|
||||||
|
NSRunningApplication.runningApplications(withBundleIdentifier: Constants.secretAgentAppID).first
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AgentStatusChecker {
|
||||||
|
|
||||||
|
enum Constants {
|
||||||
|
static let secretAgentAppID = "com.maxgoedjen.Secretive.SecretAgent"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -9,7 +9,6 @@ protocol JustUpdatedCheckerProtocol: ObservableObject {
|
|||||||
class JustUpdatedChecker: ObservableObject, JustUpdatedCheckerProtocol {
|
class JustUpdatedChecker: ObservableObject, JustUpdatedCheckerProtocol {
|
||||||
|
|
||||||
@Published var justUpdated: Bool = false
|
@Published var justUpdated: Bool = false
|
||||||
var alreadyRelaunchedForDebug = false
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
check()
|
check()
|
||||||
@ -19,11 +18,8 @@ class JustUpdatedChecker: ObservableObject, JustUpdatedCheckerProtocol {
|
|||||||
let lastBuild = UserDefaults.standard.object(forKey: Constants.previousVersionUserDefaultsKey) as? String ?? "None"
|
let lastBuild = UserDefaults.standard.object(forKey: Constants.previousVersionUserDefaultsKey) as? String ?? "None"
|
||||||
let currentBuild = Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String
|
let currentBuild = Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String
|
||||||
UserDefaults.standard.set(currentBuild, forKey: Constants.previousVersionUserDefaultsKey)
|
UserDefaults.standard.set(currentBuild, forKey: Constants.previousVersionUserDefaultsKey)
|
||||||
if currentBuild != Constants.debugVersionKey {
|
if lastBuild != currentBuild {
|
||||||
justUpdated = lastBuild != currentBuild
|
justUpdated = true
|
||||||
} else {
|
|
||||||
justUpdated = !alreadyRelaunchedForDebug
|
|
||||||
alreadyRelaunchedForDebug = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,7 +31,6 @@ extension JustUpdatedChecker {
|
|||||||
|
|
||||||
enum Constants {
|
enum Constants {
|
||||||
static let previousVersionUserDefaultsKey = "com.maxgoedjen.Secretive.lastBuild"
|
static let previousVersionUserDefaultsKey = "com.maxgoedjen.Secretive.lastBuild"
|
||||||
static let debugVersionKey = "GITHUB_CI_VERSION"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
19
Secretive/Controllers/LaunchAgentController.swift
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import Foundation
|
||||||
|
import ServiceManagement
|
||||||
|
|
||||||
|
struct LaunchAgentController {
|
||||||
|
|
||||||
|
func install() -> Bool {
|
||||||
|
setEnabled(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func relaunch() {
|
||||||
|
_ = setEnabled(false)
|
||||||
|
_ = setEnabled(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setEnabled(_ enabled: Bool) -> Bool {
|
||||||
|
SMLoginItemSetEnabled("com.maxgoedjen.Secretive.SecretAgent" as CFString, enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
{\rtf1\ansi\ansicpg1252\cocoartf2580
|
{\rtf1\ansi\ansicpg1252\cocoartf2511
|
||||||
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
|
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
|
||||||
{\colortbl;\red255\green255\blue255;}
|
{\colortbl;\red255\green255\blue255;}
|
||||||
{\*\expandedcolortbl;;}
|
{\*\expandedcolortbl;;}
|
||||||
@ -12,19 +12,6 @@
|
|||||||
{\field{\*\fldinst{HYPERLINK "GITHUB_BUILD_URL"}}{\fldrslt Build Log}}\
|
{\field{\*\fldinst{HYPERLINK "GITHUB_BUILD_URL"}}{\fldrslt Build Log}}\
|
||||||
\
|
\
|
||||||
Special Thanks To:\
|
Special Thanks To:\
|
||||||
\
|
|
||||||
{\field{\*\fldinst{HYPERLINK "https://github.com/maxgoedjen/secretive/graphs/contributors"}}{\fldrslt Contributors}}:\
|
|
||||||
{\field{\*\fldinst{HYPERLINK "https://github.com/0xflotus"}}{\fldrslt 0xflotus}}\
|
|
||||||
{\field{\*\fldinst{HYPERLINK "https://github.com/aaron-trout"}}{\fldrslt Aaron Trout}}\
|
|
||||||
\pard\pardeftab720\partightenfactor0
|
|
||||||
{\field{\*\fldinst{HYPERLINK "https://github.com/EppO"}}{\fldrslt \cf0 Florent Monbillard}}\
|
|
||||||
{\field{\*\fldinst{HYPERLINK "https://github.com/vladimyr"}}{\fldrslt Dario Vladovi\uc0\u263 }}\
|
|
||||||
{\field{\*\fldinst{HYPERLINK "https://github.com/lavalleeale"}}{\fldrslt Alex Lavallee}}\
|
|
||||||
{\field{\*\fldinst{HYPERLINK "https://github.com/joshheyse"}}{\fldrslt Josh}}\
|
|
||||||
{\field{\*\fldinst{HYPERLINK "https://github.com/diesal11"}}{\fldrslt Dylan Lundy}}\
|
|
||||||
\
|
|
||||||
\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0
|
|
||||||
\cf0 Testers:\
|
|
||||||
{\field{\*\fldinst{HYPERLINK "https://github.com/bdash"}}{\fldrslt Mark Rowe}}\
|
{\field{\*\fldinst{HYPERLINK "https://github.com/bdash"}}{\fldrslt Mark Rowe}}\
|
||||||
{\field{\*\fldinst{HYPERLINK "https://github.com/danielctull"}}{\fldrslt Daniel Tull}}\
|
{\field{\*\fldinst{HYPERLINK "https://github.com/danielctull"}}{\fldrslt Daniel Tull}}\
|
||||||
{\field{\*\fldinst{HYPERLINK "https://github.com/davedelong"}}{\fldrslt Dave DeLong}}\
|
{\field{\*\fldinst{HYPERLINK "https://github.com/davedelong"}}{\fldrslt Dave DeLong}}\
|
@ -24,6 +24,8 @@
|
|||||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
<string>$(PRODUCT_NAME) is MIT Licensed.</string>
|
<string>$(PRODUCT_NAME) is MIT Licensed.</string>
|
||||||
|
<key>NSMainStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
<key>NSPrincipalClass</key>
|
<key>NSPrincipalClass</key>
|
||||||
<string>NSApplication</string>
|
<string>NSApplication</string>
|
||||||
<key>NSSupportsAutomaticTermination</key>
|
<key>NSSupportsAutomaticTermination</key>
|
@ -4,7 +4,6 @@ import Combine
|
|||||||
class PreviewAgentStatusChecker: AgentStatusCheckerProtocol {
|
class PreviewAgentStatusChecker: AgentStatusCheckerProtocol {
|
||||||
|
|
||||||
let running: Bool
|
let running: Bool
|
||||||
let developmentBuild = false
|
|
||||||
|
|
||||||
init(running: Bool = true) {
|
init(running: Bool = true) {
|
||||||
self.running = running
|
self.running = running
|
@ -35,11 +35,8 @@ extension Preview {
|
|||||||
self.secrets.append(contentsOf: new)
|
self.secrets.append(contentsOf: new)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sign(data: Data, with secret: Preview.Secret, for provenance: SigningRequestProvenance) throws -> SignedData {
|
func sign(data: Data, with secret: Preview.Secret) throws -> Data {
|
||||||
return SignedData(data: data, requiredAuthentication: false)
|
return data
|
||||||
}
|
|
||||||
|
|
||||||
func persistAuthentication(secret: Preview.Secret, forDuration duration: TimeInterval) throws {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -53,14 +50,8 @@ extension Preview {
|
|||||||
|
|
||||||
func delete(secret: Preview.Secret) throws {
|
func delete(secret: Preview.Secret) throws {
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(secret: Preview.Secret, name: String) throws {
|
|
||||||
}
|
|
||||||
|
|
||||||
func reload() throws {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Preview {
|
extension Preview {
|
@ -5,19 +5,20 @@ import Brief
|
|||||||
class PreviewUpdater: UpdaterProtocol {
|
class PreviewUpdater: UpdaterProtocol {
|
||||||
|
|
||||||
let update: Release?
|
let update: Release?
|
||||||
let testBuild = false
|
|
||||||
|
|
||||||
init(update: Update = .none) {
|
init(update: Update = .none) {
|
||||||
switch update {
|
switch update {
|
||||||
case .none:
|
case .none:
|
||||||
self.update = nil
|
self.update = nil
|
||||||
case .advisory:
|
case .advisory:
|
||||||
self.update = Release(name: "10.10.10", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Some regular update")
|
self.update = Release(name: "10.10.10", html_url: URL(string: "https://example.com")!, body: "Some regular update")
|
||||||
case .critical:
|
case .critical:
|
||||||
self.update = Release(name: "10.10.10", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update")
|
self.update = Release(name: "10.10.10", html_url: URL(string: "https://example.com")!, body: "Critical Security Update")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ignore(release: Release) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PreviewUpdater {
|
extension PreviewUpdater {
|
@ -4,16 +4,10 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.security.app-sandbox</key>
|
<key>com.apple.security.app-sandbox</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.smartcard</key>
|
||||||
<array>
|
|
||||||
<string>$(TeamIdentifierPrefix)com.maxgoedjen.secretive</string>
|
|
||||||
</array>
|
|
||||||
<key>com.apple.security.files.user-selected.read-write</key>
|
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.network.client</key>
|
<key>com.apple.security.network.client</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.smartcard</key>
|
|
||||||
<true/>
|
|
||||||
<key>keychain-access-groups</key>
|
<key>keychain-access-groups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>$(AppIdentifierPrefix)com.maxgoedjen.Secretive</string>
|
<string>$(AppIdentifierPrefix)com.maxgoedjen.Secretive</string>
|
153
Secretive/Views/ContentView.swift
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SecretKit
|
||||||
|
import Brief
|
||||||
|
|
||||||
|
struct ContentView<UpdaterType: UpdaterProtocol, AgentStatusCheckerType: AgentStatusCheckerProtocol>: View {
|
||||||
|
|
||||||
|
@ObservedObject var storeList: SecretStoreList
|
||||||
|
@ObservedObject var updater: UpdaterType
|
||||||
|
@ObservedObject var agentStatusChecker: AgentStatusCheckerType
|
||||||
|
var runSetupBlock: (() -> Void)?
|
||||||
|
|
||||||
|
@State fileprivate var active: AnySecret.ID?
|
||||||
|
@State fileprivate var showingDeletion = false
|
||||||
|
@State fileprivate var deletingSecret: AnySecret?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
if updater.update != nil {
|
||||||
|
updateNotice()
|
||||||
|
}
|
||||||
|
if !agentStatusChecker.running {
|
||||||
|
agentNotice()
|
||||||
|
}
|
||||||
|
if storeList.anyAvailable {
|
||||||
|
NavigationView {
|
||||||
|
List(selection: $active) {
|
||||||
|
ForEach(storeList.stores) { store in
|
||||||
|
if store.isAvailable {
|
||||||
|
Section(header: Text(store.name)) {
|
||||||
|
if store.secrets.isEmpty {
|
||||||
|
if store is AnySecretStoreModifiable {
|
||||||
|
NavigationLink(destination: EmptyStoreModifiableView(), tag: Constants.emptyStoreModifiableTag, selection: self.$active) {
|
||||||
|
Text("No Secrets")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
NavigationLink(destination: EmptyStoreView(), tag: Constants.emptyStoreTag, selection: self.$active) {
|
||||||
|
Text("No Secrets")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ForEach(store.secrets) { secret in
|
||||||
|
NavigationLink(destination: SecretDetailView(secret: secret), tag: secret.id, selection: self.$active) {
|
||||||
|
Text(secret.name)
|
||||||
|
}.contextMenu {
|
||||||
|
if store is AnySecretStoreModifiable {
|
||||||
|
Button(action: { self.delete(secret: secret) }) {
|
||||||
|
Text("Delete")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.onAppear {
|
||||||
|
self.active = self.nextDefaultSecret
|
||||||
|
}
|
||||||
|
.listStyle(SidebarListStyle())
|
||||||
|
.frame(minWidth: 100, idealWidth: 240)
|
||||||
|
}
|
||||||
|
.navigationViewStyle(DoubleColumnNavigationViewStyle())
|
||||||
|
.sheet(isPresented: $showingDeletion) {
|
||||||
|
if self.storeList.modifiableStore != nil {
|
||||||
|
DeleteSecretView(secret: self.deletingSecret!, store: self.storeList.modifiableStore!) { deleted in
|
||||||
|
self.showingDeletion = false
|
||||||
|
if deleted {
|
||||||
|
self.active = self.nextDefaultSecret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
NoStoresView()
|
||||||
|
}
|
||||||
|
}.frame(minWidth: 640, minHeight: 320)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateNotice() -> some View {
|
||||||
|
guard let update = updater.update else { return AnyView(Spacer()) }
|
||||||
|
let severity: NoticeView.Severity
|
||||||
|
let text: String
|
||||||
|
if update.critical {
|
||||||
|
severity = .critical
|
||||||
|
text = "Critical Security Update Required"
|
||||||
|
} else {
|
||||||
|
severity = .advisory
|
||||||
|
text = "Update Available"
|
||||||
|
}
|
||||||
|
let action = {
|
||||||
|
_ = NSWorkspace.shared.open(update.html_url)
|
||||||
|
}
|
||||||
|
let ignoreAction = {
|
||||||
|
updater.ignore(release: update)
|
||||||
|
}
|
||||||
|
return AnyView(NoticeView(text: text, severity: severity, actionTitle: "Update", action: action, secondaryActionTitle: "Ignore", secondaryAction: ignoreAction))
|
||||||
|
}
|
||||||
|
|
||||||
|
func agentNotice() -> some View {
|
||||||
|
NoticeView(text: "Secret Agent isn't running. Run setup again to fix.", severity: .advisory, actionTitle: "Run Setup") {
|
||||||
|
self.runSetupBlock?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func delete<SecretType: Secret>(secret: SecretType) {
|
||||||
|
deletingSecret = AnySecret(secret)
|
||||||
|
self.showingDeletion = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextDefaultSecret: AnyHashable? {
|
||||||
|
let fallback: AnyHashable
|
||||||
|
if self.storeList.modifiableStore?.isAvailable ?? false {
|
||||||
|
fallback = Constants.emptyStoreModifiableTag
|
||||||
|
} else {
|
||||||
|
fallback = Constants.emptyStoreTag
|
||||||
|
}
|
||||||
|
return self.storeList.stores.compactMap { $0.secrets.first }.first?.id ?? fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate enum Constants {
|
||||||
|
static let emptyStoreModifiableTag: AnyHashable = "emptyStoreModifiableTag"
|
||||||
|
static let emptyStoreTag: AnyHashable = "emptyStoreModifiableTag"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
|
||||||
|
struct ContentView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
Group {
|
||||||
|
ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)],
|
||||||
|
modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]),
|
||||||
|
updater: PreviewUpdater(),
|
||||||
|
agentStatusChecker: PreviewAgentStatusChecker())
|
||||||
|
ContentView(storeList: Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()]), updater: PreviewUpdater(),
|
||||||
|
agentStatusChecker: PreviewAgentStatusChecker())
|
||||||
|
ContentView(storeList: Preview.storeList(stores: [Preview.Store()]), updater: PreviewUpdater(),
|
||||||
|
agentStatusChecker: PreviewAgentStatusChecker())
|
||||||
|
ContentView(storeList: Preview.storeList(modifiableStores: [Preview.StoreModifiable()]), updater: PreviewUpdater(),
|
||||||
|
agentStatusChecker: PreviewAgentStatusChecker())
|
||||||
|
ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]), updater: PreviewUpdater(update: .advisory),
|
||||||
|
agentStatusChecker: PreviewAgentStatusChecker())
|
||||||
|
ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]), updater: PreviewUpdater(update: .critical),
|
||||||
|
agentStatusChecker: PreviewAgentStatusChecker())
|
||||||
|
ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]), updater: PreviewUpdater(update: .critical),
|
||||||
|
agentStatusChecker: PreviewAgentStatusChecker(running: false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
54
Secretive/Views/CreateSecretView.swift
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SecretKit
|
||||||
|
|
||||||
|
struct CreateSecretView: View {
|
||||||
|
|
||||||
|
@ObservedObject var store: AnySecretStoreModifiable
|
||||||
|
|
||||||
|
@State var name = ""
|
||||||
|
@State var requiresAuthentication = true
|
||||||
|
|
||||||
|
var dismissalBlock: () -> ()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
HStack {
|
||||||
|
Image(nsImage: NSApp.applicationIconImage)
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 64, height: 64)
|
||||||
|
.padding()
|
||||||
|
VStack {
|
||||||
|
HStack {
|
||||||
|
Text("Create a New Secret").bold()
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
Text("Name:")
|
||||||
|
TextField("Shhhhh", text: $name)
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
Toggle(isOn: $requiresAuthentication) {
|
||||||
|
Text("Requires Authentication (Biometrics or Password)")
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onExitCommand(perform: dismissalBlock)
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Button(action: dismissalBlock) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
Button(action: save) {
|
||||||
|
Text("Create")
|
||||||
|
}.disabled(name.isEmpty)
|
||||||
|
}
|
||||||
|
}.padding()
|
||||||
|
}
|
||||||
|
|
||||||
|
func save() {
|
||||||
|
try! store.create(name: name, requiresAuthentication: requiresAuthentication)
|
||||||
|
dismissalBlock()
|
||||||
|
}
|
||||||
|
}
|
@ -2,17 +2,24 @@ import SwiftUI
|
|||||||
import SecretKit
|
import SecretKit
|
||||||
|
|
||||||
struct DeleteSecretView<StoreType: SecretStoreModifiable>: View {
|
struct DeleteSecretView<StoreType: SecretStoreModifiable>: View {
|
||||||
|
|
||||||
@ObservedObject var store: StoreType
|
|
||||||
let secret: StoreType.SecretType
|
let secret: StoreType.SecretType
|
||||||
var dismissalBlock: (Bool) -> ()
|
@ObservedObject var store: StoreType
|
||||||
|
|
||||||
@State private var confirm = ""
|
@State var confirm = ""
|
||||||
|
|
||||||
|
fileprivate var dismissalBlock: (Bool) -> ()
|
||||||
|
|
||||||
|
init(secret: StoreType.SecretType, store: StoreType, dismissalBlock: @escaping (Bool) -> ()) {
|
||||||
|
self.secret = secret
|
||||||
|
self.store = store
|
||||||
|
self.dismissalBlock = dismissalBlock
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
HStack {
|
HStack {
|
||||||
Image(nsImage: NSApplication.shared.applicationIconImage)
|
Image(nsImage: NSApp.applicationIconImage)
|
||||||
.resizable()
|
.resizable()
|
||||||
.frame(width: 64, height: 64)
|
.frame(width: 64, height: 64)
|
||||||
.padding()
|
.padding()
|
||||||
@ -30,28 +37,24 @@ struct DeleteSecretView<StoreType: SecretStoreModifiable>: View {
|
|||||||
TextField(secret.name, text: $confirm)
|
TextField(secret.name, text: $confirm)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onExitCommand {
|
||||||
|
self.dismissalBlock(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
Button("Delete", action: delete)
|
Button(action: delete) {
|
||||||
.disabled(confirm != secret.name)
|
Text("Delete")
|
||||||
.keyboardShortcut(.delete)
|
}.disabled(confirm != secret.name)
|
||||||
Button("Don't Delete") {
|
Button(action: { self.dismissalBlock(false) }) {
|
||||||
dismissalBlock(false)
|
Text("Don't Delete")
|
||||||
}
|
}
|
||||||
.keyboardShortcut(.cancelAction)
|
|
||||||
}
|
}
|
||||||
}
|
}.padding()
|
||||||
.padding()
|
|
||||||
.frame(minWidth: 400)
|
|
||||||
.onExitCommand {
|
|
||||||
dismissalBlock(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func delete() {
|
func delete() {
|
||||||
try! store.delete(secret: secret)
|
try! store.delete(secret: secret)
|
||||||
dismissalBlock(true)
|
self.dismissalBlock(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,34 +1,6 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SecretKit
|
|
||||||
|
|
||||||
struct EmptyStoreView: View {
|
struct EmptyStoreView: View {
|
||||||
|
|
||||||
@ObservedObject var store: AnySecretStore
|
|
||||||
@Binding var activeSecret: AnySecret.ID?
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
if store is AnySecretStoreModifiable {
|
|
||||||
NavigationLink(destination: EmptyStoreModifiableView(), tag: Constants.emptyStoreModifiableTag, selection: $activeSecret) {
|
|
||||||
Text("No Secrets")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
NavigationLink(destination: EmptyStoreImmutableView(), tag: Constants.emptyStoreTag, selection: $activeSecret) {
|
|
||||||
Text("No Secrets")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension EmptyStoreView {
|
|
||||||
|
|
||||||
enum Constants {
|
|
||||||
static let emptyStoreModifiableTag: AnyHashable = "emptyStoreModifiableTag"
|
|
||||||
static let emptyStoreTag: AnyHashable = "emptyStoreModifiableTag"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct EmptyStoreImmutableView: View {
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
@ -53,14 +25,14 @@ struct EmptyStoreModifiableView: View {
|
|||||||
CGPoint(x: g.size.width / 2, y: g.size.height * (1/2)), control2:
|
CGPoint(x: g.size.width / 2, y: g.size.height * (1/2)), control2:
|
||||||
CGPoint(x: g.size.width * (3/4), y: g.size.height * (1/2)))
|
CGPoint(x: g.size.width * (3/4), y: g.size.height * (1/2)))
|
||||||
path.addCurve(to:
|
path.addCurve(to:
|
||||||
CGPoint(x: g.size.width - 13, y: 0), control1:
|
CGPoint(x: g.size.width, y: 0), control1:
|
||||||
CGPoint(x: g.size.width - 13 , y: g.size.height * (1/2)), control2:
|
CGPoint(x: g.size.width, y: g.size.height * (1/2)), control2:
|
||||||
CGPoint(x: g.size.width - 13, y: 0))
|
CGPoint(x: g.size.width, y: 0))
|
||||||
}.stroke(style: StrokeStyle(lineWidth: 5, lineCap: .round))
|
}.stroke(style: StrokeStyle(lineWidth: 5, lineCap: .round))
|
||||||
Path { path in
|
Path { path in
|
||||||
path.move(to: CGPoint(x: g.size.width - 23, y: 0))
|
path.move(to: CGPoint(x: g.size.width - 10, y: 0))
|
||||||
path.addLine(to: CGPoint(x: g.size.width - 13, y: -10))
|
path.addLine(to: CGPoint(x: g.size.width, y: -10))
|
||||||
path.addLine(to: CGPoint(x: g.size.width - 3, y: 0))
|
path.addLine(to: CGPoint(x: g.size.width + 10, y: 0))
|
||||||
}.fill()
|
}.fill()
|
||||||
}.frame(height: (windowGeometry.size.height/2) - 20).padding()
|
}.frame(height: (windowGeometry.size.height/2) - 20).padding()
|
||||||
Text("No Secrets").bold()
|
Text("No Secrets").bold()
|
||||||
@ -76,7 +48,7 @@ struct EmptyStoreModifiableView: View {
|
|||||||
struct EmptyStoreModifiableView_Previews: PreviewProvider {
|
struct EmptyStoreModifiableView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
Group {
|
Group {
|
||||||
EmptyStoreImmutableView()
|
EmptyStoreView()
|
||||||
EmptyStoreModifiableView()
|
EmptyStoreModifiableView()
|
||||||
}
|
}
|
||||||
}
|
}
|
29
Secretive/Views/NoStoresView.swift
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
//
|
||||||
|
// NoStoresView.swift
|
||||||
|
// Secretive
|
||||||
|
//
|
||||||
|
// Created by Max Goedjen on 3/20/20.
|
||||||
|
// Copyright © 2020 Max Goedjen. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct NoStoresView: View {
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
Text("No Secure Storage Available").bold()
|
||||||
|
Text("Your Mac doesn't have a Secure Enclave, and there's not a compatible Smart Card inserted.")
|
||||||
|
Button(action: {
|
||||||
|
NSWorkspace.shared.open(URL(string: "https://www.yubico.com/products/compare-yubikey-5-series/")!)
|
||||||
|
}) {
|
||||||
|
Text("If you're looking to add one to your Mac, the YubiKey 5 Series are great.")
|
||||||
|
}
|
||||||
|
}.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NoStoresView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
NoStoresView()
|
||||||
|
}
|
||||||
|
}
|
73
Secretive/Views/NoticeView.swift
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct NoticeView: View {
|
||||||
|
|
||||||
|
let text: String
|
||||||
|
let severity: Severity
|
||||||
|
let actionTitle: String?
|
||||||
|
let action: (() -> Void)?
|
||||||
|
let secondaryActionTitle: String?
|
||||||
|
let secondaryAction: (() -> Void)?
|
||||||
|
|
||||||
|
public init(text: String, severity: NoticeView.Severity, actionTitle: String?, action: (() -> Void)?, secondaryActionTitle: String? = nil, secondaryAction: (() -> Void)? = nil) {
|
||||||
|
self.text = text
|
||||||
|
self.severity = severity
|
||||||
|
self.actionTitle = actionTitle
|
||||||
|
self.action = action
|
||||||
|
self.secondaryActionTitle = secondaryActionTitle
|
||||||
|
self.secondaryAction = secondaryAction
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
Text(text).bold()
|
||||||
|
Spacer()
|
||||||
|
if action != nil {
|
||||||
|
if secondaryAction != nil {
|
||||||
|
Button(action: secondaryAction!) {
|
||||||
|
Text(secondaryActionTitle!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button(action: action!) {
|
||||||
|
Text(actionTitle!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.padding().background(color)
|
||||||
|
}
|
||||||
|
|
||||||
|
var color: Color {
|
||||||
|
switch severity {
|
||||||
|
case .advisory:
|
||||||
|
return Color.orange
|
||||||
|
case .critical:
|
||||||
|
return Color.red
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NoticeView {
|
||||||
|
|
||||||
|
enum Severity {
|
||||||
|
case advisory, critical
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
|
||||||
|
struct NoticeView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
Group {
|
||||||
|
NoticeView(text: "Agent Not Running", severity: .advisory, actionTitle: "Run Setup") {
|
||||||
|
print("OK")
|
||||||
|
}
|
||||||
|
NoticeView(text: "Critical Security Update Required", severity: .critical, actionTitle: "Update") {
|
||||||
|
print("OK")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
67
Secretive/Views/SecretDetailView.swift
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SecretKit
|
||||||
|
|
||||||
|
struct SecretDetailView<SecretType: Secret>: View {
|
||||||
|
|
||||||
|
@State var secret: SecretType
|
||||||
|
let keyWriter = OpenSSHKeyWriter()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
Section {
|
||||||
|
GroupBox(label: Text("Fingerprint")) {
|
||||||
|
HStack {
|
||||||
|
Text(keyWriter.openSSHFingerprint(secret: secret))
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.frame(minWidth: 150, maxWidth: .infinity)
|
||||||
|
.padding()
|
||||||
|
}.onDrag {
|
||||||
|
return NSItemProvider(item: NSData(data: self.keyWriter.openSSHFingerprint(secret: self.secret).data(using: .utf8)!), typeIdentifier: kUTTypeUTF8PlainText as String)
|
||||||
|
}
|
||||||
|
Spacer().frame(height: 10)
|
||||||
|
GroupBox(label: Text("Public Key")) {
|
||||||
|
VStack {
|
||||||
|
Text(keyWriter.openSSHString(secret: secret))
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
.frame(minWidth: 150, maxWidth: .infinity)
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Button(action: copy) {
|
||||||
|
Text("Copy")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.onDrag {
|
||||||
|
return NSItemProvider(item: NSData(data: self.keyString.data(using: .utf8)!), typeIdentifier: kUTTypeUTF8PlainText as String)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}.padding()
|
||||||
|
.frame(minHeight: 150, maxHeight: .infinity)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var keyString: String {
|
||||||
|
keyWriter.openSSHString(secret: secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
func copy() {
|
||||||
|
NSPasteboard.general.declareTypes([.string], owner: nil)
|
||||||
|
NSPasteboard.general.setString(keyString, forType: .string)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
|
||||||
|
struct SecretDetailView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SecretDetailView(secret: Preview.Store(numberOfRandomSecrets: 1).secrets[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
145
Secretive/Views/SetupView.swift
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SetupView: View {
|
||||||
|
|
||||||
|
var completion: ((Bool) -> Void)?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
SetupStepView<Spacer>(text: "Secretive needs to install a helper app to sign requests when the main app isn't running. This app is called \"SecretAgent\" and you might see it in Activity Manager from time to time.",
|
||||||
|
index: 1,
|
||||||
|
nestedView: nil,
|
||||||
|
actionText: "Install") {
|
||||||
|
self.installLaunchAgent()
|
||||||
|
}
|
||||||
|
SetupStepView(text: "Add this line to your shell config (.bashrc or .zshrc) telling SSH to talk to SecretAgent when it wants to authenticate. Drag this into your config file.",
|
||||||
|
index: 2,
|
||||||
|
nestedView: SetupStepCommandView(text: Constants.socketPrompt),
|
||||||
|
actionText: "Added") {
|
||||||
|
self.markAsDone()
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Button(action: { self.completion?(true) }) {
|
||||||
|
Text("Finish")
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}.frame(minWidth: 640, minHeight: 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SetupStepView<NestedViewType: View>: View {
|
||||||
|
|
||||||
|
let text: String
|
||||||
|
let index: Int
|
||||||
|
let nestedView: NestedViewType?
|
||||||
|
@State var completed = false
|
||||||
|
let actionText: String
|
||||||
|
let action: (() -> Bool)
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Section {
|
||||||
|
HStack {
|
||||||
|
ZStack {
|
||||||
|
if completed {
|
||||||
|
Circle().foregroundColor(.green)
|
||||||
|
.frame(width: 30, height: 30)
|
||||||
|
Text("✓")
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.bold()
|
||||||
|
} else {
|
||||||
|
Circle().foregroundColor(.blue)
|
||||||
|
.frame(width: 30, height: 30)
|
||||||
|
Text(String(describing: index))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.bold()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
VStack {
|
||||||
|
Text(text)
|
||||||
|
.opacity(completed ? 0.5 : 1)
|
||||||
|
.lineLimit(nil)
|
||||||
|
if nestedView != nil {
|
||||||
|
nestedView!.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
Button(action: {
|
||||||
|
self.completed = self.action()
|
||||||
|
}) {
|
||||||
|
Text(actionText)
|
||||||
|
}.disabled(completed)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SetupStepCommandView: View {
|
||||||
|
|
||||||
|
let text: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text(text)
|
||||||
|
.lineLimit(nil)
|
||||||
|
.font(.system(.caption, design: .monospaced))
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
.frame(minHeight: 50)
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Button(action: copy) {
|
||||||
|
Text("Copy")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color(white: 0, opacity: 0.10))
|
||||||
|
.cornerRadius(10)
|
||||||
|
.onDrag {
|
||||||
|
return NSItemProvider(item: NSData(data: self.text.data(using: .utf8)!), typeIdentifier: kUTTypeUTF8PlainText as String)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func copy() {
|
||||||
|
NSPasteboard.general.declareTypes([.string], owner: nil)
|
||||||
|
NSPasteboard.general.setString(text, forType: .string)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SetupView {
|
||||||
|
|
||||||
|
func installLaunchAgent() -> Bool {
|
||||||
|
LaunchAgentController().install()
|
||||||
|
}
|
||||||
|
|
||||||
|
func markAsDone() -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SetupView {
|
||||||
|
|
||||||
|
enum Constants {
|
||||||
|
static let socketPath = (NSHomeDirectory().replacingOccurrences(of: "com.maxgoedjen.Secretive.Host", with: "com.maxgoedjen.Secretive.SecretAgent") as NSString).appendingPathComponent("socket.ssh") as String
|
||||||
|
static let socketPrompt = "export SSH_AUTH_SOCK=\(socketPath)"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
|
||||||
|
struct SetupView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SetupView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
22
SecretiveTests/Info.plist
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
@ -1,74 +0,0 @@
|
|||||||
// swift-tools-version:5.5
|
|
||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
|
||||||
|
|
||||||
import PackageDescription
|
|
||||||
|
|
||||||
let package = Package(
|
|
||||||
name: "SecretivePackages",
|
|
||||||
platforms: [
|
|
||||||
.macOS(.v11)
|
|
||||||
],
|
|
||||||
products: [
|
|
||||||
.library(
|
|
||||||
name: "SecretKit",
|
|
||||||
targets: ["SecretKit"]),
|
|
||||||
.library(
|
|
||||||
name: "SecureEnclaveSecretKit",
|
|
||||||
targets: ["SecureEnclaveSecretKit"]),
|
|
||||||
.library(
|
|
||||||
name: "SmartCardSecretKit",
|
|
||||||
targets: ["SmartCardSecretKit"]),
|
|
||||||
.library(
|
|
||||||
name: "SecretAgentKit",
|
|
||||||
targets: ["SecretAgentKit"]),
|
|
||||||
.library(
|
|
||||||
name: "SecretAgentKitHeaders",
|
|
||||||
targets: ["SecretAgentKitHeaders"]),
|
|
||||||
.library(
|
|
||||||
name: "SecretAgentKitProtocol",
|
|
||||||
targets: ["SecretAgentKitProtocol"]),
|
|
||||||
.library(
|
|
||||||
name: "Brief",
|
|
||||||
targets: ["Brief"]),
|
|
||||||
],
|
|
||||||
dependencies: [
|
|
||||||
],
|
|
||||||
targets: [
|
|
||||||
.target(
|
|
||||||
name: "SecretKit"
|
|
||||||
),
|
|
||||||
.testTarget(
|
|
||||||
name: "SecretKitTests",
|
|
||||||
dependencies: ["SecretKit", "SecureEnclaveSecretKit", "SmartCardSecretKit"]
|
|
||||||
),
|
|
||||||
.target(
|
|
||||||
name: "SecureEnclaveSecretKit",
|
|
||||||
dependencies: ["SecretKit"]
|
|
||||||
),
|
|
||||||
.target(
|
|
||||||
name: "SmartCardSecretKit",
|
|
||||||
dependencies: ["SecretKit"]
|
|
||||||
),
|
|
||||||
.target(
|
|
||||||
name: "SecretAgentKit",
|
|
||||||
dependencies: ["SecretKit", "SecretAgentKitHeaders", "SecretAgentKitProtocol"]
|
|
||||||
),
|
|
||||||
.systemLibrary(
|
|
||||||
name: "SecretAgentKitHeaders"
|
|
||||||
),
|
|
||||||
.target(
|
|
||||||
name: "SecretAgentKitProtocol"
|
|
||||||
),
|
|
||||||
.testTarget(
|
|
||||||
name: "SecretAgentKitTests",
|
|
||||||
dependencies: ["SecretAgentKit"])
|
|
||||||
,
|
|
||||||
.target(
|
|
||||||
name: "Brief"
|
|
||||||
),
|
|
||||||
.testTarget(
|
|
||||||
name: "BriefTests",
|
|
||||||
dependencies: ["Brief"]
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
|
@ -1,15 +0,0 @@
|
|||||||
# ``Brief``
|
|
||||||
|
|
||||||
Brief is a collection of protocols and concrete implmentation describing updates.
|
|
||||||
|
|
||||||
## Topics
|
|
||||||
|
|
||||||
### Versioning
|
|
||||||
|
|
||||||
- ``SemVer``
|
|
||||||
- ``Release``
|
|
||||||
|
|
||||||
### Updater
|
|
||||||
|
|
||||||
- ``UpdaterProtocol``
|
|
||||||
- ``Updater``
|
|