From 9b3415989c8f6ab98f48123ff88d84bf101587ea Mon Sep 17 00:00:00 2001 From: saehejkang Date: Thu, 29 Jan 2026 22:20:50 -0800 Subject: [PATCH 1/7] add functions for container registry list command --- .../Client/KeychainHelper.swift | 6 ++++ .../Keychain/KeychainQuery.swift | 29 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/Sources/ContainerizationOCI/Client/KeychainHelper.swift b/Sources/ContainerizationOCI/Client/KeychainHelper.swift index 63970e92..95f8d357 100644 --- a/Sources/ContainerizationOCI/Client/KeychainHelper.swift +++ b/Sources/ContainerizationOCI/Client/KeychainHelper.swift @@ -46,6 +46,12 @@ public struct KeychainHelper: Sendable { } } } + + /// List all registry domains this id has credentials for. + public func listDomains() throws -> [String] { + let kq = KeychainQuery() + return try kq.listHosts(id: self.id) + } /// Delete authorization data for a given domain from the keychain. public func delete(domain: String) throws { diff --git a/Sources/ContainerizationOS/Keychain/KeychainQuery.swift b/Sources/ContainerizationOS/Keychain/KeychainQuery.swift index 0cdf6b15..93070ec4 100644 --- a/Sources/ContainerizationOS/Keychain/KeychainQuery.swift +++ b/Sources/ContainerizationOS/Keychain/KeychainQuery.swift @@ -107,6 +107,35 @@ public struct KeychainQuery { createdDate: createdDate ) } + + /// List all hostnames in the keychain. + public func listHosts(id: String) throws -> [String] { + let query: [String: Any] = [ + kSecClass as String: kSecClassInternetPassword, + kSecAttrSecurityDomain as String: id, + kSecReturnAttributes as String: true, + kSecMatchLimit as String: kSecMatchLimitAll, + kSecReturnData as String: false, + ] + + var result: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &result) + + guard status != errSecItemNotFound else { + return [] + } + guard status == errSecSuccess else { + throw Error.unhandledError(status: status) + } + + guard let items = result as? [[String: Any]] else { + throw Error.unexpectedDataFetched + } + + return items.compactMap { + $0[kSecAttrServer as String] as? String + } + } private func isQuerySuccessful(_ status: Int32) throws -> Bool { guard status != errSecItemNotFound else { From e46086eab2dd8ad2ddfbad79648df1084929ce07 Mon Sep 17 00:00:00 2001 From: saehejkang Date: Sun, 1 Feb 2026 20:25:39 -0800 Subject: [PATCH 2/7] updates to list function + add test --- .../Client/KeychainHelper.swift | 8 +-- .../Keychain/KeychainQuery.swift | 56 +++++++++++++------ .../KeychainQueryTests.swift | 32 ++++++++++- 3 files changed, 73 insertions(+), 23 deletions(-) diff --git a/Sources/ContainerizationOCI/Client/KeychainHelper.swift b/Sources/ContainerizationOCI/Client/KeychainHelper.swift index 95f8d357..b7155570 100644 --- a/Sources/ContainerizationOCI/Client/KeychainHelper.swift +++ b/Sources/ContainerizationOCI/Client/KeychainHelper.swift @@ -46,11 +46,11 @@ public struct KeychainHelper: Sendable { } } } - - /// List all registry domains this id has credentials for. - public func listDomains() throws -> [String] { + + /// List all registry entries for this domain. + public func list() throws -> [RegistryInfo] { let kq = KeychainQuery() - return try kq.listHosts(id: self.id) + return try kq.list(domain: self.id) } /// Delete authorization data for a given domain from the keychain. diff --git a/Sources/ContainerizationOS/Keychain/KeychainQuery.swift b/Sources/ContainerizationOS/Keychain/KeychainQuery.swift index 93070ec4..7454d846 100644 --- a/Sources/ContainerizationOS/Keychain/KeychainQuery.swift +++ b/Sources/ContainerizationOS/Keychain/KeychainQuery.swift @@ -25,6 +25,14 @@ public struct KeychainQueryResult { public var createdDate: Date } +/// Holds the stored attributes for a registry. +public struct RegistryInfo: Sendable { + public var hostname: String + public var account: String + public let modifiedDate: Date + public let createdDate: Date +} + /// Type that facilitates interacting with the macOS keychain. public struct KeychainQuery { public init() {} @@ -107,33 +115,47 @@ public struct KeychainQuery { createdDate: createdDate ) } - - /// List all hostnames in the keychain. - public func listHosts(id: String) throws -> [String] { + + /// List all registry entries in the keychain for a domain. + public func list(domain: String) throws -> [RegistryInfo] { let query: [String: Any] = [ kSecClass as String: kSecClassInternetPassword, - kSecAttrSecurityDomain as String: id, + kSecAttrSecurityDomain as String: domain, kSecReturnAttributes as String: true, - kSecMatchLimit as String: kSecMatchLimitAll, kSecReturnData as String: false, + kSecMatchLimit as String: kSecMatchLimitAll, ] - - var result: CFTypeRef? - let status = SecItemCopyMatching(query as CFDictionary, &result) - - guard status != errSecItemNotFound else { + var item: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &item) + let exists = try isQuerySuccessful(status) + if !exists { return [] } - guard status == errSecSuccess else { - throw Error.unhandledError(status: status) - } - guard let items = result as? [[String: Any]] else { - throw Error.unexpectedDataFetched + guard let fetched = item as? [[String: Any]] else { + throw Self.Error.unexpectedDataFetched } - return items.compactMap { - $0[kSecAttrServer as String] as? String + return try fetched.map { registry in + guard let hostname = registry[kSecAttrServer as String] as? String else { + throw Self.Error.keyNotPresent(key: kSecAttrServer as String) + } + guard let account = registry[kSecAttrAccount as String] as? String else { + throw Self.Error.keyNotPresent(key: kSecAttrAccount as String) + } + guard let modifiedDate = registry[kSecAttrModificationDate as String] as? Date else { + throw Self.Error.keyNotPresent(key: kSecAttrModificationDate as String) + } + guard let createdDate = registry[kSecAttrCreationDate as String] as? Date else { + throw Self.Error.keyNotPresent(key: kSecAttrCreationDate as String) + } + + return RegistryInfo( + hostname: hostname, + account: account, + modifiedDate: modifiedDate, + createdDate: createdDate + ) } } diff --git a/Tests/ContainerizationOSTests/KeychainQueryTests.swift b/Tests/ContainerizationOSTests/KeychainQueryTests.swift index 3b209f73..faf3cc83 100644 --- a/Tests/ContainerizationOSTests/KeychainQueryTests.swift +++ b/Tests/ContainerizationOSTests/KeychainQueryTests.swift @@ -14,8 +14,6 @@ // limitations under the License. //===----------------------------------------------------------------------===// -// - import Foundation import Testing @@ -45,6 +43,36 @@ struct KeychainQueryTests { } } + @Test(.enabled(if: !isCI)) + func list() throws { + let domain1 = "testing-1-keychain.example.com" + let domain2 = "testing-2-keychain.example.com" + + defer { + try? kq.delete(id: id, host: domain1) + try? kq.delete(id: id, host: domain2) + } + + do { + try kq.save(id: id, host: domain1, user: user, token: "foobar") + try kq.save(id: id, host: domain2, user: user, token: "foobar") + + let entries = try kq.list(domain: id) + + // Verify that both hostnames exist + let hostnames = entries.map { $0.hostname } + #expect(hostnames.contains(domain1)) + #expect(hostnames.contains(domain2)) + + // Verify that the accounts exist + for entry in entries { + #expect(entry.account == user) + } + } catch KeychainQuery.Error.unhandledError(status: -25308) { + // ignore errSecInteractionNotAllowed + } + } + private static var isCI: Bool { ProcessInfo.processInfo.environment["CI"] != nil } From 4e142cab8038c904b50bb462b373627c2b8f44d2 Mon Sep 17 00:00:00 2001 From: saehejkang Date: Mon, 2 Feb 2026 23:26:47 -0500 Subject: [PATCH 3/7] add docc + move registry info --- .../Client/KeychainHelper.swift | 4 ++- .../Keychain/KeychainQuery.swift | 15 ++++------ .../Keychain/RegistryInfo.swift | 29 +++++++++++++++++++ .../KeychainQueryTests.swift | 2 +- 4 files changed, 38 insertions(+), 12 deletions(-) create mode 100644 Sources/ContainerizationOS/Keychain/RegistryInfo.swift diff --git a/Sources/ContainerizationOCI/Client/KeychainHelper.swift b/Sources/ContainerizationOCI/Client/KeychainHelper.swift index b7155570..fbf1a8f8 100644 --- a/Sources/ContainerizationOCI/Client/KeychainHelper.swift +++ b/Sources/ContainerizationOCI/Client/KeychainHelper.swift @@ -47,7 +47,9 @@ public struct KeychainHelper: Sendable { } } - /// List all registry entries for this domain. + /// Lists all registry entries for this domain. + /// - Returns: An array of registry metadata for each matching entry, or an empty array if none are found. + /// - Throws: An error if the keychain query fails. public func list() throws -> [RegistryInfo] { let kq = KeychainQuery() return try kq.list(domain: self.id) diff --git a/Sources/ContainerizationOS/Keychain/KeychainQuery.swift b/Sources/ContainerizationOS/Keychain/KeychainQuery.swift index 7454d846..6b79342b 100644 --- a/Sources/ContainerizationOS/Keychain/KeychainQuery.swift +++ b/Sources/ContainerizationOS/Keychain/KeychainQuery.swift @@ -25,14 +25,6 @@ public struct KeychainQueryResult { public var createdDate: Date } -/// Holds the stored attributes for a registry. -public struct RegistryInfo: Sendable { - public var hostname: String - public var account: String - public let modifiedDate: Date - public let createdDate: Date -} - /// Type that facilitates interacting with the macOS keychain. public struct KeychainQuery { public init() {} @@ -117,6 +109,9 @@ public struct KeychainQuery { } /// List all registry entries in the keychain for a domain. + /// - Parameter domain: The security domain used to fetch registry entries in the keychain. + /// - Returns: An array of registry metadata for each matching entry, or an empty array if none are found. + /// - Throws: An error if the keychain query fails or returns unexpected data. public func list(domain: String) throws -> [RegistryInfo] { let query: [String: Any] = [ kSecClass as String: kSecClassInternetPassword, @@ -140,7 +135,7 @@ public struct KeychainQuery { guard let hostname = registry[kSecAttrServer as String] as? String else { throw Self.Error.keyNotPresent(key: kSecAttrServer as String) } - guard let account = registry[kSecAttrAccount as String] as? String else { + guard let username = registry[kSecAttrAccount as String] as? String else { throw Self.Error.keyNotPresent(key: kSecAttrAccount as String) } guard let modifiedDate = registry[kSecAttrModificationDate as String] as? Date else { @@ -152,7 +147,7 @@ public struct KeychainQuery { return RegistryInfo( hostname: hostname, - account: account, + username: username, modifiedDate: modifiedDate, createdDate: createdDate ) diff --git a/Sources/ContainerizationOS/Keychain/RegistryInfo.swift b/Sources/ContainerizationOS/Keychain/RegistryInfo.swift new file mode 100644 index 00000000..12dd67d9 --- /dev/null +++ b/Sources/ContainerizationOS/Keychain/RegistryInfo.swift @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import Foundation + +/// Holds the stored attributes for a registry. +public struct RegistryInfo: Sendable { + /// The registry host as a domain name with an optional port. + public var hostname: String + /// The username used to authenticate with the registry. + public var username: String + /// The date the registry was last modified. + public let modifiedDate: Date + /// The date the registry was created. + public let createdDate: Date +} diff --git a/Tests/ContainerizationOSTests/KeychainQueryTests.swift b/Tests/ContainerizationOSTests/KeychainQueryTests.swift index faf3cc83..edbe9b73 100644 --- a/Tests/ContainerizationOSTests/KeychainQueryTests.swift +++ b/Tests/ContainerizationOSTests/KeychainQueryTests.swift @@ -66,7 +66,7 @@ struct KeychainQueryTests { // Verify that the accounts exist for entry in entries { - #expect(entry.account == user) + #expect(entry.username == user) } } catch KeychainQuery.Error.unhandledError(status: -25308) { // ignore errSecInteractionNotAllowed From a21a7baf2073f8663163de7c8b8ebdc4e1b3d36b Mon Sep 17 00:00:00 2001 From: saehejkang Date: Mon, 2 Feb 2026 23:31:06 -0500 Subject: [PATCH 4/7] update id to securityDomain --- .../Client/KeychainHelper.swift | 14 ++++++------- .../Keychain/KeychainQuery.swift | 20 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Sources/ContainerizationOCI/Client/KeychainHelper.swift b/Sources/ContainerizationOCI/Client/KeychainHelper.swift index fbf1a8f8..770ae4e8 100644 --- a/Sources/ContainerizationOCI/Client/KeychainHelper.swift +++ b/Sources/ContainerizationOCI/Client/KeychainHelper.swift @@ -20,9 +20,9 @@ import ContainerizationOS /// Helper type to lookup registry related values in the macOS keychain. public struct KeychainHelper: Sendable { - private let id: String - public init(id: String) { - self.id = id + private let securityDomain: String + public init(securityDomain: String) { + self.securityDomain = securityDomain } /// Lookup authorization data for a given registry domain. @@ -30,7 +30,7 @@ public struct KeychainHelper: Sendable { let kq = KeychainQuery() do { - guard let fetched = try kq.get(id: self.id, host: domain) else { + guard let fetched = try kq.get(securityDomain: self.securityDomain, host: domain) else { throw Self.Error.keyNotFound } return BasicAuthentication( @@ -52,19 +52,19 @@ public struct KeychainHelper: Sendable { /// - Throws: An error if the keychain query fails. public func list() throws -> [RegistryInfo] { let kq = KeychainQuery() - return try kq.list(domain: self.id) + return try kq.list(domain: self.securityDomain) } /// Delete authorization data for a given domain from the keychain. public func delete(domain: String) throws { let kq = KeychainQuery() - try kq.delete(id: self.id, host: domain) + try kq.delete(securityDomain: self.securityDomain, host: domain) } /// Save authorization data for a given domain to the keychain. public func save(domain: String, username: String, password: String) throws { let kq = KeychainQuery() - try kq.save(id: self.id, host: domain, user: username, token: password) + try kq.save(securityDomain: self.securityDomain, host: domain, user: username, token: password) } /// Prompt for authorization data for a given domain to be saved to the keychain. diff --git a/Sources/ContainerizationOS/Keychain/KeychainQuery.swift b/Sources/ContainerizationOS/Keychain/KeychainQuery.swift index 6b79342b..ab43bc6f 100644 --- a/Sources/ContainerizationOS/Keychain/KeychainQuery.swift +++ b/Sources/ContainerizationOS/Keychain/KeychainQuery.swift @@ -30,9 +30,9 @@ public struct KeychainQuery { public init() {} /// Save a value to the keychain. - public func save(id: String, host: String, user: String, token: String) throws { - if try exists(id: id, host: host) { - try delete(id: id, host: host) + public func save(securityDomain: String, host: String, user: String, token: String) throws { + if try exists(securityDomain: securityDomain, host: host) { + try delete(securityDomain: securityDomain, host: host) } guard let tokenEncoded = token.data(using: String.Encoding.utf8) else { @@ -40,7 +40,7 @@ public struct KeychainQuery { } let query: [String: Any] = [ kSecClass as String: kSecClassInternetPassword, - kSecAttrSecurityDomain as String: id, + kSecAttrSecurityDomain as String: securityDomain, kSecAttrServer as String: host, kSecAttrAccount as String: user, kSecValueData as String: tokenEncoded, @@ -52,10 +52,10 @@ public struct KeychainQuery { } /// Delete a value from the keychain. - public func delete(id: String, host: String) throws { + public func delete(securityDomain: String, host: String) throws { let query: [String: Any] = [ kSecClass as String: kSecClassInternetPassword, - kSecAttrSecurityDomain as String: id, + kSecAttrSecurityDomain as String: securityDomain, kSecAttrServer as String: host, kSecMatchLimit as String: kSecMatchLimitOne, ] @@ -66,10 +66,10 @@ public struct KeychainQuery { } /// Retrieve a value from the keychain. - public func get(id: String, host: String) throws -> KeychainQueryResult? { + public func get(securityDomain: String, host: String) throws -> KeychainQueryResult? { let query: [String: Any] = [ kSecClass as String: kSecClassInternetPassword, - kSecAttrSecurityDomain as String: id, + kSecAttrSecurityDomain as String: securityDomain, kSecAttrServer as String: host, kSecReturnAttributes as String: true, kSecMatchLimit as String: kSecMatchLimitOne, @@ -165,10 +165,10 @@ public struct KeychainQuery { } /// Check if a value exists in the keychain. - public func exists(id: String, host: String) throws -> Bool { + public func exists(securityDomain: String, host: String) throws -> Bool { let query: [String: Any] = [ kSecClass as String: kSecClassInternetPassword, - kSecAttrSecurityDomain as String: id, + kSecAttrSecurityDomain as String: securityDomain, kSecAttrServer as String: host, kSecReturnAttributes as String: true, kSecMatchLimit as String: kSecMatchLimitOne, From a8e9451b1de4037959f4236ed7f4c8b24e6b0522 Mon Sep 17 00:00:00 2001 From: saehejkang Date: Mon, 2 Feb 2026 23:50:57 -0500 Subject: [PATCH 5/7] updates to host and domain --- .../Client/KeychainHelper.swift | 32 +++++++++---------- .../Keychain/KeychainQuery.swift | 26 +++++++-------- Sources/cctl/ImageCommand.swift | 4 +-- Sources/cctl/LoginCommand.swift | 6 ++-- .../KeychainQueryTests.swift | 30 ++++++++--------- 5 files changed, 49 insertions(+), 49 deletions(-) diff --git a/Sources/ContainerizationOCI/Client/KeychainHelper.swift b/Sources/ContainerizationOCI/Client/KeychainHelper.swift index 770ae4e8..6d161db5 100644 --- a/Sources/ContainerizationOCI/Client/KeychainHelper.swift +++ b/Sources/ContainerizationOCI/Client/KeychainHelper.swift @@ -25,12 +25,12 @@ public struct KeychainHelper: Sendable { self.securityDomain = securityDomain } - /// Lookup authorization data for a given registry domain. - public func lookup(domain: String) throws -> Authentication { + /// Lookup authorization data for a given registry hostname. + public func lookup(hostname: String) throws -> Authentication { let kq = KeychainQuery() do { - guard let fetched = try kq.get(securityDomain: self.securityDomain, host: domain) else { + guard let fetched = try kq.get(securityDomain: self.securityDomain, hostname: hostname) else { throw Self.Error.keyNotFound } return BasicAuthentication( @@ -47,38 +47,38 @@ public struct KeychainHelper: Sendable { } } - /// Lists all registry entries for this domain. + /// Lists all registry entries for this security domain. /// - Returns: An array of registry metadata for each matching entry, or an empty array if none are found. /// - Throws: An error if the keychain query fails. public func list() throws -> [RegistryInfo] { let kq = KeychainQuery() - return try kq.list(domain: self.securityDomain) + return try kq.list(securityDomain: self.securityDomain) } - /// Delete authorization data for a given domain from the keychain. - public func delete(domain: String) throws { + /// Delete authorization data for a given hostname from the keychain. + public func delete(hostname: String) throws { let kq = KeychainQuery() - try kq.delete(securityDomain: self.securityDomain, host: domain) + try kq.delete(securityDomain: self.securityDomain, hostname: hostname) } - /// Save authorization data for a given domain to the keychain. - public func save(domain: String, username: String, password: String) throws { + /// Save authorization data for a given hostname to the keychain. + public func save(hostname: String, username: String, password: String) throws { let kq = KeychainQuery() - try kq.save(securityDomain: self.securityDomain, host: domain, user: username, token: password) + try kq.save(securityDomain: self.securityDomain, hostname: hostname, user: username, token: password) } - /// Prompt for authorization data for a given domain to be saved to the keychain. + /// Prompt for authorization data for a given hostname to be saved to the keychain. /// This will cause the current terminal to enter a password prompt state where /// key strokes are hidden. - public func credentialPrompt(domain: String) throws -> Authentication { - let username = try userPrompt(domain: domain) + public func credentialPrompt(hostname: String) throws -> Authentication { + let username = try userPrompt(hostname: hostname) let password = try passwordPrompt() return BasicAuthentication(username: username, password: password) } /// Prompts the current stdin for a username entry and then returns the value. - public func userPrompt(domain: String) throws -> String { - print("Provide registry username \(domain): ", terminator: "") + public func userPrompt(hostname: String) throws -> String { + print("Provide registry username \(hostname): ", terminator: "") guard let username = readLine() else { throw Self.Error.invalidInput } diff --git a/Sources/ContainerizationOS/Keychain/KeychainQuery.swift b/Sources/ContainerizationOS/Keychain/KeychainQuery.swift index ab43bc6f..564947b4 100644 --- a/Sources/ContainerizationOS/Keychain/KeychainQuery.swift +++ b/Sources/ContainerizationOS/Keychain/KeychainQuery.swift @@ -30,9 +30,9 @@ public struct KeychainQuery { public init() {} /// Save a value to the keychain. - public func save(securityDomain: String, host: String, user: String, token: String) throws { - if try exists(securityDomain: securityDomain, host: host) { - try delete(securityDomain: securityDomain, host: host) + public func save(securityDomain: String, hostname: String, user: String, token: String) throws { + if try exists(securityDomain: securityDomain, hostname: hostname) { + try delete(securityDomain: securityDomain, hostname: hostname) } guard let tokenEncoded = token.data(using: String.Encoding.utf8) else { @@ -41,7 +41,7 @@ public struct KeychainQuery { let query: [String: Any] = [ kSecClass as String: kSecClassInternetPassword, kSecAttrSecurityDomain as String: securityDomain, - kSecAttrServer as String: host, + kSecAttrServer as String: hostname, kSecAttrAccount as String: user, kSecValueData as String: tokenEncoded, kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock, @@ -52,11 +52,11 @@ public struct KeychainQuery { } /// Delete a value from the keychain. - public func delete(securityDomain: String, host: String) throws { + public func delete(securityDomain: String, hostname: String) throws { let query: [String: Any] = [ kSecClass as String: kSecClassInternetPassword, kSecAttrSecurityDomain as String: securityDomain, - kSecAttrServer as String: host, + kSecAttrServer as String: hostname, kSecMatchLimit as String: kSecMatchLimitOne, ] let status = SecItemDelete(query as CFDictionary) @@ -66,11 +66,11 @@ public struct KeychainQuery { } /// Retrieve a value from the keychain. - public func get(securityDomain: String, host: String) throws -> KeychainQueryResult? { + public func get(securityDomain: String, hostname: String) throws -> KeychainQueryResult? { let query: [String: Any] = [ kSecClass as String: kSecClassInternetPassword, kSecAttrSecurityDomain as String: securityDomain, - kSecAttrServer as String: host, + kSecAttrServer as String: hostname, kSecReturnAttributes as String: true, kSecMatchLimit as String: kSecMatchLimitOne, kSecReturnData as String: true, @@ -109,13 +109,13 @@ public struct KeychainQuery { } /// List all registry entries in the keychain for a domain. - /// - Parameter domain: The security domain used to fetch registry entries in the keychain. + /// - Parameter securityDomain: The security domain used to fetch registry entries in the keychain. /// - Returns: An array of registry metadata for each matching entry, or an empty array if none are found. /// - Throws: An error if the keychain query fails or returns unexpected data. - public func list(domain: String) throws -> [RegistryInfo] { + public func list(securityDomain: String) throws -> [RegistryInfo] { let query: [String: Any] = [ kSecClass as String: kSecClassInternetPassword, - kSecAttrSecurityDomain as String: domain, + kSecAttrSecurityDomain as String: securityDomain, kSecReturnAttributes as String: true, kSecReturnData as String: false, kSecMatchLimit as String: kSecMatchLimitAll, @@ -165,11 +165,11 @@ public struct KeychainQuery { } /// Check if a value exists in the keychain. - public func exists(securityDomain: String, host: String) throws -> Bool { + public func exists(securityDomain: String, hostname: String) throws -> Bool { let query: [String: Any] = [ kSecClass as String: kSecClassInternetPassword, kSecAttrSecurityDomain as String: securityDomain, - kSecAttrServer as String: host, + kSecAttrServer as String: hostname, kSecReturnAttributes as String: true, kSecMatchLimit as String: kSecMatchLimitOne, kSecReturnData as String: false, diff --git a/Sources/cctl/ImageCommand.swift b/Sources/cctl/ImageCommand.swift index 4bbb2dfb..42712205 100644 --- a/Sources/cctl/ImageCommand.swift +++ b/Sources/cctl/ImageCommand.swift @@ -275,8 +275,8 @@ extension Application { if let authentication { return try await body(authentication) } - let keychain = KeychainHelper(id: Application.keychainID) - authentication = try? keychain.lookup(domain: host) + let keychain = KeychainHelper(securityDomain: Application.keychainID) + authentication = try? keychain.lookup(hostname: host) return try await body(authentication) } diff --git a/Sources/cctl/LoginCommand.swift b/Sources/cctl/LoginCommand.swift index afe3c7bb..5737d5a1 100644 --- a/Sources/cctl/LoginCommand.swift +++ b/Sources/cctl/LoginCommand.swift @@ -54,9 +54,9 @@ extension Application { } password = String(decoding: passwordData, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines) } - let keychain = KeychainHelper(id: Application.keychainID) + let keychain = KeychainHelper(securityDomain: Application.keychainID) if username == "" { - username = try keychain.userPrompt(domain: server) + username = try keychain.userPrompt(hostname: server) } if password == "" { password = try keychain.passwordPrompt() @@ -79,7 +79,7 @@ extension Application { tlsConfiguration: TLSUtils.makeEnvironmentAwareTLSConfiguration(), ) try await client.ping() - try keychain.save(domain: server, username: username, password: password) + try keychain.save(hostname: server, username: username, password: password) print("Login succeeded") } } diff --git a/Tests/ContainerizationOSTests/KeychainQueryTests.swift b/Tests/ContainerizationOSTests/KeychainQueryTests.swift index edbe9b73..6a9a70ff 100644 --- a/Tests/ContainerizationOSTests/KeychainQueryTests.swift +++ b/Tests/ContainerizationOSTests/KeychainQueryTests.swift @@ -20,21 +20,21 @@ import Testing @testable import ContainerizationOS struct KeychainQueryTests { - let id = "com.example.container-testing-keychain" - let domain = "testing-keychain.example.com" + let securityDomain = "com.example.container-testing-keychain" + let hostname = "testing-keychain.example.com" let user = "containerization-test" let kq = KeychainQuery() @Test(.enabled(if: !isCI)) func keychainQuery() throws { - defer { try? kq.delete(id: id, host: domain) } + defer { try? kq.delete(securityDomain: securityDomain, hostname: hostname) } do { - try kq.save(id: id, host: domain, user: user, token: "foobar") - #expect(try kq.exists(id: id, host: domain)) + try kq.save(securityDomain: securityDomain, hostname: hostname, user: user, token: "foobar") + #expect(try kq.exists(securityDomain: securityDomain, hostname: hostname)) - let fetched = try kq.get(id: id, host: domain) + let fetched = try kq.get(securityDomain: securityDomain, hostname: hostname) let result = try #require(fetched) #expect(result.account == user) #expect(result.data == "foobar") @@ -45,24 +45,24 @@ struct KeychainQueryTests { @Test(.enabled(if: !isCI)) func list() throws { - let domain1 = "testing-1-keychain.example.com" - let domain2 = "testing-2-keychain.example.com" + let hostname1 = "testing-1-keychain.example.com" + let hostname2 = "testing-2-keychain.example.com" defer { - try? kq.delete(id: id, host: domain1) - try? kq.delete(id: id, host: domain2) + try? kq.delete(securityDomain: securityDomain, hostname: hostname1) + try? kq.delete(securityDomain: securityDomain, hostname: hostname2) } do { - try kq.save(id: id, host: domain1, user: user, token: "foobar") - try kq.save(id: id, host: domain2, user: user, token: "foobar") + try kq.save(securityDomain: securityDomain, hostname: hostname1, user: user, token: "foobar") + try kq.save(securityDomain: securityDomain, hostname: hostname2, user: user, token: "foobar") - let entries = try kq.list(domain: id) + let entries = try kq.list(securityDomain: securityDomain) // Verify that both hostnames exist let hostnames = entries.map { $0.hostname } - #expect(hostnames.contains(domain1)) - #expect(hostnames.contains(domain2)) + #expect(hostnames.contains(hostname1)) + #expect(hostnames.contains(hostname2)) // Verify that the accounts exist for entry in entries { From 8685b88cb579cd6b82d7a71a99727c17ac2f96c9 Mon Sep 17 00:00:00 2001 From: saehejkang Date: Mon, 2 Feb 2026 23:56:29 -0500 Subject: [PATCH 6/7] update user + account to username --- .../ContainerizationOCI/Client/KeychainHelper.swift | 4 ++-- .../ContainerizationOS/Keychain/KeychainQuery.swift | 10 +++++----- .../ContainerizationOSTests/KeychainQueryTests.swift | 12 ++++++------ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Sources/ContainerizationOCI/Client/KeychainHelper.swift b/Sources/ContainerizationOCI/Client/KeychainHelper.swift index 6d161db5..def314b7 100644 --- a/Sources/ContainerizationOCI/Client/KeychainHelper.swift +++ b/Sources/ContainerizationOCI/Client/KeychainHelper.swift @@ -34,7 +34,7 @@ public struct KeychainHelper: Sendable { throw Self.Error.keyNotFound } return BasicAuthentication( - username: fetched.account, + username: fetched.username, password: fetched.data ) } catch let err as KeychainQuery.Error { @@ -64,7 +64,7 @@ public struct KeychainHelper: Sendable { /// Save authorization data for a given hostname to the keychain. public func save(hostname: String, username: String, password: String) throws { let kq = KeychainQuery() - try kq.save(securityDomain: self.securityDomain, hostname: hostname, user: username, token: password) + try kq.save(securityDomain: self.securityDomain, hostname: hostname, username: username, token: password) } /// Prompt for authorization data for a given hostname to be saved to the keychain. diff --git a/Sources/ContainerizationOS/Keychain/KeychainQuery.swift b/Sources/ContainerizationOS/Keychain/KeychainQuery.swift index 564947b4..c75d5f8a 100644 --- a/Sources/ContainerizationOS/Keychain/KeychainQuery.swift +++ b/Sources/ContainerizationOS/Keychain/KeychainQuery.swift @@ -19,7 +19,7 @@ import Foundation /// Holds the result of a query to the keychain. public struct KeychainQueryResult { - public var account: String + public var username: String public var data: String public var modifiedDate: Date public var createdDate: Date @@ -30,7 +30,7 @@ public struct KeychainQuery { public init() {} /// Save a value to the keychain. - public func save(securityDomain: String, hostname: String, user: String, token: String) throws { + public func save(securityDomain: String, hostname: String, username: String, token: String) throws { if try exists(securityDomain: securityDomain, hostname: hostname) { try delete(securityDomain: securityDomain, hostname: hostname) } @@ -42,7 +42,7 @@ public struct KeychainQuery { kSecClass as String: kSecClassInternetPassword, kSecAttrSecurityDomain as String: securityDomain, kSecAttrServer as String: hostname, - kSecAttrAccount as String: user, + kSecAttrAccount as String: username, kSecValueData as String: tokenEncoded, kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock, kSecAttrSynchronizable as String: false, @@ -91,7 +91,7 @@ public struct KeychainQuery { guard let decodedData = String(data: data, encoding: String.Encoding.utf8) else { throw Self.Error.unexpectedDataFetched } - guard let account = fetched[kSecAttrAccount as String] as? String else { + guard let username = fetched[kSecAttrAccount as String] as? String else { throw Self.Error.keyNotPresent(key: kSecAttrAccount as String) } guard let modifiedDate = fetched[kSecAttrModificationDate as String] as? Date else { @@ -101,7 +101,7 @@ public struct KeychainQuery { throw Self.Error.keyNotPresent(key: kSecAttrCreationDate as String) } return KeychainQueryResult( - account: account, + username: username, data: decodedData, modifiedDate: modifiedDate, createdDate: createdDate diff --git a/Tests/ContainerizationOSTests/KeychainQueryTests.swift b/Tests/ContainerizationOSTests/KeychainQueryTests.swift index 6a9a70ff..4ef09dd2 100644 --- a/Tests/ContainerizationOSTests/KeychainQueryTests.swift +++ b/Tests/ContainerizationOSTests/KeychainQueryTests.swift @@ -22,7 +22,7 @@ import Testing struct KeychainQueryTests { let securityDomain = "com.example.container-testing-keychain" let hostname = "testing-keychain.example.com" - let user = "containerization-test" + let username = "containerization-test" let kq = KeychainQuery() @@ -31,12 +31,12 @@ struct KeychainQueryTests { defer { try? kq.delete(securityDomain: securityDomain, hostname: hostname) } do { - try kq.save(securityDomain: securityDomain, hostname: hostname, user: user, token: "foobar") + try kq.save(securityDomain: securityDomain, hostname: hostname, username: username, token: "foobar") #expect(try kq.exists(securityDomain: securityDomain, hostname: hostname)) let fetched = try kq.get(securityDomain: securityDomain, hostname: hostname) let result = try #require(fetched) - #expect(result.account == user) + #expect(result.username == username) #expect(result.data == "foobar") } catch KeychainQuery.Error.unhandledError(status: -25308) { // ignore errSecInteractionNotAllowed @@ -54,8 +54,8 @@ struct KeychainQueryTests { } do { - try kq.save(securityDomain: securityDomain, hostname: hostname1, user: user, token: "foobar") - try kq.save(securityDomain: securityDomain, hostname: hostname2, user: user, token: "foobar") + try kq.save(securityDomain: securityDomain, hostname: hostname1, username: username, token: "foobar") + try kq.save(securityDomain: securityDomain, hostname: hostname2, username: username, token: "foobar") let entries = try kq.list(securityDomain: securityDomain) @@ -66,7 +66,7 @@ struct KeychainQueryTests { // Verify that the accounts exist for entry in entries { - #expect(entry.username == user) + #expect(entry.username == username) } } catch KeychainQuery.Error.unhandledError(status: -25308) { // ignore errSecInteractionNotAllowed From 86ce9ad133e3596753b54cbec7f2d2033b275882 Mon Sep 17 00:00:00 2001 From: saehejkang Date: Tue, 3 Feb 2026 00:03:27 -0500 Subject: [PATCH 7/7] update token and data to password --- .../Client/KeychainHelper.swift | 4 ++-- .../Keychain/KeychainQuery.swift | 16 ++++++++-------- .../KeychainQueryTests.swift | 8 ++++---- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Sources/ContainerizationOCI/Client/KeychainHelper.swift b/Sources/ContainerizationOCI/Client/KeychainHelper.swift index def314b7..1d966b9f 100644 --- a/Sources/ContainerizationOCI/Client/KeychainHelper.swift +++ b/Sources/ContainerizationOCI/Client/KeychainHelper.swift @@ -35,7 +35,7 @@ public struct KeychainHelper: Sendable { } return BasicAuthentication( username: fetched.username, - password: fetched.data + password: fetched.password ) } catch let err as KeychainQuery.Error { switch err { @@ -64,7 +64,7 @@ public struct KeychainHelper: Sendable { /// Save authorization data for a given hostname to the keychain. public func save(hostname: String, username: String, password: String) throws { let kq = KeychainQuery() - try kq.save(securityDomain: self.securityDomain, hostname: hostname, username: username, token: password) + try kq.save(securityDomain: self.securityDomain, hostname: hostname, username: username, password: password) } /// Prompt for authorization data for a given hostname to be saved to the keychain. diff --git a/Sources/ContainerizationOS/Keychain/KeychainQuery.swift b/Sources/ContainerizationOS/Keychain/KeychainQuery.swift index c75d5f8a..f873b419 100644 --- a/Sources/ContainerizationOS/Keychain/KeychainQuery.swift +++ b/Sources/ContainerizationOS/Keychain/KeychainQuery.swift @@ -20,7 +20,7 @@ import Foundation /// Holds the result of a query to the keychain. public struct KeychainQueryResult { public var username: String - public var data: String + public var password: String public var modifiedDate: Date public var createdDate: Date } @@ -30,20 +30,20 @@ public struct KeychainQuery { public init() {} /// Save a value to the keychain. - public func save(securityDomain: String, hostname: String, username: String, token: String) throws { + public func save(securityDomain: String, hostname: String, username: String, password: String) throws { if try exists(securityDomain: securityDomain, hostname: hostname) { try delete(securityDomain: securityDomain, hostname: hostname) } - guard let tokenEncoded = token.data(using: String.Encoding.utf8) else { - throw Self.Error.invalidTokenConversion + guard let passwordEncoded = password.data(using: String.Encoding.utf8) else { + throw Self.Error.invalidPasswordConversion } let query: [String: Any] = [ kSecClass as String: kSecClassInternetPassword, kSecAttrSecurityDomain as String: securityDomain, kSecAttrServer as String: hostname, kSecAttrAccount as String: username, - kSecValueData as String: tokenEncoded, + kSecValueData as String: passwordEncoded, kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock, kSecAttrSynchronizable as String: false, ] @@ -88,7 +88,7 @@ public struct KeychainQuery { guard let data = fetched[kSecValueData as String] as? Data else { throw Self.Error.keyNotPresent(key: kSecValueData as String) } - guard let decodedData = String(data: data, encoding: String.Encoding.utf8) else { + guard let password = String(data: data, encoding: String.Encoding.utf8) else { throw Self.Error.unexpectedDataFetched } guard let username = fetched[kSecAttrAccount as String] as? String else { @@ -102,7 +102,7 @@ public struct KeychainQuery { } return KeychainQueryResult( username: username, - data: decodedData, + password: password, modifiedDate: modifiedDate, createdDate: createdDate ) @@ -185,7 +185,7 @@ extension KeychainQuery { case unhandledError(status: Int32) case unexpectedDataFetched case keyNotPresent(key: String) - case invalidTokenConversion + case invalidPasswordConversion } } #endif diff --git a/Tests/ContainerizationOSTests/KeychainQueryTests.swift b/Tests/ContainerizationOSTests/KeychainQueryTests.swift index 4ef09dd2..54da7a52 100644 --- a/Tests/ContainerizationOSTests/KeychainQueryTests.swift +++ b/Tests/ContainerizationOSTests/KeychainQueryTests.swift @@ -31,13 +31,13 @@ struct KeychainQueryTests { defer { try? kq.delete(securityDomain: securityDomain, hostname: hostname) } do { - try kq.save(securityDomain: securityDomain, hostname: hostname, username: username, token: "foobar") + try kq.save(securityDomain: securityDomain, hostname: hostname, username: username, password: "foobar") #expect(try kq.exists(securityDomain: securityDomain, hostname: hostname)) let fetched = try kq.get(securityDomain: securityDomain, hostname: hostname) let result = try #require(fetched) #expect(result.username == username) - #expect(result.data == "foobar") + #expect(result.password == "foobar") } catch KeychainQuery.Error.unhandledError(status: -25308) { // ignore errSecInteractionNotAllowed } @@ -54,8 +54,8 @@ struct KeychainQueryTests { } do { - try kq.save(securityDomain: securityDomain, hostname: hostname1, username: username, token: "foobar") - try kq.save(securityDomain: securityDomain, hostname: hostname2, username: username, token: "foobar") + try kq.save(securityDomain: securityDomain, hostname: hostname1, username: username, password: "foobar") + try kq.save(securityDomain: securityDomain, hostname: hostname2, username: username, password: "foobar") let entries = try kq.list(securityDomain: securityDomain)