Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LSUIElement</key>
<true/>
<key>NSFaceIDUsageDescription</key>
<string>nbox uses Touch ID to protect your secrets.</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
</dict>
</plist>
7 changes: 0 additions & 7 deletions NBox.xcodeproj/project.xcworkspace/contents.xcworkspacedata

This file was deleted.

This file was deleted.

4 changes: 2 additions & 2 deletions NBox/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,15 @@ class AppState {
}

func revealSecret(account: String) -> Result<String, KeychainError> {
KeychainManager().get(account: account)
KeychainManager().get(account: account, reason: "Reveal value for \"\(account)\"")
}

private var pendingDeletes: [String: DispatchWorkItem] = [:]

/// Encrypts secret to a temp file, copies a `source <(openssl ...)` command to clipboard.
/// File auto-deletes after 60s. Clipboard auto-clears after 30s.
func shareSecret(account: String) -> Result<String, KeychainError> {
switch KeychainManager().get(account: account) {
switch KeychainManager().get(account: account, reason: "Share \"\(account)\" (encrypted to temp file)") {
case .success(let value):
// Derive env var name from the last path component
let envVar = account.split(separator: "/").last.map(String.init) ?? account
Expand Down
10 changes: 10 additions & 0 deletions NBox/Assets.xcassets/AppIcon.appiconset/Contents.json
Original file line number Diff line number Diff line change
@@ -1,51 +1,61 @@
{
"images" : [
{
"filename" : "icon_16x16.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "icon_16x16@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "icon_32x32.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "icon_32x32@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "icon_128x128.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "icon_128x128@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "icon_256x256.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "icon_256x256@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "icon_512x512.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "icon_512x512@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 0 additions & 24 deletions NBox/ContentView.swift

This file was deleted.

55 changes: 55 additions & 0 deletions NBox/HotkeyManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Carbon
import AppKit

/// Registers a global hotkey (Cmd+Shift+K) using the Carbon API.
/// Works in sandboxed apps without Accessibility permissions.
final class HotkeyManager {
private var hotkeyRef: EventHotKeyRef?
private var eventHandler: EventHandlerRef?
var onTrigger: (() -> Void)?

func register() {
let hotkeyID = EventHotKeyID(signature: OSType(0x4E424F58), // "NBOX"
id: 1)

// Cmd+Shift+K — kVK_ANSI_K = 0x28
let modifiers: UInt32 = UInt32(cmdKey | shiftKey)
let status = RegisterEventHotKey(UInt32(kVK_ANSI_K), modifiers, hotkeyID,
GetApplicationEventTarget(), 0, &hotkeyRef)
guard status == noErr else {
print("nbox: failed to register hotkey: \(status)")
return
}

// Install Carbon event handler for hotkey events
var eventType = EventTypeSpec(eventClass: OSType(kEventClassKeyboard),
eventKind: UInt32(kEventHotKeyPressed))

let handlerBlock: EventHandlerUPP = { _, event, userData -> OSStatus in
guard let userData else { return OSStatus(eventNotHandledErr) }
let manager = Unmanaged<HotkeyManager>.fromOpaque(userData).takeUnretainedValue()
DispatchQueue.main.async {
manager.onTrigger?()
}
return noErr
}

let selfPtr = Unmanaged.passUnretained(self).toOpaque()
InstallEventHandler(GetApplicationEventTarget(), handlerBlock, 1, &eventType, selfPtr, &eventHandler)
}

func unregister() {
if let hotkeyRef {
UnregisterEventHotKey(hotkeyRef)
self.hotkeyRef = nil
}
if let eventHandler {
RemoveEventHandler(eventHandler)
self.eventHandler = nil
}
}

deinit {
unregister()
}
}
13 changes: 8 additions & 5 deletions NBox/KeychainManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,22 +116,25 @@ struct KeychainManager {
return .success(())
}

func get(account: String) -> Result<String, KeychainError> {
func get(account: String, reason: String? = nil) -> Result<String, KeychainError> {
let context = LAContext()
context.localizedReason = "Read value for \"\(account)\""
return get(account: account, context: context)
let prompt = reason ?? "Read value for \"\(account)\""
return get(account: account, context: context, reason: prompt)
}

/// Read a secret using a pre-authenticated LAContext (avoids repeated Touch ID).
func get(account: String, context: LAContext) -> Result<String, KeychainError> {
let query: [String: Any] = [
func get(account: String, context: LAContext, reason: String? = nil) -> Result<String, KeychainError> {
var query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecReturnData as String: true,
kSecUseAuthenticationContext as String: context,
kSecUseDataProtectionKeychain as String: true,
]
if let reason {
query[kSecUseOperationPrompt as String] = reason
}

var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
Expand Down
6 changes: 5 additions & 1 deletion NBox/NBox.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<false/>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)dev.nobox.nbox</string>
Expand Down
8 changes: 8 additions & 0 deletions NBox/NBoxApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,18 @@ struct NBoxApp: App {

class AppDelegate: NSObject, NSApplicationDelegate {
private var socketServer: SocketServer?
private let hotkeyManager = HotkeyManager()
private lazy var quickAccessPanel = QuickAccessPanel(appState: AppState.shared)

func applicationDidFinishLaunching(_ notification: Notification) {
socketServer = SocketServer(keychain: KeychainManager())
socketServer?.start()
installCLI()

hotkeyManager.onTrigger = { [weak self] in
self?.quickAccessPanel.toggle()
}
hotkeyManager.register()
}

/// Copy the bundled nbox-cli to ~/.local/bin/nbox on every launch,
Expand All @@ -46,6 +53,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}

func applicationWillTerminate(_ notification: Notification) {
hotkeyManager.unregister()
socketServer?.stop()
AppState.shared.cleanupSessionDir()
}
Expand Down
Loading