This commit is contained in:
Max Goedjen 2020-03-15 00:06:22 -07:00
parent ee23c97b09
commit 979e5b3e3c
No known key found for this signature in database
GPG Key ID: E58C21DD77B9B8E8
8 changed files with 235 additions and 56 deletions

View File

@ -30,6 +30,9 @@
506838A12415EA5600F55094 /* AnySecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506838A02415EA5600F55094 /* AnySecret.swift */; };
506838A32415EA5D00F55094 /* AnySecretStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506838A22415EA5D00F55094 /* AnySecretStore.swift */; };
506AB87E2412334700335D91 /* SecretAgent.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = 50A3B78A24026B7500D209EA /* SecretAgent.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
50731666241DF8660023809E /* Updater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50731665241DF8660023809E /* Updater.swift */; };
50731669241E00C20023809E /* NoticeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50731668241E00C20023809E /* NoticeView.swift */; };
508A58AA241E06B40069DC07 /* PreviewUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508A58A9241E06B40069DC07 /* PreviewUpdater.swift */; };
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */; };
5099A02723FE34FA0062B6F2 /* SmartCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02623FE34FA0062B6F2 /* SmartCard.swift */; };
5099A02923FE35240062B6F2 /* SmartCardStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02823FE35240062B6F2 /* SmartCardStore.swift */; };
@ -179,6 +182,9 @@
5068389D241471CD00F55094 /* SecretStoreList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretStoreList.swift; sourceTree = "<group>"; };
506838A02415EA5600F55094 /* AnySecret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnySecret.swift; sourceTree = "<group>"; };
506838A22415EA5D00F55094 /* AnySecretStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnySecretStore.swift; sourceTree = "<group>"; };
50731665241DF8660023809E /* Updater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Updater.swift; sourceTree = "<group>"; };
50731668241E00C20023809E /* NoticeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeView.swift; sourceTree = "<group>"; };
508A58A9241E06B40069DC07 /* PreviewUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewUpdater.swift; sourceTree = "<group>"; };
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSecretView.swift; sourceTree = "<group>"; };
5099A02623FE34FA0062B6F2 /* SmartCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartCard.swift; sourceTree = "<group>"; };
5099A02823FE35240062B6F2 /* SmartCardStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartCardStore.swift; sourceTree = "<group>"; };
@ -298,11 +304,13 @@
children = (
50617D8223FCE48E0099B055 /* AppDelegate.swift */,
50617D8423FCE48E0099B055 /* ContentView.swift */,
50731668241E00C20023809E /* NoticeView.swift */,
50C385A42407A76D00AF2719 /* SecretDetailView.swift */,
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */,
50B8550C24138C4F009958AC /* DeleteSecretView.swift */,
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */,
50C385A8240B636500AF2719 /* SetupView.swift */,
50731665241DF8660023809E /* Updater.swift */,
50617D8623FCE48E0099B055 /* Assets.xcassets */,
50617D8B23FCE48E0099B055 /* Main.storyboard */,
50617D8E23FCE48E0099B055 /* Info.plist */,
@ -317,6 +325,7 @@
children = (
50617D8923FCE48E0099B055 /* Preview Assets.xcassets */,
50617DD123FCEFA90099B055 /* PreviewStore.swift */,
508A58A9241E06B40069DC07 /* PreviewUpdater.swift */,
);
path = "Preview Content";
sourceTree = "<group>";
@ -735,9 +744,12 @@
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */,
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */,
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */,
50731666241DF8660023809E /* Updater.swift in Sources */,
50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */,
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */,
50731669241E00C20023809E /* NoticeView.swift in Sources */,
50617D8323FCE48E0099B055 /* AppDelegate.swift in Sources */,
508A58AA241E06B40069DC07 /* PreviewUpdater.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1001,6 +1013,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 0.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1027,6 +1040,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 0.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.maxgoedjen.Secretive.Host;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "Secretive - Host";

View File

@ -13,10 +13,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
list.add(store: SmartCard.Store())
return list
}()
let updater = Updater()
func applicationDidFinishLaunching(_ aNotification: Notification) {
let contentView = ContentView(storeList: storeList)
let contentView = ContentView(storeList: storeList, updater: updater)
// Create the window and set the content view.
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),

View File

@ -4,35 +4,41 @@ import SecretKit
struct ContentView: View {
@ObservedObject var storeList: SecretStoreList
@ObservedObject var updater: Updater
@State fileprivate var active: AnySecret.ID?
@State fileprivate var showingDeletion = false
@State fileprivate var deletingSecret: AnySecret?
var body: some View {
NavigationView {
List(selection: $active) {
ForEach(storeList.stores) { store in
if store.isAvailable {
Section(header: Text(store.name)) {
if store.secrets.isEmpty {
if store is AnySecretStoreModifiable {
NavigationLink(destination: EmptyStoreModifiableView(), tag: Constants.emptyStoreModifiableTag, selection: self.$active) {
Text("No Secrets")
VStack {
if updater.update != nil {
updateNotice()
}
NavigationView {
List(selection: $active) {
ForEach(storeList.stores) { store in
if store.isAvailable {
Section(header: Text(store.name)) {
if store.secrets.isEmpty {
if store is AnySecretStoreModifiable {
NavigationLink(destination: EmptyStoreModifiableView(), tag: Constants.emptyStoreModifiableTag, selection: self.$active) {
Text("No Secrets")
}
} else {
NavigationLink(destination: EmptyStoreView(), tag: Constants.emptyStoreTag, selection: self.$active) {
Text("No Secrets")
}
}
} else {
NavigationLink(destination: EmptyStoreView(), tag: Constants.emptyStoreTag, selection: self.$active) {
Text("No Secrets")
}
}
} else {
ForEach(store.secrets) { secret in
NavigationLink(destination: SecretDetailView(secret: secret), tag: secret.id, selection: self.$active) {
Text(secret.name)
}.contextMenu {
if store is AnySecretStoreModifiable {
Button(action: { self.delete(secret: secret) }) {
Text("Delete")
ForEach(store.secrets) { secret in
NavigationLink(destination: SecretDetailView(secret: secret), tag: secret.id, selection: self.$active) {
Text(secret.name)
}.contextMenu {
if store is AnySecretStoreModifiable {
Button(action: { self.delete(secret: secret) }) {
Text("Delete")
}
}
}
}
@ -40,31 +46,45 @@ struct ContentView: View {
}
}
}
}.onAppear {
let fallback: AnyHashable
if self.storeList.modifiableStore?.isAvailable ?? false {
fallback = Constants.emptyStoreModifiableTag
} else {
fallback = Constants.emptyStoreTag
}
self.active = self.storeList.stores.compactMap { $0.secrets.first }.first?.id ?? fallback
}
}.onAppear {
let fallback: AnyHashable
if self.storeList.modifiableStore?.isAvailable ?? false {
fallback = Constants.emptyStoreModifiableTag
} else {
fallback = Constants.emptyStoreTag
}
self.active = self.storeList.stores.compactMap { $0.secrets.first }.first?.id ?? fallback
.listStyle(SidebarListStyle())
.frame(minWidth: 100, idealWidth: 240)
}
.listStyle(SidebarListStyle())
.frame(minWidth: 100, idealWidth: 240)
}
.navigationViewStyle(DoubleColumnNavigationViewStyle())
.sheet(isPresented: $showingDeletion) {
if self.storeList.modifiableStore != nil {
DeleteSecretView(secret: self.deletingSecret!, store: self.storeList.modifiableStore!) {
self.showingDeletion = false
.navigationViewStyle(DoubleColumnNavigationViewStyle())
.sheet(isPresented: $showingDeletion) {
if self.storeList.modifiableStore != nil {
DeleteSecretView(secret: self.deletingSecret!, store: self.storeList.modifiableStore!) {
self.showingDeletion = false
}
}
}
}
}
func updateNotice() -> some View {
guard let update = updater.update else { return AnyView(Spacer()) }
let severity: NoticeView.Severity
let text: String
if update.critical {
severity = .critical
text = "Critical Security Update Required"
} else {
severity = .advisory
text = "Update Available"
}
return AnyView(NoticeView(text: text, severity: severity, actionTitle: "Update") {
NSWorkspace.shared.open(update.html_url)
})
}
func delete<SecretType: Secret>(secret: SecretType) {
deletingSecret = AnySecret(secret)
self.showingDeletion = true
@ -81,17 +101,17 @@ extension ContentView {
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]))
ContentView(storeList: Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()]))
ContentView(storeList: Preview.storeList(stores: [Preview.Store()]))
ContentView(storeList: Preview.storeList(modifiableStores: [Preview.StoreModifiable()]))
}
}
}
#endif
//#if DEBUG
//
//struct ContentView_Previews: PreviewProvider {
// static var previews: some View {
// Group {
// ContentView(storeList: Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]), updater: PreviewUpdater())
// ContentView(storeList: Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()]), updater: PreviewUpdater())
// ContentView(storeList: Preview.storeList(stores: [Preview.Store()]), updater: PreviewUpdater())
// ContentView(storeList: Preview.storeList(modifiableStores: [Preview.StoreModifiable()]), updater: PreviewUpdater())
// }
// }
//}
//
//#endif

View File

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>

View File

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

View File

@ -0,0 +1,6 @@
import Foundation
import Combine
class PreviewUpdater: ObservableObject, UpdaterProtocol {
var update: Release? = nil
}

View File

@ -6,6 +6,8 @@
<true/>
<key>com.apple.security.smartcard</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.maxgoedjen.Secretive</string>

79
Secretive/Updater.swift Normal file
View File

@ -0,0 +1,79 @@
import Foundation
import Combine
protocol UpdaterProtocol: ObservableObject {
var update: Release? { get }
}
class Updater: ObservableObject, UpdaterProtocol {
@Published var update: Release?
init() {
DispatchQueue.global().asyncAfter(deadline: .now() + 3) {
self.checkForUpdates()
}
}
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()
}
func evaluate(release: Release) {
let latestVersion = semVer(from: release.name)
let currentVersion = semVer(from: Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String)
for (latest, current) in zip(latestVersion, currentVersion) {
if latest > current {
DispatchQueue.main.async {
self.update = release
}
return
}
}
}
func semVer(from stringVersion: String) -> [Int] {
var split = stringVersion.split(separator: ".").compactMap { Int($0) }
while split.count < 3 {
split.append(0)
}
return split
}
}
extension Updater {
enum Constants {
static let updateURL = URL(string: "https://api.github.com/repos/rails/rails/releases/latest")!
}
}
struct Release: Codable {
let name: String
let html_url: URL
fileprivate let body: String
}
extension Release {
var critical: Bool {
return body.contains(Constants.securityContent)
}
}
extension Release {
enum Constants {
static let securityContent = "Critical Security Update"
}
}