Compare commits
1 Commits
newsetup_l
...
v1.0.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd0a1b0a68 |
BIN
.github/readme/app-dark.png
vendored
|
Before Width: | Height: | Size: 520 KiB |
BIN
.github/readme/app-light.png
vendored
|
Before Width: | Height: | Size: 519 KiB |
BIN
.github/readme/app.png
vendored
Normal file
|
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/localize_add.png
vendored
|
Before Width: | Height: | Size: 1.3 MiB |
BIN
.github/readme/localize_sidebar.png
vendored
|
Before Width: | Height: | Size: 162 KiB |
BIN
.github/readme/localize_translate.png
vendored
|
Before Width: | Height: | Size: 1.7 MiB |
BIN
.github/readme/notification.png
vendored
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.6 MiB |
BIN
.github/readme/touchid.png
vendored
|
Before Width: | Height: | Size: 259 KiB After Width: | Height: | Size: 135 KiB |
5
.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
|
|
||||||
|
|||||||
16
.github/templates/release.md
vendored
@@ -1,16 +0,0 @@
|
|||||||
Update description
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
|
|
||||||
## Fixes
|
|
||||||
|
|
||||||
|
|
||||||
## Minimum macOS Version
|
|
||||||
|
|
||||||
|
|
||||||
## Build
|
|
||||||
https://github.com/maxgoedjen/secretive/actions/runs/RUN_ID
|
|
||||||
|
|
||||||
## Attestation
|
|
||||||
https://github.com/maxgoedjen/secretive/attestations/ATTESTATION_ID
|
|
||||||
51
.github/workflows/nightly.yml
vendored
@@ -1,51 +0,0 @@
|
|||||||
name: Nightly
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 8 * * *"
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
# runs-on: macOS-latest
|
|
||||||
runs-on: macos-15
|
|
||||||
timeout-minutes: 10
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
- 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_26.0.app
|
|
||||||
- name: Update Build Number
|
|
||||||
env:
|
|
||||||
RUN_ID: ${{ github.run_id }}
|
|
||||||
run: |
|
|
||||||
sed -i '' -e "s/GITHUB_CI_VERSION/0.0.0/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: Attest
|
|
||||||
id: attest
|
|
||||||
uses: actions/attest-build-provenance@v2
|
|
||||||
with:
|
|
||||||
subject-path: 'Secretive.zip'
|
|
||||||
- name: Upload App to Artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: Secretive.zip
|
|
||||||
path: Secretive.zip
|
|
||||||
90
.github/workflows/release.yml
vendored
@@ -6,89 +6,83 @@ on:
|
|||||||
- '*'
|
- '*'
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
# runs-on: macOS-latest
|
runs-on: macOS-latest
|
||||||
runs-on: macos-15
|
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v1
|
||||||
- name: Setup Signing
|
- name: Setup Signing
|
||||||
env:
|
env:
|
||||||
SIGNING_DATA: ${{ secrets.SIGNING_DATA }}
|
SIGNING_DATA: ${{ secrets.SIGNING_DATA }}
|
||||||
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
|
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
|
||||||
HOST_PROFILE_DATA: ${{ secrets.HOST_PROFILE_DATA }}
|
HOST_PROFILE_DATA: ${{ secrets.HOST_PROFILE_DATA }}
|
||||||
AGENT_PROFILE_DATA: ${{ secrets.AGENT_PROFILE_DATA }}
|
AGENT_PROFILE_DATA: ${{ secrets.AGENT_PROFILE_DATA }}
|
||||||
APPLE_API_KEY_DATA: ${{ secrets.APPLE_API_KEY_DATA }}
|
|
||||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
|
||||||
run: ./.github/scripts/signing.sh
|
run: ./.github/scripts/signing.sh
|
||||||
- name: Set Environment
|
|
||||||
run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: swift test --build-system swiftbuild --package-path Sources/Packages
|
run: xcrun xcodebuild test -project Secretive.xcodeproj -scheme Secretive
|
||||||
build:
|
build:
|
||||||
# runs-on: macOS-latest
|
runs-on: macOS-latest
|
||||||
runs-on: macos-15
|
|
||||||
permissions:
|
|
||||||
id-token: write
|
|
||||||
contents: write
|
|
||||||
attestations: write
|
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v1
|
||||||
|
- name: Create Release
|
||||||
|
id: create_release
|
||||||
|
uses: actions/create-release@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref }}
|
||||||
|
release_name: ${{ github.ref }}
|
||||||
|
body: "Build: https://github.com/maxgoedjen/secretive/actions/runs/${{ github.run_id }}"
|
||||||
|
draft: true
|
||||||
|
prerelease: false
|
||||||
- name: Setup Signing
|
- name: Setup Signing
|
||||||
env:
|
env:
|
||||||
SIGNING_DATA: ${{ secrets.SIGNING_DATA }}
|
SIGNING_DATA: ${{ secrets.SIGNING_DATA }}
|
||||||
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
|
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
|
||||||
HOST_PROFILE_DATA: ${{ secrets.HOST_PROFILE_DATA }}
|
HOST_PROFILE_DATA: ${{ secrets.HOST_PROFILE_DATA }}
|
||||||
AGENT_PROFILE_DATA: ${{ secrets.AGENT_PROFILE_DATA }}
|
AGENT_PROFILE_DATA: ${{ secrets.AGENT_PROFILE_DATA }}
|
||||||
APPLE_API_KEY_DATA: ${{ secrets.APPLE_API_KEY_DATA }}
|
|
||||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
|
||||||
run: ./.github/scripts/signing.sh
|
run: ./.github/scripts/signing.sh
|
||||||
- name: Set Environment
|
|
||||||
run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app
|
|
||||||
- name: Update Build Number
|
- name: Update Build Number
|
||||||
env:
|
env:
|
||||||
TAG_NAME: ${{ github.ref }}
|
TAG_NAME: ${{ github.ref }}
|
||||||
RUN_ID: ${{ github.run_id }}
|
RUN_ID: ${{ github.run_id }}
|
||||||
run: |
|
run: |
|
||||||
export CLEAN_TAG=$(echo $TAG_NAME | sed -e 's/refs\/tags\/v//')
|
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_CI_VERSION/$CLEAN_TAG/g" Config/Config.xcconfig
|
||||||
sed -i '' -e "s/GITHUB_BUILD_NUMBER/1.$RUN_ID/g" Sources/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" Sources/Secretive/Credits.rtf
|
sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Secretive/Credits.rtf
|
||||||
- name: Build
|
- name: Build
|
||||||
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
|
run: xcrun xcodebuild -project Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
|
||||||
- name: Create ZIPs
|
- name: Create ZIPs
|
||||||
run: |
|
run: |
|
||||||
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive/Products/Applications/Secretive.app ./Secretive.zip
|
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive/Products/Applications/Secretive.app ./Secretive.zip
|
||||||
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive ./Xcode_Archive.zip
|
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive ./Archive.zip
|
||||||
- name: Notarize
|
- name: Notarize
|
||||||
env:
|
env:
|
||||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
APPLE_USERNAME: ${{ secrets.APPLE_USERNAME }}
|
||||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip
|
run: xcrun altool --notarize-app --primary-bundle-id "com.maxgoedjen.secretive.host" --username $APPLE_USERNAME --password $APPLE_PASSWORD --file Secretive.zip
|
||||||
- name: Attest
|
- name: Document SHAs
|
||||||
id: attest
|
|
||||||
uses: actions/attest-build-provenance@v2
|
|
||||||
with:
|
|
||||||
subject-path: 'Secretive.zip, Xcode_Archive.zip'
|
|
||||||
- name: Create Release
|
|
||||||
run: |
|
run: |
|
||||||
sed -i.tmp "s/RUN_ID/$RUN_ID/g" .github/templates/release.md
|
shasum -a 512 Secretive.zip
|
||||||
sed -i.tmp "s/ATTESTATION_ID/$ATTESTATION_ID/g" .github/templates/release.md
|
shasum -a 512 Archive.zip
|
||||||
gh release create $TAG_NAME -d -F .github/templates/release.md
|
- name: Upload App to Release
|
||||||
gh release upload Secretive.zip
|
id: upload-release-asset
|
||||||
gh release upload Xcode_Archive.zip
|
uses: actions/upload-release-asset@v1.0.1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
TAG_NAME: ${{ github.ref }}
|
with:
|
||||||
RUN_ID: ${{ github.run_id }}
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
ATTESTATION_ID: ${{ steps.attest.outputs.attestation-id }}
|
asset_path: ./Secretive.zip
|
||||||
- name: Upload App to Artifacts
|
asset_name: Secretive.zip
|
||||||
uses: actions/upload-artifact@v4
|
asset_content_type: application/zip
|
||||||
|
- 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
|
||||||
with:
|
with:
|
||||||
name: Secretive.zip
|
name: Secretive.zip
|
||||||
path: Secretive.zip
|
path: Secretive.zip
|
||||||
- name: Upload Archive to Artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: Xcode_Archive.zip
|
|
||||||
path: Xcode_Archive.zip
|
|
||||||
|
|||||||
22
.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-latest
|
runs-on: macOS-latest
|
||||||
runs-on: macos-15
|
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v1
|
||||||
- name: Set Environment
|
- name: Setup Signing
|
||||||
run: sudo xcrun xcode-select -s /Applications/Xcode_26.0.app
|
env:
|
||||||
- name: Test Main Packages
|
SIGNING_DATA: ${{ secrets.SIGNING_DATA }}
|
||||||
run: swift test --build-system swiftbuild --package-path Sources/Packages
|
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
|
||||||
- name: Test SecretKit Packages
|
HOST_PROFILE_DATA: ${{ secrets.HOST_PROFILE_DATA }}
|
||||||
run: swift test --build-system swiftbuild
|
AGENT_PROFILE_DATA: ${{ secrets.AGENT_PROFILE_DATA }}
|
||||||
|
run: ./.github/scripts/signing.sh
|
||||||
|
- name: Test
|
||||||
|
run: xcrun xcodebuild test -project Secretive.xcodeproj -scheme Secretive
|
||||||
|
|||||||
2
.gitignore
vendored
@@ -91,5 +91,3 @@ iOSInjectionProject/
|
|||||||
|
|
||||||
# Build script products
|
# Build script products
|
||||||
Archive.xcarchive
|
Archive.xcarchive
|
||||||
.DS_Store
|
|
||||||
contents.xcworkspacedata
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
# App Configuration
|
|
||||||
|
|
||||||
Instructions for setting up apps and shells has moved to [secretive-config-instructions](https://github.com/maxgoedjen/secretive-config-instructions)!
|
|
||||||
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,24 +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)
|
||||||
|
|
||||||
## Localization
|
|
||||||
|
|
||||||
If you'd like to contribute a translation, please see [Localizing](LOCALIZING.md) to get started.
|
|
||||||
|
|
||||||
## 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/Sources/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).
|
|
||||||
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 69 KiB |
@@ -1,59 +0,0 @@
|
|||||||
{
|
|
||||||
"fill" : {
|
|
||||||
"solid" : "srgb:0.00000,0.53333,1.00000,0.00000"
|
|
||||||
},
|
|
||||||
"groups" : [
|
|
||||||
{
|
|
||||||
"blur-material" : 0.5,
|
|
||||||
"layers" : [
|
|
||||||
{
|
|
||||||
"image-name" : "Icon 7.png",
|
|
||||||
"name" : "Signature",
|
|
||||||
"position" : {
|
|
||||||
"scale" : 1,
|
|
||||||
"translation-in-points" : [
|
|
||||||
64.00083178971097,
|
|
||||||
-58.21801551632592
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"image-name" : "Rectangle Copy 10.png",
|
|
||||||
"name" : "Border"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fill-specializations" : [
|
|
||||||
{
|
|
||||||
"appearance" : "tinted",
|
|
||||||
"value" : {
|
|
||||||
"solid" : "display-p3:0.00000,0.00000,0.00000,0.50000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"image-name" : "Rectangle 2 8.png",
|
|
||||||
"name" : "Backing",
|
|
||||||
"opacity-specializations" : [
|
|
||||||
{
|
|
||||||
"appearance" : "tinted",
|
|
||||||
"value" : 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"shadow" : {
|
|
||||||
"kind" : "layer-color",
|
|
||||||
"opacity" : 0.5
|
|
||||||
},
|
|
||||||
"specular" : true,
|
|
||||||
"translucency" : {
|
|
||||||
"enabled" : true,
|
|
||||||
"value" : 0.5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"supported-platforms" : {
|
|
||||||
"squares" : [
|
|
||||||
"macOS"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
44
FAQ.md
@@ -4,42 +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 [secretive-config-instructions](https://github.com/maxgoedjen/secretive-config-instructions) repo.
|
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 was working for me, but now it has stopped
|
|
||||||
|
|
||||||
Try running the "Setup Secretive" process by clicking on "Help", then "Setup Secretive." If that doesn't work, follow the process above.
|
|
||||||
|
|
||||||
### Secretive prompts me to type my password instead of using my Apple Watch
|
|
||||||
|
|
||||||
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?
|
|
||||||
|
|
||||||
Beginning with Secretive 2.2, every secret has an automatically generated public key file representation on disk, and the path to it is listed under "Public Key Path" in Secretive. You can 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.
|
|
||||||
|
|
||||||
### How can I generate an RSA key?
|
|
||||||
|
|
||||||
The Mac's Secure Enclave only supports 256-bit EC keys, so inherently Secretive cannot support generating RSA keys.
|
|
||||||
|
|
||||||
### Can I use Secretive for SSH Agent Forwarding?
|
|
||||||
|
|
||||||
Yes, you can! Once you've set up Secretive, just add `ForwardAgent yes` to the hosts you want to forward to in your SSH config file. Afterwards, any use of one of your SSH keys on the remote host must be authenticated through Secretive.
|
|
||||||
|
|
||||||
### 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).
|
||||||
@@ -48,14 +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/Sources/Packages/Sources/Brief/Updater.swift).
|
|
||||||
|
|
||||||
### How do I uninstall Secretive?
|
|
||||||
|
|
||||||
Drag Secretive.app to the trash and remove `~/Library/Containers/com.maxgoedjen.Secretive.SecretAgent`. `SecretAgent` may continue running until you quit it or reboot.
|
|
||||||
|
|
||||||
### 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.
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
# Localizing Secretive
|
|
||||||
|
|
||||||
If you speak another language, and would like to help translate Secretive to support that language, we'd love your help!
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
### Download Xcode
|
|
||||||
|
|
||||||
Download the latest version of Xcode (at minimum, Xcode 15) from [Apple](http://developer.apple.com/download/applications/).
|
|
||||||
|
|
||||||
### Clone Secretive
|
|
||||||
|
|
||||||
Clone Secretive using [these instructions from GitHub](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository).
|
|
||||||
|
|
||||||
### Open Secretive
|
|
||||||
|
|
||||||
Open [Sources/Secretive.xcodeproj](Sources/Secretive.xcodeproj) in Xcode.
|
|
||||||
|
|
||||||
### Translate
|
|
||||||
|
|
||||||
Navigate to [Secretive/Localizable](Sources/Secretive/Localizable.xcstrings).
|
|
||||||
|
|
||||||
<img src="/.github/readme/localize_sidebar.png" alt="Screenshot of Xcode navigating to the Localizable file" width="300">
|
|
||||||
|
|
||||||
If your language already has an in-progress localization, select it from the list. If it isn't there, hit the "+" button and choose your language from the list.
|
|
||||||
|
|
||||||
<img src="/.github/readme/localize_add.png" alt="Screenshot of Xcode adding a new language" width="600">
|
|
||||||
|
|
||||||
Start translating! You'll see a list of english phrases, and a space to add a translation of your language.
|
|
||||||
|
|
||||||
### Create a Pull Request
|
|
||||||
|
|
||||||
Push your changes and open a pull request.
|
|
||||||
|
|
||||||
### Questions
|
|
||||||
|
|
||||||
Please open an issue if you have a question about translating the app. I'm more than happy to clarify any terms that are ambiguous or confusing. Thanks for contributing!
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
// swift-tools-version:6.2
|
|
||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
|
||||||
|
|
||||||
import PackageDescription
|
|
||||||
|
|
||||||
// This is basically the same package as `Sources/Packages/Package.swift`, but thinned slightly.
|
|
||||||
// Ideally this would be the same package, but SPM requires it to be at the root of the project,
|
|
||||||
// and Xcode does _not_ like that, so they're separate.
|
|
||||||
let package = Package(
|
|
||||||
name: "SecretKit",
|
|
||||||
defaultLocalization: "en",
|
|
||||||
platforms: [
|
|
||||||
.macOS(.v14)
|
|
||||||
],
|
|
||||||
products: [
|
|
||||||
.library(
|
|
||||||
name: "SecretKit",
|
|
||||||
targets: ["SecretKit"]),
|
|
||||||
.library(
|
|
||||||
name: "SecureEnclaveSecretKit",
|
|
||||||
targets: ["SecureEnclaveSecretKit"]),
|
|
||||||
.library(
|
|
||||||
name: "SmartCardSecretKit",
|
|
||||||
targets: ["SmartCardSecretKit"]),
|
|
||||||
],
|
|
||||||
dependencies: [
|
|
||||||
],
|
|
||||||
targets: [
|
|
||||||
.target(
|
|
||||||
name: "SecretKit",
|
|
||||||
dependencies: [],
|
|
||||||
path: "Sources/Packages/Sources/SecretKit",
|
|
||||||
resources: [localization],
|
|
||||||
swiftSettings: swiftSettings
|
|
||||||
),
|
|
||||||
.testTarget(
|
|
||||||
name: "SecretKitTests",
|
|
||||||
dependencies: ["SecretKit", "SecureEnclaveSecretKit", "SmartCardSecretKit"],
|
|
||||||
path: "Sources/Packages/Tests/SecretKitTests",
|
|
||||||
swiftSettings: swiftSettings
|
|
||||||
),
|
|
||||||
.target(
|
|
||||||
name: "SecureEnclaveSecretKit",
|
|
||||||
dependencies: ["SecretKit"],
|
|
||||||
path: "Sources/Packages/Sources/SecureEnclaveSecretKit",
|
|
||||||
resources: [localization],
|
|
||||||
swiftSettings: swiftSettings
|
|
||||||
),
|
|
||||||
.target(
|
|
||||||
name: "SmartCardSecretKit",
|
|
||||||
dependencies: ["SecretKit"],
|
|
||||||
path: "Sources/Packages/Sources/SmartCardSecretKit",
|
|
||||||
resources: [localization],
|
|
||||||
swiftSettings: swiftSettings
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
var localization: Resource {
|
|
||||||
.process("../../Localizable.xcstrings")
|
|
||||||
}
|
|
||||||
|
|
||||||
var swiftSettings: [PackageDescription.SwiftSetting] {
|
|
||||||
[
|
|
||||||
.swiftLanguageMode(.v6),
|
|
||||||
// This freaks out Xcode in a dependency context.
|
|
||||||
// .treatAllWarnings(as: .error),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
29
README.md
@@ -1,12 +1,9 @@
|
|||||||
# Secretive [](https://github.com/maxgoedjen/secretive/actions/workflows/test.yml) 
|
# Secretive  
|
||||||
|
|
||||||
|
|
||||||
Secretive is an app for storing and managing SSH keys in the Secure Enclave. It is inspired by the [sekey project](https://github.com/sekey/sekey), but rewritten in Swift with no external dependencies and with a handy native management app.
|
Secretive is an app for storing and managing SSH keys in the Secure Enclave. It is inspired by the [sekey project](https://github.com/sekey/sekey), but rewritten in Swift with no external dependencies and with a handy native management app.
|
||||||
|
|
||||||
<picture>
|
<img src="/.github/readme/app.png" alt="Screenshot of Secretive" width="600">
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="/.github/readme/app-dark.png">
|
|
||||||
<img src="/.github/readme/app-light.png" alt="Screenshot of Secretive" width="600">
|
|
||||||
</picture>
|
|
||||||
|
|
||||||
|
|
||||||
## Why?
|
## Why?
|
||||||
@@ -17,15 +14,15 @@ The most common setup for SSH keys is just keeping them on disk, guarded by prop
|
|||||||
|
|
||||||
### Access Control
|
### Access Control
|
||||||
|
|
||||||
If your Mac has a Secure Enclave, it also has support for strong access controls like Touch ID, or authentication with Apple Watch. You can configure your keys so that they require Touch ID (or Watch) authentication before they're accessed.
|
If your Mac has a Secure Enclave, it also has support for strong access controls like Touch ID, or authentication with Apple Watch. You can configure your key so that they require Touch ID (or Watch) authentication before they're accessed.
|
||||||
|
|
||||||
<img src="/.github/readme/touchid.png" alt="Screenshot of Secretive authenticating with Touch ID" width="400">
|
<img src="/.github/readme/touchid.png" alt="Screenshot of Secretive authenticating with Touch ID">
|
||||||
|
|
||||||
### 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" width="600">
|
<img src="/.github/readme/notification.png" alt="Screenshot of Secretive notifying the user">
|
||||||
|
|
||||||
### Support for Smart Cards Too!
|
### Support for Smart Cards Too!
|
||||||
|
|
||||||
@@ -33,23 +30,13 @@ 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).
|
||||||
|
|
||||||
### Auditable Build Process
|
### Auditable Build Process
|
||||||
|
|
||||||
Builds are produced by GitHub Actions with an auditable build and release generation process. Starting with Secretive 3.0, builds are attested using [GitHub Artifact Attestation](https://docs.github.com/en/actions/concepts/security/artifact-attestations). Attestations are viewable in the build log for a build, and also on the [main attestation page](https://github.com/maxgoedjen/secretive/attestations).
|
Builds are produced by GitHub Actions with an auditable build and release generation process. Each build has a "Document SHAs" step, which will output SHA checksums for the build produced by the GitHub Action, so you can verify that the source code for a given build corresponds to any given release.
|
||||||
|
|
||||||
### A Note Around Code Signing and Keychains
|
### A Note Around Code Signing and Keychains
|
||||||
|
|
||||||
@@ -57,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
|
||||||
|
|
||||||
|
|||||||
27
SECURITY.md
@@ -1,27 +0,0 @@
|
|||||||
# Security Policy
|
|
||||||
|
|
||||||
## Security Principles
|
|
||||||
|
|
||||||
Secretive is designed with a few general tenets in mind:
|
|
||||||
|
|
||||||
### It's Hard to Leak a Key Secretive Can't Read The Key Material
|
|
||||||
|
|
||||||
Secretive only operates on hardware-backed keys. In general terms, this means that it should be _very_ hard for Secretive to have any sort of bug that causes a key to be shared, because Secretive can't access private key data even if it wants to.
|
|
||||||
|
|
||||||
### Simplicity and Auditability
|
|
||||||
|
|
||||||
Secretive won't expand to have every feature it could possibly have. Part of the goal of the app is that it is possible for consumers to reasonably audit the code, and that often means not implementing features that might be cool, but which would significantly inflate the size of the codebase.
|
|
||||||
|
|
||||||
### Dependencies
|
|
||||||
|
|
||||||
Both in support of the previous principle and to rule out supply chain attacks, Secretive does not rely on any third party dependencies.
|
|
||||||
|
|
||||||
There are limited exceptions to this, particularly in the build process, but the app itself does not depend on any third party code.
|
|
||||||
|
|
||||||
## 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:))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,61 +1,53 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "Icon-macOS-ClearDark-16x16@1x.png",
|
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "1x",
|
"scale" : "1x",
|
||||||
"size" : "16x16"
|
"size" : "16x16"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "Icon-macOS-ClearDark-16x16@2x.png",
|
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "16x16"
|
"size" : "16x16"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "Icon-macOS-ClearDark-32x32@1x.png",
|
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "1x",
|
"scale" : "1x",
|
||||||
"size" : "32x32"
|
"size" : "32x32"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "Icon-macOS-ClearDark-32x32@2x.png",
|
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "32x32"
|
"size" : "32x32"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "Icon-macOS-ClearDark-128x128@1x.png",
|
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "1x",
|
"scale" : "1x",
|
||||||
"size" : "128x128"
|
"size" : "128x128"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "Icon-macOS-ClearDark-128x128@2x.png",
|
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "128x128"
|
"size" : "128x128"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "Icon-macOS-ClearDark-256x256@1x.png",
|
"filename" : "Icon 2@1x.png",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "1x",
|
"scale" : "1x",
|
||||||
"size" : "256x256"
|
"size" : "256x256"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "Icon-macOS-ClearDark-256x256@2x.png",
|
"filename" : "Icon 2@2x.png",
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "256x256"
|
"size" : "256x256"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "Icon-macOS-ClearDark-512x512@1x.png",
|
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "1x",
|
"scale" : "1x",
|
||||||
"size" : "512x512"
|
"size" : "512x512"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "Icon-macOS-ClearDark-1024x1024@1x.png",
|
|
||||||
"idiom" : "mac",
|
"idiom" : "mac",
|
||||||
"scale" : "2x",
|
"scale" : "2x",
|
||||||
"size" : "512x512"
|
"size" : "512x512"
|
||||||
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,6 +2,10 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>com.apple.security.app-sandbox</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
<key>com.apple.security.smartcard</key>
|
<key>com.apple.security.smartcard</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>keychain-access-groups</key>
|
<key>keychain-access-groups</key>
|
||||||
183
SecretAgentKit/Agent.swift
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import Foundation
|
||||||
|
import CryptoKit
|
||||||
|
import OSLog
|
||||||
|
import SecretKit
|
||||||
|
import AppKit
|
||||||
|
|
||||||
|
public class Agent {
|
||||||
|
|
||||||
|
fileprivate let storeList: SecretStoreList
|
||||||
|
fileprivate let witness: SigningWitness?
|
||||||
|
fileprivate let writer = OpenSSHKeyWriter()
|
||||||
|
fileprivate let requestTracer = SigningRequestTracer()
|
||||||
|
|
||||||
|
public init(storeList: SecretStoreList, witness: SigningWitness? = nil) {
|
||||||
|
os_log(.debug, "Agent is running")
|
||||||
|
self.storeList = storeList
|
||||||
|
self.witness = witness
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Agent {
|
||||||
|
|
||||||
|
public func handle(reader: FileHandleReader, writer: FileHandleWriter) {
|
||||||
|
os_log(.debug, "Agent handling new data")
|
||||||
|
let data = reader.availableData
|
||||||
|
guard !data.isEmpty else { return }
|
||||||
|
let requestTypeInt = data[4]
|
||||||
|
guard let requestType = SSHAgent.RequestType(rawValue: requestTypeInt) else {
|
||||||
|
writer.write(OpenSSHKeyWriter().lengthAndData(of: SSHAgent.ResponseType.agentFailure.data))
|
||||||
|
os_log(.debug, "Agent returned %@", SSHAgent.ResponseType.agentFailure.debugDescription)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
os_log(.debug, "Agent handling request of type %@", requestType.debugDescription)
|
||||||
|
let subData = Data(data[5...])
|
||||||
|
let response = handle(requestType: requestType, data: subData, reader: reader)
|
||||||
|
writer.write(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handle(requestType: SSHAgent.RequestType, data: Data, reader: FileHandleReader) -> Data {
|
||||||
|
var response = Data()
|
||||||
|
do {
|
||||||
|
switch requestType {
|
||||||
|
case .requestIdentities:
|
||||||
|
response.append(SSHAgent.ResponseType.agentIdentitiesAnswer.data)
|
||||||
|
response.append(identities())
|
||||||
|
os_log(.debug, "Agent returned %@", SSHAgent.ResponseType.agentIdentitiesAnswer.debugDescription)
|
||||||
|
case .signRequest:
|
||||||
|
let provenance = requestTracer.provenance(from: reader)
|
||||||
|
response.append(SSHAgent.ResponseType.agentSignResponse.data)
|
||||||
|
response.append(try sign(data: data, provenance: provenance))
|
||||||
|
os_log(.debug, "Agent returned %@", SSHAgent.ResponseType.agentSignResponse.debugDescription)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
response.removeAll()
|
||||||
|
response.append(SSHAgent.ResponseType.agentFailure.data)
|
||||||
|
os_log(.debug, "Agent returned %@", SSHAgent.ResponseType.agentFailure.debugDescription)
|
||||||
|
}
|
||||||
|
let full = OpenSSHKeyWriter().lengthAndData(of: response)
|
||||||
|
return full
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Agent {
|
||||||
|
|
||||||
|
func identities() -> Data {
|
||||||
|
// 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
|
||||||
|
let countData = Data(bytes: &count, count: UInt32.bitWidth/8)
|
||||||
|
var keyData = Data()
|
||||||
|
let writer = OpenSSHKeyWriter()
|
||||||
|
for secret in secrets {
|
||||||
|
let keyBlob = writer.data(secret: secret)
|
||||||
|
keyData.append(writer.lengthAndData(of: keyBlob))
|
||||||
|
let curveData = writer.curveType(for: secret.algorithm, length: secret.keySize).data(using: .utf8)!
|
||||||
|
keyData.append(writer.lengthAndData(of: curveData))
|
||||||
|
}
|
||||||
|
os_log(.debug, "Agent enumerated %@ identities", secrets.count as NSNumber)
|
||||||
|
return countData + keyData
|
||||||
|
}
|
||||||
|
|
||||||
|
func sign(data: Data, provenance: SigningRequestProvenance) throws -> Data {
|
||||||
|
let reader = OpenSSHReader(data: data)
|
||||||
|
let hash = reader.readNextChunk()
|
||||||
|
guard let (store, secret) = secret(matching: hash) else {
|
||||||
|
os_log(.debug, "Agent did not have a key matching %@", hash as NSData)
|
||||||
|
throw AgentError.noMatchingKey
|
||||||
|
}
|
||||||
|
|
||||||
|
if let witness = witness {
|
||||||
|
try witness.speakNowOrForeverHoldYourPeace(forAccessTo: secret, by: provenance)
|
||||||
|
}
|
||||||
|
|
||||||
|
let dataToSign = reader.readNextChunk()
|
||||||
|
let derSignature = try store.sign(data: dataToSign, with: secret)
|
||||||
|
|
||||||
|
let curveData = writer.curveType(for: secret.algorithm, length: secret.keySize).data(using: .utf8)!
|
||||||
|
|
||||||
|
// Convert from DER formatted rep to raw (r||s)
|
||||||
|
|
||||||
|
let rawRepresentation: Data
|
||||||
|
switch (secret.algorithm, secret.keySize) {
|
||||||
|
case (.ellipticCurve, 256):
|
||||||
|
rawRepresentation = try CryptoKit.P256.Signing.ECDSASignature(derRepresentation: derSignature).rawRepresentation
|
||||||
|
case (.ellipticCurve, 384):
|
||||||
|
rawRepresentation = try CryptoKit.P384.Signing.ECDSASignature(derRepresentation: derSignature).rawRepresentation
|
||||||
|
default:
|
||||||
|
throw AgentError.unsupportedKeyType
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let rawLength = rawRepresentation.count/2
|
||||||
|
// Check if we need to pad with 0x00 to prevent certain
|
||||||
|
// ssh servers from thinking r or s is negative
|
||||||
|
let paddingRange: ClosedRange<UInt8> = 0x80...0xFF
|
||||||
|
var r = Data(rawRepresentation[0..<rawLength])
|
||||||
|
if paddingRange ~= r.first! {
|
||||||
|
r.insert(0x00, at: 0)
|
||||||
|
}
|
||||||
|
var s = Data(rawRepresentation[rawLength...])
|
||||||
|
if paddingRange ~= s.first! {
|
||||||
|
s.insert(0x00, at: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var signatureChunk = Data()
|
||||||
|
signatureChunk.append(writer.lengthAndData(of: r))
|
||||||
|
signatureChunk.append(writer.lengthAndData(of: s))
|
||||||
|
|
||||||
|
var signedData = Data()
|
||||||
|
var sub = Data()
|
||||||
|
sub.append(writer.lengthAndData(of: curveData))
|
||||||
|
sub.append(writer.lengthAndData(of: signatureChunk))
|
||||||
|
signedData.append(writer.lengthAndData(of: sub))
|
||||||
|
|
||||||
|
if let witness = witness {
|
||||||
|
try witness.witness(accessTo: secret, by: provenance)
|
||||||
|
}
|
||||||
|
|
||||||
|
os_log(.debug, "Agent signed request")
|
||||||
|
|
||||||
|
return signedData
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Agent {
|
||||||
|
|
||||||
|
func secret(matching hash: Data) -> (AnySecretStore, AnySecret)? {
|
||||||
|
storeList.stores.compactMap { store -> (AnySecretStore, AnySecret)? in
|
||||||
|
let allMatching = store.secrets.filter { secret in
|
||||||
|
hash == writer.data(secret: secret)
|
||||||
|
}
|
||||||
|
if let matching = allMatching.first {
|
||||||
|
return (store, matching)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}.first
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension Agent {
|
||||||
|
|
||||||
|
enum AgentError: Error {
|
||||||
|
case unhandledType
|
||||||
|
case noMatchingKey
|
||||||
|
case unsupportedKeyType
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SSHAgent.ResponseType {
|
||||||
|
|
||||||
|
var data: Data {
|
||||||
|
var raw = self.rawValue
|
||||||
|
return Data(bytes: &raw, count: UInt8.bitWidth/8)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,21 +1,15 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Protocol abstraction of the reading aspects of FileHandle.
|
public protocol FileHandleReader {
|
||||||
public protocol FileHandleReader: Sendable {
|
|
||||||
|
|
||||||
/// 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: Sendable {
|
|
||||||
|
|
||||||
/// 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://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#section-5.1
|
|
||||||
public enum SSHAgent {}
|
public enum SSHAgent {}
|
||||||
|
|
||||||
extension SSHAgent {
|
extension SSHAgent {
|
||||||
|
|
||||||
/// The type of the SSH Agent Request, as described in https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#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,11 +18,8 @@ extension SSHAgent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The type of the SSH Agent Response, as described in https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#section-5.1
|
|
||||||
public enum ResponseType: UInt8, CustomDebugStringConvertible {
|
public enum ResponseType: UInt8, CustomDebugStringConvertible {
|
||||||
|
|
||||||
case agentFailure = 5
|
case agentFailure = 5
|
||||||
case agentSuccess = 6
|
|
||||||
case agentIdentitiesAnswer = 12
|
case agentIdentitiesAnswer = 12
|
||||||
case agentSignResponse = 14
|
case agentSignResponse = 14
|
||||||
|
|
||||||
@@ -33,8 +27,6 @@ extension SSHAgent {
|
|||||||
switch self {
|
switch self {
|
||||||
case .agentFailure:
|
case .agentFailure:
|
||||||
return "AgentFailure"
|
return "AgentFailure"
|
||||||
case .agentSuccess:
|
|
||||||
return "AgentSuccess"
|
|
||||||
case .agentIdentitiesAnswer:
|
case .agentIdentitiesAnswer:
|
||||||
return "AgentIdentitiesAnswer"
|
return "AgentIdentitiesAnswer"
|
||||||
case .agentSignResponse:
|
case .agentSignResponse:
|
||||||
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
|
||||||
|
|
||||||
|
}
|
||||||
67
SecretAgentKit/SocketController.swift
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import Foundation
|
||||||
|
import OSLog
|
||||||
|
|
||||||
|
public class SocketController {
|
||||||
|
|
||||||
|
fileprivate var fileHandle: FileHandle?
|
||||||
|
fileprivate var port: SocketPort?
|
||||||
|
public var handler: ((FileHandleReader, FileHandleWriter) -> Void)?
|
||||||
|
|
||||||
|
public init(path: String) {
|
||||||
|
os_log(.debug, "Socket controller setting up at %@", path)
|
||||||
|
if let _ = try? FileManager.default.removeItem(atPath: path) {
|
||||||
|
os_log(.debug, "Socket controller removed existing socket")
|
||||||
|
}
|
||||||
|
let exists = FileManager.default.fileExists(atPath: path)
|
||||||
|
assert(!exists)
|
||||||
|
os_log(.debug, "Socket controller path is clear")
|
||||||
|
port = socketPort(at: path)
|
||||||
|
configureSocket(at: path)
|
||||||
|
os_log(.debug, "Socket listening at %@", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func configureSocket(at path: String) {
|
||||||
|
guard let port = port else { return }
|
||||||
|
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(handleConnectionDataAvailable(notification:)), name: .NSFileHandleDataAvailable, object: nil)
|
||||||
|
fileHandle?.acceptConnectionInBackgroundAndNotify(forModes: [RunLoop.current.currentMode!])
|
||||||
|
}
|
||||||
|
|
||||||
|
func socketPort(at path: String) -> SocketPort {
|
||||||
|
var addr = sockaddr_un()
|
||||||
|
addr.sun_family = sa_family_t(AF_UNIX)
|
||||||
|
|
||||||
|
var len: Int = 0
|
||||||
|
_ = withUnsafeMutablePointer(to: &addr.sun_path.0) { pointer in
|
||||||
|
path.withCString { cstring in
|
||||||
|
len = strlen(cstring)
|
||||||
|
strncpy(pointer, cstring, len)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addr.sun_len = UInt8(len+2)
|
||||||
|
|
||||||
|
var data: Data!
|
||||||
|
_ = withUnsafePointer(to: &addr) { pointer in
|
||||||
|
data = Data(bytes: pointer, count: MemoryLayout<sockaddr_un>.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
return SocketPort(protocolFamily: AF_UNIX, socketType: SOCK_STREAM, protocol: 0, address: data)!
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func handleConnectionAccept(notification: Notification) {
|
||||||
|
os_log(.debug, "Socket controller accepted connection")
|
||||||
|
guard let new = notification.userInfo?[NSFileHandleNotificationFileHandleItem] as? FileHandle else { return }
|
||||||
|
handler?(new, new)
|
||||||
|
new.waitForDataInBackgroundAndNotify()
|
||||||
|
fileHandle?.acceptConnectionInBackgroundAndNotify(forModes: [RunLoop.current.currentMode!])
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func handleConnectionDataAvailable(notification: Notification) {
|
||||||
|
os_log(.debug, "Socket controller has new data available")
|
||||||
|
guard let new = notification.object as? FileHandle else { return }
|
||||||
|
os_log(.debug, "Socket controller received new file handle")
|
||||||
|
handler?(new, new)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
169
SecretAgentKitTests/AgentTests.swift
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import Foundation
|
||||||
|
import XCTest
|
||||||
|
import CryptoKit
|
||||||
|
@testable import SecretKit
|
||||||
|
@testable import SecretAgentKit
|
||||||
|
|
||||||
|
class AgentTests: XCTestCase {
|
||||||
|
|
||||||
|
let stubWriter = StubFileHandleWriter()
|
||||||
|
|
||||||
|
// MARK: Identity Listing
|
||||||
|
|
||||||
|
func testEmptyStores() {
|
||||||
|
let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestIdentities)
|
||||||
|
let agent = Agent(storeList: SecretStoreList())
|
||||||
|
agent.handle(reader: stubReader, writer: stubWriter)
|
||||||
|
XCTAssertEqual(stubWriter.data, Constants.Responses.requestIdentitiesEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIdentitiesList() {
|
||||||
|
let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestIdentities)
|
||||||
|
let list = storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
|
||||||
|
let agent = Agent(storeList: list)
|
||||||
|
agent.handle(reader: stubReader, writer: stubWriter)
|
||||||
|
XCTAssertEqual(stubWriter.data, Constants.Responses.requestIdentitiesMultiple)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Signatures
|
||||||
|
|
||||||
|
func testNoMatchingIdentities() {
|
||||||
|
let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignatureWithNoneMatching)
|
||||||
|
let list = storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
|
||||||
|
let agent = Agent(storeList: list)
|
||||||
|
agent.handle(reader: stubReader, writer: stubWriter)
|
||||||
|
// XCTAssertEqual(stubWriter.data, Constants.Responses.requestFailure)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSignature() {
|
||||||
|
let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature)
|
||||||
|
let requestReader = OpenSSHReader(data: Constants.Requests.requestSignature[5...])
|
||||||
|
_ = requestReader.readNextChunk()
|
||||||
|
let dataToSign = requestReader.readNextChunk()
|
||||||
|
let list = storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
|
||||||
|
let agent = Agent(storeList: list)
|
||||||
|
agent.handle(reader: stubReader, writer: stubWriter)
|
||||||
|
let outer = OpenSSHReader(data: stubWriter.data[5...])
|
||||||
|
let payload = outer.readNextChunk()
|
||||||
|
let inner = OpenSSHReader(data: payload)
|
||||||
|
_ = inner.readNextChunk()
|
||||||
|
let signedData = inner.readNextChunk()
|
||||||
|
let rsData = OpenSSHReader(data: signedData)
|
||||||
|
var r = rsData.readNextChunk()
|
||||||
|
var s = rsData.readNextChunk()
|
||||||
|
// This is fine IRL, but it freaks out CryptoKit
|
||||||
|
if r[0] == 0 {
|
||||||
|
r.removeFirst()
|
||||||
|
}
|
||||||
|
if s[0] == 0 {
|
||||||
|
s.removeFirst()
|
||||||
|
}
|
||||||
|
var rs = r
|
||||||
|
rs.append(s)
|
||||||
|
let signature = try! P256.Signing.ECDSASignature(rawRepresentation: rs)
|
||||||
|
let valid = try! P256.Signing.PublicKey(x963Representation: Constants.Secrets.ecdsa256Secret.publicKey).isValidSignature(signature, for: dataToSign)
|
||||||
|
XCTAssertTrue(valid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Witness protocol
|
||||||
|
|
||||||
|
func testWitnessObjectionStopsRequest() {
|
||||||
|
let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature)
|
||||||
|
let list = storeList(with: [Constants.Secrets.ecdsa256Secret])
|
||||||
|
let witness = StubWitness(speakNow: { _,_ in
|
||||||
|
return true
|
||||||
|
}, witness: { _, _ in })
|
||||||
|
let agent = Agent(storeList: list, witness: witness)
|
||||||
|
agent.handle(reader: stubReader, writer: stubWriter)
|
||||||
|
XCTAssertEqual(stubWriter.data, Constants.Responses.requestFailure)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testWitnessSignature() {
|
||||||
|
let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature)
|
||||||
|
let list = storeList(with: [Constants.Secrets.ecdsa256Secret])
|
||||||
|
var witnessed = false
|
||||||
|
let witness = StubWitness(speakNow: { _, trace in
|
||||||
|
return false
|
||||||
|
}, witness: { _, trace in
|
||||||
|
witnessed = true
|
||||||
|
})
|
||||||
|
let agent = Agent(storeList: list, witness: witness)
|
||||||
|
agent.handle(reader: stubReader, writer: stubWriter)
|
||||||
|
XCTAssertTrue(witnessed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRequestTracing() {
|
||||||
|
let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature)
|
||||||
|
let list = storeList(with: [Constants.Secrets.ecdsa256Secret])
|
||||||
|
var speakNowTrace: SigningRequestProvenance! = nil
|
||||||
|
var witnessTrace: SigningRequestProvenance! = nil
|
||||||
|
let witness = StubWitness(speakNow: { _, trace in
|
||||||
|
speakNowTrace = trace
|
||||||
|
return false
|
||||||
|
}, witness: { _, trace in
|
||||||
|
witnessTrace = trace
|
||||||
|
})
|
||||||
|
let agent = Agent(storeList: list, witness: witness)
|
||||||
|
agent.handle(reader: stubReader, writer: stubWriter)
|
||||||
|
XCTAssertEqual(witnessTrace, speakNowTrace)
|
||||||
|
XCTAssertEqual(witnessTrace.origin.name, "Finder")
|
||||||
|
XCTAssertEqual(witnessTrace.origin.validSignature, true)
|
||||||
|
XCTAssertEqual(witnessTrace.origin.parentPID, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Exception Handling
|
||||||
|
|
||||||
|
func testSignatureException() {
|
||||||
|
let stubReader = StubFileHandleReader(availableData: Constants.Requests.requestSignature)
|
||||||
|
let list = storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
|
||||||
|
let store = list.stores.first?.base as! Stub.Store
|
||||||
|
store.shouldThrow = true
|
||||||
|
let agent = Agent(storeList: list)
|
||||||
|
agent.handle(reader: stubReader, writer: stubWriter)
|
||||||
|
XCTAssertEqual(stubWriter.data, Constants.Responses.requestFailure)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Unsupported
|
||||||
|
|
||||||
|
func testUnhandledAdd() {
|
||||||
|
let stubReader = StubFileHandleReader(availableData: Constants.Requests.addIdentity)
|
||||||
|
let agent = Agent(storeList: SecretStoreList())
|
||||||
|
agent.handle(reader: stubReader, writer: stubWriter)
|
||||||
|
XCTAssertEqual(stubWriter.data, Constants.Responses.requestFailure)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AgentTests {
|
||||||
|
|
||||||
|
func storeList(with secrets: [Stub.Secret]) -> SecretStoreList {
|
||||||
|
let store = Stub.Store()
|
||||||
|
store.secrets.append(contentsOf: secrets)
|
||||||
|
let storeList = SecretStoreList()
|
||||||
|
storeList.add(store: store)
|
||||||
|
return storeList
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Constants {
|
||||||
|
|
||||||
|
enum Requests {
|
||||||
|
static let requestIdentities = Data(base64Encoded: "AAAAAQs=")!
|
||||||
|
static let addIdentity = Data(base64Encoded: "AAAAARE=")!
|
||||||
|
static let requestSignatureWithNoneMatching = Data(base64Encoded: "AAABhA0AAACIAAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBEqCbkJbOHy5S1wVCaJoKPmpS0egM4frMqllgnlRRQ/Uvnn6EVS8oV03cPA2Bz0EdESyRKA/sbmn0aBtgjIwGELxu45UXEW1TEz6TxyS0u3vuIqR3Wo1CrQWRDnkrG/pBQAAAO8AAAAgbqmrqPUtJ8mmrtaSVexjMYyXWNqjHSnoto7zgv86xvcyAAAAA2dpdAAAAA5zc2gtY29ubmVjdGlvbgAAAAlwdWJsaWNrZXkBAAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAACIAAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBEqCbkJbOHy5S1wVCaJoKPmpS0egM4frMqllgnlRRQ/Uvnn6EVS8oV03cPA2Bz0EdESyRKA/sbmn0aBtgjIwGELxu45UXEW1TEz6TxyS0u3vuIqR3Wo1CrQWRDnkrG/pBQAAAAA=")!
|
||||||
|
static let requestSignature = Data(base64Encoded: "AAABRA0AAABoAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKzOkUiVJEcACMtAd9X7xalbc0FYZyhbmv2dsWl4IP2GWIi+RcsaHQNw+nAIQ8CKEYmLnl0VLDp5Ef8KMhgIy08AAADPAAAAIBIFsbCZ4/dhBmLNGHm0GKj7EJ4N8k/jXRxlyg+LFIYzMgAAAANnaXQAAAAOc3NoLWNvbm5lY3Rpb24AAAAJcHVibGlja2V5AQAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAAaAAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQSszpFIlSRHAAjLQHfV+8WpW3NBWGcoW5r9nbFpeCD9hliIvkXLGh0DcPpwCEPAihGJi55dFSw6eRH/CjIYCMtPAAAAAA==")!
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Responses {
|
||||||
|
static let requestIdentitiesEmpty = Data(base64Encoded: "AAAABQwAAAAA")!
|
||||||
|
static let requestIdentitiesMultiple = Data(base64Encoded: "AAABKwwAAAACAAAAaAAAABNlY2RzYS1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQSszpFIlSRHAAjLQHfV+8WpW3NBWGcoW5r9nbFpeCD9hliIvkXLGh0DcPpwCEPAihGJi55dFSw6eRH/CjIYCMtPAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAACIAAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBLKSzA5q3jCb3q0JKigvcxfWVGrJ+bklpG0Zc9YzUwrbsh9SipvlSJi+sHQI+O0m88DOpRBAtuAHX60euD/Yv250tovN7/+MEFbXGZ/hLdd0BoFpWbLfJcQj806KJGlcDAAAABNlY2RzYS1zaGEyLW5pc3RwMzg0")!
|
||||||
|
static let requestFailure = Data(base64Encoded: "AAAAAQU=")!
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Secrets {
|
||||||
|
static let ecdsa256Secret = Stub.Secret(keySize: 256, publicKey: Data(base64Encoded: "BKzOkUiVJEcACMtAd9X7xalbc0FYZyhbmv2dsWl4IP2GWIi+RcsaHQNw+nAIQ8CKEYmLnl0VLDp5Ef8KMhgIy08=")!, privateKey: Data(base64Encoded: "BKzOkUiVJEcACMtAd9X7xalbc0FYZyhbmv2dsWl4IP2GWIi+RcsaHQNw+nAIQ8CKEYmLnl0VLDp5Ef8KMhgIy09nw780wy/TSfUmzj15iJkV234AaCLNl+H8qFL6qK8VIg==")!)
|
||||||
|
static let ecdsa384Secret = Stub.Secret(keySize: 384, publicKey: Data(base64Encoded: "BLKSzA5q3jCb3q0JKigvcxfWVGrJ+bklpG0Zc9YzUwrbsh9SipvlSJi+sHQI+O0m88DOpRBAtuAHX60euD/Yv250tovN7/+MEFbXGZ/hLdd0BoFpWbLfJcQj806KJGlcDA==")!, privateKey: Data(base64Encoded: "BLKSzA5q3jCb3q0JKigvcxfWVGrJ+bklpG0Zc9YzUwrbsh9SipvlSJi+sHQI+O0m88DOpRBAtuAHX60euD/Yv250tovN7/+MEFbXGZ/hLdd0BoFpWbLfJcQj806KJGlcDHNapAOzrt9E+9QC4/KYoXS7Uw4pmdAz53uIj02tttiq3c0ZyIQ7XoscWWRqRrz8Kw==")!)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
22
SecretAgentKitTests/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>
|
||||||
14
SecretAgentKitTests/StubFileHandleReader.swift
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import SecretAgentKit
|
||||||
|
import AppKit
|
||||||
|
|
||||||
|
struct StubFileHandleReader: FileHandleReader {
|
||||||
|
|
||||||
|
let availableData: Data
|
||||||
|
var fileDescriptor: Int32 {
|
||||||
|
NSWorkspace.shared.runningApplications.filter({ $0.localizedName == "Finder" }).first!.processIdentifier
|
||||||
|
}
|
||||||
|
var pidOfConnectedProcess: Int32 {
|
||||||
|
fileDescriptor
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
11
SecretAgentKitTests/StubFileHandleWriter.swift
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import SecretAgentKit
|
||||||
|
|
||||||
|
class StubFileHandleWriter: FileHandleWriter {
|
||||||
|
|
||||||
|
var data = Data()
|
||||||
|
|
||||||
|
func write(_ data: Data) {
|
||||||
|
self.data.append(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import Foundation
|
|
||||||
import SecretKit
|
import SecretKit
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
|
|
||||||
@@ -6,7 +5,7 @@ struct Stub {}
|
|||||||
|
|
||||||
extension Stub {
|
extension Stub {
|
||||||
|
|
||||||
public final class Store: SecretStore, @unchecked Sendable {
|
public class Store: SecretStore {
|
||||||
|
|
||||||
public let isAvailable = true
|
public let isAvailable = true
|
||||||
public let id = UUID()
|
public let id = UUID()
|
||||||
@@ -27,7 +26,7 @@ extension Stub {
|
|||||||
flags,
|
flags,
|
||||||
nil) as Any
|
nil) as Any
|
||||||
|
|
||||||
let attributes = KeychainDictionary([
|
let attributes = [
|
||||||
kSecAttrLabel: name,
|
kSecAttrLabel: name,
|
||||||
kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
|
kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
|
||||||
kSecAttrKeySizeInBits: size,
|
kSecAttrKeySizeInBits: size,
|
||||||
@@ -35,35 +34,40 @@ extension Stub {
|
|||||||
kSecAttrIsPermanent: true,
|
kSecAttrIsPermanent: true,
|
||||||
kSecAttrAccessControl: access
|
kSecAttrAccessControl: access
|
||||||
]
|
]
|
||||||
])
|
] as CFDictionary
|
||||||
|
|
||||||
let privateKey = SecKeyCreateRandomKey(attributes, nil)!
|
var privateKey: SecKey! = nil
|
||||||
let publicKey = SecKeyCopyPublicKey(privateKey)!
|
var publicKey: SecKey! = nil
|
||||||
|
SecKeyGeneratePair(attributes, &publicKey, &privateKey)
|
||||||
let publicAttributes = SecKeyCopyAttributes(publicKey) as! [CFString: Any]
|
let publicAttributes = SecKeyCopyAttributes(publicKey) as! [CFString: Any]
|
||||||
let privateAttributes = SecKeyCopyAttributes(privateKey) as! [CFString: Any]
|
let privateAttributes = SecKeyCopyAttributes(privateKey) as! [CFString: Any]
|
||||||
let publicData = (publicAttributes[kSecValueData] as! Data)
|
let publicData = (publicAttributes[kSecValueData] as! Data)
|
||||||
let privateData = (privateAttributes[kSecValueData] as! Data)
|
let privateData = (privateAttributes[kSecValueData] as! Data)
|
||||||
let secret = Secret(keySize: size, publicKey: publicData, privateKey: privateData)
|
let secret = Secret(keySize: size, publicKey: publicData, privateKey: privateData)
|
||||||
print(secret)
|
print(secret)
|
||||||
print("Public Key OpenSSH: \(OpenSSHPublicKeyWriter().openSSHString(secret: secret))")
|
print("Public Key OpenSSH: \(OpenSSHKeyWriter().openSSHString(secret: secret))")
|
||||||
}
|
}
|
||||||
|
|
||||||
public func sign(data: Data, with secret: Secret, for provenance: SigningRequestProvenance) throws -> Data {
|
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 = try CryptoKit.P256.Signing.PrivateKey(x963Representation: secret.privateKey)
|
let privateKey = SecKeyCreateWithData(secret.privateKey as CFData, [
|
||||||
return try privateKey.signature(for: data).rawRepresentation
|
kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
|
||||||
|
kSecAttrKeySizeInBits: secret.keySize,
|
||||||
|
kSecAttrKeyClass: kSecAttrKeyClassPrivate
|
||||||
|
] as CFDictionary
|
||||||
|
, nil)!
|
||||||
|
let signatureAlgorithm: SecKeyAlgorithm
|
||||||
|
switch secret.keySize {
|
||||||
|
case 256:
|
||||||
|
signatureAlgorithm = .ecdsaSignatureMessageX962SHA256
|
||||||
|
case 384:
|
||||||
|
signatureAlgorithm = .ecdsaSignatureMessageX962SHA384
|
||||||
|
default:
|
||||||
|
fatalError()
|
||||||
}
|
}
|
||||||
|
return SecKeyCreateSignature(privateKey, signatureAlgorithm, data as CFData, nil)! as Data
|
||||||
public func existingPersistedAuthenticationContext(secret: Stub.Secret) -> PersistedAuthenticationContext? {
|
|
||||||
nil
|
|
||||||
}
|
|
||||||
|
|
||||||
public func persistAuthentication(secret: Stub.Secret, forDuration duration: TimeInterval) throws {
|
|
||||||
}
|
|
||||||
|
|
||||||
public func reloadSecrets() {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -74,22 +78,23 @@ extension Stub {
|
|||||||
|
|
||||||
struct Secret: SecretKit.Secret, CustomDebugStringConvertible {
|
struct Secret: SecretKit.Secret, CustomDebugStringConvertible {
|
||||||
|
|
||||||
let id = Data(UUID().uuidString.utf8)
|
let id = UUID().uuidString.data(using: .utf8)!
|
||||||
let name = UUID().uuidString
|
let name = UUID().uuidString
|
||||||
let attributes: Attributes
|
let algorithm = Algorithm.ellipticCurve
|
||||||
|
|
||||||
|
let keySize: Int
|
||||||
let publicKey: Data
|
let publicKey: Data
|
||||||
let requiresAuthentication = false
|
|
||||||
let privateKey: Data
|
let privateKey: Data
|
||||||
|
|
||||||
init(keySize: Int, publicKey: Data, privateKey: Data) {
|
init(keySize: Int, publicKey: Data, privateKey: Data) {
|
||||||
self.attributes = Attributes(keyType: .init(algorithm: .ecdsa, size: keySize), authentication: .notRequired)
|
self.keySize = keySize
|
||||||
self.publicKey = publicKey
|
self.publicKey = publicKey
|
||||||
self.privateKey = privateKey
|
self.privateKey = privateKey
|
||||||
}
|
}
|
||||||
|
|
||||||
var debugDescription: String {
|
var debugDescription: String {
|
||||||
"""
|
"""
|
||||||
Key Size \(attributes.keyType.size)
|
Key Size \(keySize)
|
||||||
Private: \(privateKey.base64EncodedString())
|
Private: \(privateKey.base64EncodedString())
|
||||||
Public: \(publicKey.base64EncodedString())
|
Public: \(publicKey.base64EncodedString())
|
||||||
"""
|
"""
|
||||||
32
SecretAgentKitTests/StubWitness.swift
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import SecretKit
|
||||||
|
import SecretAgentKit
|
||||||
|
|
||||||
|
struct StubWitness {
|
||||||
|
|
||||||
|
let speakNow: (AnySecret, SigningRequestProvenance) -> Bool
|
||||||
|
let witness: (AnySecret, SigningRequestProvenance) -> ()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StubWitness: SigningWitness {
|
||||||
|
|
||||||
|
func speakNowOrForeverHoldYourPeace(forAccessTo secret: AnySecret, by provenance: SigningRequestProvenance) throws {
|
||||||
|
let objection = speakNow(secret, provenance)
|
||||||
|
if objection {
|
||||||
|
throw TheresMyChance()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func witness(accessTo secret: AnySecret, by provenance: SigningRequestProvenance) throws {
|
||||||
|
witness(secret, provenance)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StubWitness {
|
||||||
|
|
||||||
|
struct TheresMyChance: Error {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
62
SecretKit/Common/Erasers/AnySecret.swift
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct AnySecret: Secret {
|
||||||
|
|
||||||
|
let base: Any
|
||||||
|
fileprivate let hashable: AnyHashable
|
||||||
|
fileprivate let _id: () -> AnyHashable
|
||||||
|
fileprivate let _name: () -> String
|
||||||
|
fileprivate let _algorithm: () -> Algorithm
|
||||||
|
fileprivate let _keySize: () -> Int
|
||||||
|
fileprivate let _publicKey: () -> Data
|
||||||
|
|
||||||
|
public init<T>(_ secret: T) where T: Secret {
|
||||||
|
if let secret = secret as? AnySecret {
|
||||||
|
base = secret.base
|
||||||
|
hashable = secret.hashable
|
||||||
|
_id = secret._id
|
||||||
|
_name = secret._name
|
||||||
|
_algorithm = secret._algorithm
|
||||||
|
_keySize = secret._keySize
|
||||||
|
_publicKey = secret._publicKey
|
||||||
|
} else {
|
||||||
|
base = secret as Any
|
||||||
|
self.hashable = secret
|
||||||
|
_id = { secret.id as AnyHashable }
|
||||||
|
_name = { secret.name }
|
||||||
|
_algorithm = { secret.algorithm }
|
||||||
|
_keySize = { secret.keySize }
|
||||||
|
_publicKey = { secret.publicKey }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var id: AnyHashable {
|
||||||
|
_id()
|
||||||
|
}
|
||||||
|
|
||||||
|
public var name: String {
|
||||||
|
_name()
|
||||||
|
}
|
||||||
|
|
||||||
|
public var algorithm: Algorithm {
|
||||||
|
_algorithm()
|
||||||
|
}
|
||||||
|
|
||||||
|
public var keySize: Int {
|
||||||
|
_keySize()
|
||||||
|
}
|
||||||
|
|
||||||
|
public var publicKey: Data {
|
||||||
|
_publicKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func == (lhs: AnySecret, rhs: AnySecret) -> Bool {
|
||||||
|
lhs.hashable == rhs.hashable
|
||||||
|
}
|
||||||
|
|
||||||
|
public func hash(into hasher: inout Hasher) {
|
||||||
|
hashable.hash(into: &hasher)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
67
SecretKit/Common/Erasers/AnySecretStore.swift
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
public class AnySecretStore: SecretStore {
|
||||||
|
|
||||||
|
let base: Any
|
||||||
|
fileprivate let _isAvailable: () -> Bool
|
||||||
|
fileprivate let _id: () -> UUID
|
||||||
|
fileprivate let _name: () -> String
|
||||||
|
fileprivate let _secrets: () -> [AnySecret]
|
||||||
|
fileprivate let _sign: (Data, AnySecret) throws -> Data
|
||||||
|
fileprivate var sink: AnyCancellable?
|
||||||
|
|
||||||
|
public init<SecretStoreType>(_ secretStore: SecretStoreType) where SecretStoreType: SecretStore {
|
||||||
|
base = secretStore
|
||||||
|
_isAvailable = { secretStore.isAvailable }
|
||||||
|
_name = { secretStore.name }
|
||||||
|
_id = { secretStore.id }
|
||||||
|
_secrets = { secretStore.secrets.map { AnySecret($0) } }
|
||||||
|
_sign = { try secretStore.sign(data: $0, with: $1.base as! SecretStoreType.SecretType) }
|
||||||
|
sink = secretStore.objectWillChange.sink { _ in
|
||||||
|
self.objectWillChange.send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var isAvailable: Bool {
|
||||||
|
return _isAvailable()
|
||||||
|
}
|
||||||
|
|
||||||
|
public var id: UUID {
|
||||||
|
return _id()
|
||||||
|
}
|
||||||
|
|
||||||
|
public var name: String {
|
||||||
|
return _name()
|
||||||
|
}
|
||||||
|
|
||||||
|
public var secrets: [AnySecret] {
|
||||||
|
return _secrets()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func sign(data: Data, with secret: AnySecret) throws -> Data {
|
||||||
|
try _sign(data, secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AnySecretStoreModifiable: AnySecretStore, SecretStoreModifiable {
|
||||||
|
|
||||||
|
fileprivate let _create: (String, Bool) throws -> Void
|
||||||
|
fileprivate let _delete: (AnySecret) throws -> Void
|
||||||
|
|
||||||
|
public init<SecretStoreType>(modifiable secretStore: SecretStoreType) where SecretStoreType: SecretStoreModifiable {
|
||||||
|
_create = { try secretStore.create(name: $0, requiresAuthentication: $1) }
|
||||||
|
_delete = { try secretStore.delete(secret: $0.base as! SecretStoreType.SecretType) }
|
||||||
|
super.init(secretStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func create(name: String, requiresAuthentication: Bool) throws {
|
||||||
|
try _create(name, requiresAuthentication)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func delete(secret: AnySecret) throws {
|
||||||
|
try _delete(secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
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,23 +1,20 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Reads OpenSSH protocol data.
|
public class OpenSSHReader {
|
||||||
public final 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]
|
||||||
remaining.removeSubrange(lengthRange)
|
remaining.removeSubrange(lengthRange)
|
||||||
let littleEndianLength = lengthChunk.bytes.unsafeLoad(as: UInt32.self)
|
let littleEndianLength = lengthChunk.withUnsafeBytes { pointer in
|
||||||
|
return pointer.load(as: UInt32.self)
|
||||||
|
}
|
||||||
let length = Int(littleEndianLength.bigEndian)
|
let length = Int(littleEndianLength.bigEndian)
|
||||||
let dataRange = 0..<length
|
let dataRange = 0..<length
|
||||||
let ret = Data(remaining[dataRange])
|
let ret = Data(remaining[dataRange])
|
||||||
39
SecretKit/Common/SecretStoreList.swift
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
public class SecretStoreList: ObservableObject {
|
||||||
|
|
||||||
|
@Published public var stores: [AnySecretStore] = []
|
||||||
|
@Published public var modifiableStore: AnySecretStoreModifiable?
|
||||||
|
fileprivate var sinks: [AnyCancellable] = []
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public func add<SecretStoreType: SecretStore>(store: SecretStoreType) {
|
||||||
|
addInternal(store: AnySecretStore(store))
|
||||||
|
}
|
||||||
|
|
||||||
|
public func add<SecretStoreType: SecretStoreModifiable>(store: SecretStoreType) {
|
||||||
|
let modifiable = AnySecretStoreModifiable(modifiable: store)
|
||||||
|
modifiableStore = modifiable
|
||||||
|
addInternal(store: modifiable)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var anyAvailable: Bool {
|
||||||
|
stores.reduce(false, { $0 || $1.isAvailable })
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SecretStoreList {
|
||||||
|
|
||||||
|
fileprivate func addInternal(store: AnySecretStore) {
|
||||||
|
stores.append(store)
|
||||||
|
let sink = store.objectWillChange.sink {
|
||||||
|
self.objectWillChange.send()
|
||||||
|
}
|
||||||
|
sinks.append(sink)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
21
SecretKit/Common/Types/Secret.swift
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
public protocol Secret: Identifiable, Hashable {
|
||||||
|
|
||||||
|
var name: String { get }
|
||||||
|
var algorithm: Algorithm { get }
|
||||||
|
var keySize: Int { get }
|
||||||
|
var publicKey: Data { get }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Algorithm: Hashable {
|
||||||
|
case ellipticCurve
|
||||||
|
public init(secAttr: NSNumber) {
|
||||||
|
let secAttrString = secAttr.stringValue as CFString
|
||||||
|
switch secAttrString {
|
||||||
|
case kSecAttrKeyTypeEC:
|
||||||
|
self = .ellipticCurve
|
||||||
|
default:
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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 {}
|
||||||
16
SecretKit/SecureEnclave/SecureEnclaveSecret.swift
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
extension SecureEnclave {
|
||||||
|
|
||||||
|
public struct Secret: SecretKit.Secret {
|
||||||
|
|
||||||
|
public let id: Data
|
||||||
|
public let name: String
|
||||||
|
public let algorithm = Algorithm.ellipticCurve
|
||||||
|
public let keySize = 256
|
||||||
|
public let publicKey: 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,15 +1,15 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SecretKit
|
import Combine
|
||||||
|
|
||||||
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
|
||||||
public let name: String
|
public let name: String
|
||||||
|
public let algorithm: Algorithm
|
||||||
|
public let keySize: Int
|
||||||
public let publicKey: Data
|
public let publicKey: Data
|
||||||
public var attributes: Attributes
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
160
SecretKit/SmartCard/SmartCardStore.swift
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
import Foundation
|
||||||
|
import Security
|
||||||
|
import CryptoTokenKit
|
||||||
|
|
||||||
|
// TODO: Might need to split this up into "sub-stores?"
|
||||||
|
// ie, each token has its own Store.
|
||||||
|
extension SmartCard {
|
||||||
|
|
||||||
|
public class Store: SecretStore {
|
||||||
|
|
||||||
|
// TODO: Read actual smart card name, eg "YubiKey 5c"
|
||||||
|
@Published public var isAvailable: Bool = false
|
||||||
|
public let id = UUID()
|
||||||
|
public fileprivate(set) var name = NSLocalizedString("Smart Card", comment: "Smart Card")
|
||||||
|
@Published public fileprivate(set) var secrets: [Secret] = []
|
||||||
|
fileprivate let watcher = TKTokenWatcher()
|
||||||
|
fileprivate var tokenID: String?
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
tokenID = watcher.nonSecureEnclaveTokens.first
|
||||||
|
watcher.setInsertionHandler { string in
|
||||||
|
guard self.tokenID == nil else { return }
|
||||||
|
guard !string.contains("setoken") else { return }
|
||||||
|
|
||||||
|
self.tokenID = string
|
||||||
|
self.reloadSecrets()
|
||||||
|
self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: string)
|
||||||
|
}
|
||||||
|
if let tokenID = tokenID {
|
||||||
|
self.isAvailable = true
|
||||||
|
self.watcher.addRemovalHandler(self.smartcardRemoved, forTokenID: tokenID)
|
||||||
|
}
|
||||||
|
loadSecrets()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Public API
|
||||||
|
|
||||||
|
public func create(name: String) throws {
|
||||||
|
fatalError("Keys must be created on the smart card.")
|
||||||
|
}
|
||||||
|
|
||||||
|
public func delete(secret: Secret) throws {
|
||||||
|
fatalError("Keys must be deleted on the smart card.")
|
||||||
|
}
|
||||||
|
|
||||||
|
public func sign(data: Data, with secret: SecretType) throws -> Data {
|
||||||
|
guard let tokenID = tokenID else { fatalError() }
|
||||||
|
let attributes = [
|
||||||
|
kSecClass: kSecClassKey,
|
||||||
|
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
|
||||||
|
kSecAttrApplicationLabel: secret.id as CFData,
|
||||||
|
kSecAttrTokenID: tokenID,
|
||||||
|
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?
|
||||||
|
let signatureAlgorithm: SecKeyAlgorithm
|
||||||
|
switch (secret.algorithm, secret.keySize) {
|
||||||
|
case (.ellipticCurve, 256):
|
||||||
|
signatureAlgorithm = .ecdsaSignatureMessageX962SHA256
|
||||||
|
case (.ellipticCurve, 384):
|
||||||
|
signatureAlgorithm = .ecdsaSignatureMessageX962SHA384
|
||||||
|
default:
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
guard let signature = SecKeyCreateSignature(key, signatureAlgorithm, data as CFData, &signError) else {
|
||||||
|
throw SigningError(error: signError)
|
||||||
|
}
|
||||||
|
return signature as Data
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SmartCard.Store {
|
||||||
|
|
||||||
|
fileprivate func smartcardRemoved(for tokenID: String? = nil) {
|
||||||
|
self.tokenID = nil
|
||||||
|
reloadSecrets()
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func reloadSecrets() {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.isAvailable = self.tokenID != nil
|
||||||
|
self.secrets.removeAll()
|
||||||
|
self.loadSecrets()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func loadSecrets() {
|
||||||
|
guard let tokenID = tokenID else { return }
|
||||||
|
// Hack to read name if there's only one smart card
|
||||||
|
let slotNames = TKSmartCardSlotManager().slotNames
|
||||||
|
if watcher.nonSecureEnclaveTokens.count == 1 && slotNames.count == 1 {
|
||||||
|
name = slotNames.first!
|
||||||
|
} else {
|
||||||
|
name = NSLocalizedString("Smart Card", comment: "Smart Card")
|
||||||
|
}
|
||||||
|
|
||||||
|
let attributes = [
|
||||||
|
kSecClass: kSecClassKey,
|
||||||
|
kSecAttrTokenID: tokenID,
|
||||||
|
kSecAttrKeyType: kSecAttrKeyTypeEC, // Restrict to EC
|
||||||
|
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: [SmartCard.Secret] = typed.map {
|
||||||
|
let name = $0[kSecAttrLabel] as? String ?? "Unnamed"
|
||||||
|
let tokenID = $0[kSecAttrApplicationLabel] as! Data
|
||||||
|
let algorithm = Algorithm(secAttr: $0[kSecAttrKeyType] as! NSNumber)
|
||||||
|
let keySize = $0[kSecAttrKeySizeInBits] as! Int
|
||||||
|
let publicKeyRef = $0[kSecValueRef] as! SecKey
|
||||||
|
let publicKeySecRef = SecKeyCopyPublicKey(publicKeyRef)!
|
||||||
|
let publicKeyAttributes = SecKeyCopyAttributes(publicKeySecRef) as! [CFString: Any]
|
||||||
|
let publicKey = publicKeyAttributes[kSecValueData] as! Data
|
||||||
|
return SmartCard.Secret(id: tokenID, name: name, algorithm: algorithm, keySize: keySize, publicKey: publicKey)
|
||||||
|
}
|
||||||
|
secrets.append(contentsOf: wrapped)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TKTokenWatcher {
|
||||||
|
|
||||||
|
fileprivate var nonSecureEnclaveTokens: [String] {
|
||||||
|
tokenIDs.filter { !$0.contains("setoken") }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SmartCard {
|
||||||
|
|
||||||
|
public struct KeychainError: Error {
|
||||||
|
public let statusCode: OSStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct SigningError: Error {
|
||||||
|
public let error: SecurityError?
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SmartCard {
|
||||||
|
|
||||||
|
public typealias SecurityError = Unmanaged<CFError>
|
||||||
|
|
||||||
|
}
|
||||||
17
SecretKitTests/AnySecretTests.swift
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import Foundation
|
||||||
|
import XCTest
|
||||||
|
@testable import SecretKit
|
||||||
|
|
||||||
|
class AnySecretTests: XCTestCase {
|
||||||
|
|
||||||
|
func testEraser() {
|
||||||
|
let secret = SmartCard.Secret(id: UUID().uuidString.data(using: .utf8)!, name: "Name", algorithm: .ellipticCurve, keySize: 256, publicKey: UUID().uuidString.data(using: .utf8)!)
|
||||||
|
let erased = AnySecret(secret)
|
||||||
|
XCTAssert(erased.id == secret.id as AnyHashable)
|
||||||
|
XCTAssert(erased.name == secret.name)
|
||||||
|
XCTAssert(erased.algorithm == secret.algorithm)
|
||||||
|
XCTAssert(erased.keySize == secret.keySize)
|
||||||
|
XCTAssert(erased.publicKey == secret.publicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
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,19 +1,17 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Testing
|
import XCTest
|
||||||
@testable import SecretKit
|
@testable import SecretKit
|
||||||
@testable import SecureEnclaveSecretKit
|
|
||||||
@testable import SmartCardSecretKit
|
|
||||||
|
|
||||||
@Suite struct OpenSSHReaderTests {
|
class OpenSSHReaderTests: XCTestCase {
|
||||||
|
|
||||||
@Test func signatureRequest() {
|
func testSignatureRequest() {
|
||||||
let reader = OpenSSHReader(data: Constants.signatureRequest)
|
let reader = OpenSSHReader(data: Constants.signatureRequest)
|
||||||
let hash = reader.readNextChunk()
|
let hash = reader.readNextChunk()
|
||||||
#expect(hash == Data(base64Encoded: "AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBEqCbkJbOHy5S1wVCaJoKPmpS0egM4frMqllgnlRRQ/Uvnn6EVS8oV03cPA2Bz0EdESyRKA/sbmn0aBtgjIwGELxu45UXEW1TEz6TxyS0u3vuIqR3Wo1CrQWRDnkrG/pBQ=="))
|
XCTAssert(hash == Data(base64Encoded: "AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBEqCbkJbOHy5S1wVCaJoKPmpS0egM4frMqllgnlRRQ/Uvnn6EVS8oV03cPA2Bz0EdESyRKA/sbmn0aBtgjIwGELxu45UXEW1TEz6TxyS0u3vuIqR3Wo1CrQWRDnkrG/pBQ=="))
|
||||||
let dataToSign = reader.readNextChunk()
|
let dataToSign = reader.readNextChunk()
|
||||||
#expect(dataToSign == Data(base64Encoded: "AAAAICi5xf1ixOestUlxdjvt/BDcM+rzhwy7Vo8cW5YcxA8+MgAAAANnaXQAAAAOc3NoLWNvbm5lY3Rpb24AAAAJcHVibGlja2V5AQAAABNlY2RzYS1zaGEyLW5pc3RwMzg0AAAAiAAAABNlY2RzYS1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQRKgm5CWzh8uUtcFQmiaCj5qUtHoDOH6zKpZYJ5UUUP1L55+hFUvKFdN3DwNgc9BHREskSgP7G5p9GgbYIyMBhC8buOVFxFtUxM+k8cktLt77iKkd1qNQq0FkQ55Kxv6QU="))
|
XCTAssert(dataToSign == Data(base64Encoded: "AAAAICi5xf1ixOestUlxdjvt/BDcM+rzhwy7Vo8cW5YcxA8+MgAAAANnaXQAAAAOc3NoLWNvbm5lY3Rpb24AAAAJcHVibGlja2V5AQAAABNlY2RzYS1zaGEyLW5pc3RwMzg0AAAAiAAAABNlY2RzYS1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQRKgm5CWzh8uUtcFQmiaCj5qUtHoDOH6zKpZYJ5UUUP1L55+hFUvKFdN3DwNgc9BHREskSgP7G5p9GgbYIyMBhC8buOVFxFtUxM+k8cktLt77iKkd1qNQq0FkQ55Kxv6QU="))
|
||||||
let empty = reader.readNextChunk()
|
let empty = reader.readNextChunk()
|
||||||
#expect(empty.isEmpty)
|
XCTAssert(empty.isEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
45
SecretKitTests/OpenSSHWriterTests.swift
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import Foundation
|
||||||
|
import XCTest
|
||||||
|
@testable import SecretKit
|
||||||
|
|
||||||
|
class OpenSSHWriterTests: XCTestCase {
|
||||||
|
|
||||||
|
let writer = OpenSSHKeyWriter()
|
||||||
|
|
||||||
|
func testECDSA256Fingerprint() {
|
||||||
|
XCTAssertEqual(writer.openSSHFingerprint(secret: Constants.ecdsa256Secret), "dc:60:4d:ff:c2:d9:18:8b:2f:24:40:b5:7f:43:47:e5")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testECDSA256PublicKey() {
|
||||||
|
XCTAssertEqual(writer.openSSHString(secret: Constants.ecdsa256Secret),
|
||||||
|
"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOVEjgAA5PHqRgwykjN5qM21uWCHFSY/Sqo5gkHAkn+e1MMQKHOLga7ucB9b3mif33MBid59GRK9GEPVlMiSQwo=")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testECDSA256Hash() {
|
||||||
|
XCTAssertEqual(writer.data(secret: Constants.ecdsa256Secret), Data(base64Encoded: "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOVEjgAA5PHqRgwykjN5qM21uWCHFSY/Sqo5gkHAkn+e1MMQKHOLga7ucB9b3mif33MBid59GRK9GEPVlMiSQwo="))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testECDSA384Fingerprint() {
|
||||||
|
XCTAssertEqual(writer.openSSHFingerprint(secret: Constants.ecdsa384Secret), "66:e0:66:d7:41:ed:19:8e:e2:20:df:ce:ac:7e:2b:6e")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testECDSA384PublicKey() {
|
||||||
|
XCTAssertEqual(writer.openSSHString(secret: Constants.ecdsa384Secret),
|
||||||
|
"ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBG2MNc/C5OTHFE2tBvbZCVcpOGa8vBMquiTLkH4lwkeqOPxhi+PyYUfQZMTRJNPiTyWPoMBqNiCIFRVv60yPN/AHufHaOgbdTP42EgMlMMImkAjYUEv9DESHTVIs2PW1yQ==")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testECDSA384Hash() {
|
||||||
|
XCTAssertEqual(writer.data(secret: Constants.ecdsa384Secret), Data(base64Encoded: "AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBG2MNc/C5OTHFE2tBvbZCVcpOGa8vBMquiTLkH4lwkeqOPxhi+PyYUfQZMTRJNPiTyWPoMBqNiCIFRVv60yPN/AHufHaOgbdTP42EgMlMMImkAjYUEv9DESHTVIs2PW1yQ=="))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension OpenSSHWriterTests {
|
||||||
|
|
||||||
|
enum Constants {
|
||||||
|
static let ecdsa256Secret = SmartCard.Secret(id: Data(), name: "Test Key (ECDSA 256)", algorithm: .ellipticCurve, keySize: 256, publicKey: Data(base64Encoded: "BOVEjgAA5PHqRgwykjN5qM21uWCHFSY/Sqo5gkHAkn+e1MMQKHOLga7ucB9b3mif33MBid59GRK9GEPVlMiSQwo=")!)
|
||||||
|
static let ecdsa384Secret = SmartCard.Secret(id: Data(), name: "Test Key (ECDSA 384)", algorithm: .ellipticCurve, keySize: 384, publicKey: Data(base64Encoded: "BG2MNc/C5OTHFE2tBvbZCVcpOGa8vBMquiTLkH4lwkeqOPxhi+PyYUfQZMTRJNPiTyWPoMBqNiCIFRVv60yPN/AHufHaOgbdTP42EgMlMMImkAjYUEv9DESHTVIs2PW1yQ==")!)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
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 = "2600"
|
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 = "2600"
|
LastUpgradeVersion = "1140"
|
||||||
version = "1.7">
|
version = "1.7">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
@@ -75,7 +75,6 @@
|
|||||||
ignoresPersistentStateOnLaunch = "NO"
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
debugDocumentVersioning = "YES"
|
debugDocumentVersioning = "YES"
|
||||||
debugServiceExtension = "internal"
|
debugServiceExtension = "internal"
|
||||||
enableGPUValidationMode = "1"
|
|
||||||
allowLocationSimulation = "YES">
|
allowLocationSimulation = "YES">
|
||||||
<BuildableProductRunnable
|
<BuildableProductRunnable
|
||||||
runnableDebuggingMode = "0">
|
runnableDebuggingMode = "0">
|
||||||
@@ -89,7 +88,7 @@
|
|||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
<ProfileAction
|
<ProfileAction
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Release"
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
savedToolIdentifier = ""
|
savedToolIdentifier = ""
|
||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "NO"
|
||||||
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
60
Secretive/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "16x16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "16x16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "32x32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "32x32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "128x128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "128x128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Icon 2@1x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "256x256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Icon 2@2x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "256x256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "512x512"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import Combine
|
||||||
import AppKit
|
import AppKit
|
||||||
|
|
||||||
protocol JustUpdatedCheckerProtocol: Observable {
|
protocol JustUpdatedCheckerProtocol: ObservableObject {
|
||||||
var justUpdated: Bool { get }
|
var justUpdated: Bool { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Observable class JustUpdatedChecker: JustUpdatedCheckerProtocol {
|
class JustUpdatedChecker: ObservableObject, JustUpdatedCheckerProtocol {
|
||||||
|
|
||||||
var justUpdated: Bool = false
|
@Published var justUpdated: Bool = false
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
check()
|
check()
|
||||||
@@ -17,7 +18,9 @@ protocol JustUpdatedCheckerProtocol: Observable {
|
|||||||
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)
|
||||||
justUpdated = lastBuild != currentBuild
|
if lastBuild != currentBuild {
|
||||||
|
justUpdated = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
Secretive/Preview Content/PreviewAgentStatusChecker.swift
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
class PreviewAgentStatusChecker: AgentStatusCheckerProtocol {
|
||||||
|
|
||||||
|
let running: Bool
|
||||||
|
|
||||||
|
init(running: Bool = true) {
|
||||||
|
self.running = running
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
70
Secretive/Preview Content/PreviewStore.swift
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import Foundation
|
||||||
|
import SecretKit
|
||||||
|
|
||||||
|
enum Preview {}
|
||||||
|
|
||||||
|
extension Preview {
|
||||||
|
|
||||||
|
struct Secret: SecretKit.Secret {
|
||||||
|
|
||||||
|
let id = UUID().uuidString
|
||||||
|
let name: String
|
||||||
|
let algorithm = Algorithm.ellipticCurve
|
||||||
|
let keySize = 256
|
||||||
|
let publicKey = UUID().uuidString.data(using: .utf8)!
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Preview {
|
||||||
|
|
||||||
|
class Store: SecretStore, ObservableObject {
|
||||||
|
|
||||||
|
let isAvailable = true
|
||||||
|
let id = UUID()
|
||||||
|
var name: String { "Preview Store" }
|
||||||
|
@Published var secrets: [Secret] = []
|
||||||
|
|
||||||
|
init(secrets: [Secret]) {
|
||||||
|
self.secrets.append(contentsOf: secrets)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(numberOfRandomSecrets: Int = 5) {
|
||||||
|
let new = (0..<numberOfRandomSecrets).map { Secret(name: String(describing: $0)) }
|
||||||
|
self.secrets.append(contentsOf: new)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sign(data: Data, with secret: Preview.Secret) throws -> Data {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class StoreModifiable: Store, SecretStoreModifiable {
|
||||||
|
|
||||||
|
override var name: String { "Modifiable Preview Store" }
|
||||||
|
|
||||||
|
func create(name: String, requiresAuthentication: Bool) throws {
|
||||||
|
}
|
||||||
|
|
||||||
|
func delete(secret: Preview.Secret) throws {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Preview {
|
||||||
|
|
||||||
|
static func storeList(stores: [Store] = [], modifiableStores: [StoreModifiable] = []) -> SecretStoreList {
|
||||||
|
let list = SecretStoreList()
|
||||||
|
for store in stores {
|
||||||
|
list.add(store: store)
|
||||||
|
}
|
||||||
|
for storeModifiable in modifiableStores {
|
||||||
|
list.add(store: storeModifiable)
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
30
Secretive/Preview Content/PreviewUpdater.swift
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
import Brief
|
||||||
|
|
||||||
|
class PreviewUpdater: UpdaterProtocol {
|
||||||
|
|
||||||
|
let update: Release?
|
||||||
|
|
||||||
|
init(update: Update = .none) {
|
||||||
|
switch update {
|
||||||
|
case .none:
|
||||||
|
self.update = nil
|
||||||
|
case .advisory:
|
||||||
|
self.update = Release(name: "10.10.10", html_url: URL(string: "https://example.com")!, body: "Some regular update")
|
||||||
|
case .critical:
|
||||||
|
self.update = Release(name: "10.10.10", html_url: URL(string: "https://example.com")!, body: "Critical Security Update")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ignore(release: Release) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PreviewUpdater {
|
||||||
|
|
||||||
|
enum Update {
|
||||||
|
case none, advisory, critical
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||