Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
12 changes: 12 additions & 0 deletions docs/notes-3.7.txt
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,18 @@ Python API Improvements:
Password Manager Improvements:
- Bitwarden is now a supported password manager.
- Keeper Security is also supported.
- The Keeper adapter supports Nested Shared
Folders as well as the classic vault.
Both appear in one list, labeled (Classic)
or (Nested). Add, edit, and delete work
for either type.
- Password manager adapters can declare
optional toggles in their handshake that the
Add Account panel renders as checkboxes. The
Keeper adapter uses this to offer a “Use
classic permission model” choice — leaving

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: this line uses an em dash, which the project style avoids in user-facing text. Could swap it for " - " or reword.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — replaced the em dash with a period per project style.

it unchecked adds to Nested Shared Folders;
checking it adds to the classic vault.

Shell Integration Improvements:
- xonsh is now a supported shell.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ public enum PasswordManagerProtocol {
}
}

public struct AddAccountToggle: Codable {
public var key: String
public var label: String
public var note: String?
public var defaultValue: Bool

public init(key: String, label: String, note: String?, defaultValue: Bool) {
self.key = key
self.label = label
self.note = note
self.defaultValue = defaultValue
}
}

// A custom setting for your adapter.
public struct SettingsField: Codable {
public var key: String
Expand Down Expand Up @@ -79,9 +93,10 @@ public enum PasswordManagerProtocol {
public var persistsCredentials: Bool?
public var customCommands: [CustomCommand]?
public var settingsFields: [SettingsField]?
public var addAccountToggles: [AddAccountToggle]?

public init(protocolVersion: Int, name: String, requiresMasterPassword: Bool, canSetPasswords: Bool, userAccounts: [UserAccount]?, needsPathToDatabase: Bool, databaseExtension: String?, needsPathToExecutable: String?,
pathToDatabaseKind: PathKind? = nil, pathToDatabasePrompt: String? = nil, pathToDatabasePlaceholder: String? = nil, masterPasswordLabel: String? = nil, persistsCredentials: Bool? = nil, customCommands: [CustomCommand]? = nil, settingsFields: [SettingsField]? = nil) {
pathToDatabaseKind: PathKind? = nil, pathToDatabasePrompt: String? = nil, pathToDatabasePlaceholder: String? = nil, masterPasswordLabel: String? = nil, persistsCredentials: Bool? = nil, customCommands: [CustomCommand]? = nil, settingsFields: [SettingsField]? = nil, addAccountToggles: [AddAccountToggle]? = nil) {
self.protocolVersion = protocolVersion
self.name = name
self.requiresMasterPassword = requiresMasterPassword
Expand All @@ -97,6 +112,7 @@ public enum PasswordManagerProtocol {
self.persistsCredentials = persistsCredentials
self.customCommands = customCommands
self.settingsFields = settingsFields
self.addAccountToggles = addAccountToggles
}
}

Expand Down Expand Up @@ -232,6 +248,8 @@ public enum PasswordManagerProtocol {
public var userName: String
public var accountName: String
public var password: String?

public var flags: [String: Bool]?
}

public struct AddAccountResponse: Codable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,55 @@ func keeperUserFacingPasswordUpdateError(apiDetail: String) -> String {
}
return apiDetail
}

private let keeperUpdateCredentialsAdvice =
"Update your Service URL and API key in Password Manager settings, and confirm your Keeper Commander server is running."

private func keeperResponseBodyLooksLikeHTML(_ data: Data) -> Bool {
guard let head = String(data: data.prefix(2048), encoding: .utf8)?.lowercased() else { return false }
return head.contains("<!doctype html") || head.contains("<html") || head.contains("<body")
}

func keeperConnectivityErrorMessage(statusCode: Int?, data: Data?) -> String {
if let detail = keeperHumanReadableError(fromResponseData: data) {
return detail
}
if let body = data, !body.isEmpty, keeperResponseBodyLooksLikeHTML(body) {
return "The Service URL returned an HTML error page (the server may be offline, the URL may be wrong, or a proxy/tunnel may be misconfigured). \(keeperUpdateCredentialsAdvice)"
}
if let code = statusCode {
if code == 401 || code == 403 {
return "API key rejected (HTTP \(code)). \(keeperUpdateCredentialsAdvice)"
}
if code >= 500 {
return "Keeper Commander returned an error (HTTP \(code)). \(keeperUpdateCredentialsAdvice)"
}
return "Unexpected response from Keeper Commander (HTTP \(code)). \(keeperUpdateCredentialsAdvice)"
}
return "Could not reach Keeper Commander. \(keeperUpdateCredentialsAdvice)"
}

func keeperConnectivityErrorMessage(urlError: Error?) -> String? {
guard let urlError = urlError as? URLError else { return nil }
switch urlError.code {
case .timedOut:
return "Could not reach the Keeper Commander server (request timed out). \(keeperUpdateCredentialsAdvice)"
case .cannotFindHost, .dnsLookupFailed:
return "The Service URL host could not be resolved. \(keeperUpdateCredentialsAdvice)"
case .cannotConnectToHost:
return "The Service URL is unreachable (connection refused). \(keeperUpdateCredentialsAdvice)"
case .notConnectedToInternet:
return "No internet connection. \(keeperUpdateCredentialsAdvice)"
case .networkConnectionLost, .resourceUnavailable:
return "The connection to Keeper Commander was lost. \(keeperUpdateCredentialsAdvice)"
case .secureConnectionFailed, .serverCertificateUntrusted,
.serverCertificateHasBadDate, .serverCertificateNotYetValid,
.serverCertificateHasUnknownRoot, .clientCertificateRejected,
.clientCertificateRequired:
return "TLS handshake with the Service URL failed. \(keeperUpdateCredentialsAdvice)"
case .badURL, .unsupportedURL:
return "The Service URL is malformed. \(keeperUpdateCredentialsAdvice)"
default:
return "Could not reach Keeper Commander (\(urlError.localizedDescription)). \(keeperUpdateCredentialsAdvice)"
}
}
44 changes: 44 additions & 0 deletions pwmplugin/Sources/iterm2-keeper-adapter/KeeperAdapterLog.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Best-effort file log at ~/Library/Logs/iterm2-keeper-adapter.log (stdout is JSON).

import Foundation

enum KeeperAdapterLog {
static let defaultURL: URL = {
let logsDir = FileManager.default
.urls(for: .libraryDirectory, in: .userDomainMask)
.first?
.appendingPathComponent("Logs")
let dir = logsDir ?? URL(fileURLWithPath: NSTemporaryDirectory())
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
return dir.appendingPathComponent("iterm2-keeper-adapter.log")
}()

static let isEnabled: Bool = {
ProcessInfo.processInfo.environment["ITERM2_KEEPER_ADAPTER_LOG_DISABLED"] == nil

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logger is enabled by default and writes to a persistent file at ~/Library/Logs/iterm2-keeper-adapter.log. Passwords are redacted, but account titles and usernames are written in plaintext. For a password-manager integration it'd be safer to default this off (opt-in via the env var) rather than opt-out.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to opt-in: logging is off by default and enabled with ITERM2_KEEPER_ADAPTER_LOG_ENABLED=1.

}()

private static let formatter: ISO8601DateFormatter = {
let f = ISO8601DateFormatter()
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return f
}()

// Synchronous so short-lived CLI processes do not drop lines on exit.
private static let lock = NSLock()

static func write(_ message: String) {
guard isEnabled else { return }
let stamp = formatter.string(from: Date())
let line = "[\(stamp)] [\(getpid())] \(message)\n"
guard let data = line.data(using: .utf8) else { return }
lock.lock()
defer { lock.unlock() }
if let handle = try? FileHandle(forWritingTo: defaultURL) {
defer { try? handle.close() }
_ = try? handle.seekToEnd()
try? handle.write(contentsOf: data)
} else {
try? data.write(to: defaultURL, options: .atomic)
}
}
}
Loading