Files
ghostgram/submodules/PasscodeCore/Tests/PasscodeCoreTests/PasscodeKeychainScopeTests.swift
T
ichmagmaus111andClaude Opus 5.5 0622057a4f Ghostgram: open-source release
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:57:09 +01:00

270 lines
17 KiB
Swift

#if os(macOS)
import XCTest
import Security
@testable import PasscodeCore
final class PasscodeKeychainScopeTests: XCTestCase {
private var scope = ""
private var account = ""
private var passcodeService = ""
private var walletPrefix = ""
private var biometricService = ""
private var environment: PasscodeEnvironment!
private var created: [(String, String)] = []
override func setUpWithError() throws {
try super.setUpWithError()
let id = UUID().uuidString
self.scope = "tests-\(id)"
self.account = "tests-\(id)"
self.passcodeService = "org.telegram.tests.\(id).passcode"
self.walletPrefix = "org.telegram.tests.\(id).wallet."
self.biometricService = "org.telegram.tests.\(id).biometric"
try XCTSkipUnless(PasscodeKeychainScope.isDefaultKeychainUnlocked(), "The default keychain is locked")
self.environment = try self.makeEnvironment(scope: self.scope)
}
override func tearDown() {
guard self.environment != nil else {
super.tearDown()
return
}
var deletions = [self.query(service: PasscodeKeychainScope.legacyPasscodeService, account: self.account)]
for service in [self.scoped(PasscodeKeychainScope.legacyPasscodeService), self.passcodeService, self.scoped(self.passcodeService)] {
for account in [self.account, "credential", "device", "attempts"] {
deletions.append(self.query(service: service, account: account))
}
}
for (service, account) in self.created {
deletions.append(self.query(service: service, account: account))
deletions.append(self.query(service: self.scoped(service), account: account))
}
for deletion in deletions {
let status = SecItemDelete(deletion as CFDictionary)
XCTAssertTrue(status == errSecSuccess || status == errSecItemNotFound, "\(deletion[kSecAttrService as String] ?? "") deletion: \(status)")
}
super.tearDown()
}
private func makeEnvironment(scope: String?) throws -> PasscodeEnvironment {
let environment = PasscodeEnvironment()
try environment.configure(PasscodeConfiguration(appGroupIdentifier: "6N38VWS5BX.org.telegram.tests", processRole: .mainApp, biometricKeychainService: self.biometricService, keychainScope: scope), privateAccessGroup: { "6N38VWS5BX.org.telegram.tests" })
return environment
}
private func query(service: String, account: String) -> [String: Any] {
return [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecAttrSynchronizable as String: false,
kSecUseDataProtectionKeychain as String: false
]
}
private func add(_ value: String, service: String, account: String) {
var insert = self.query(service: service, account: account)
insert[kSecValueData as String] = Data(value.utf8)
XCTAssertEqual(SecItemAdd(insert as CFDictionary, nil), errSecSuccess)
self.created.append((service, account))
}
private func addNeedingConfirmation(_ value: String, service: String, account: String) throws {
var access: SecAccess?
XCTAssertEqual(SecAccessCreate("tests" as CFString, [] as CFArray, &access), errSecSuccess)
var insert = self.query(service: service, account: account)
insert[kSecValueData as String] = Data(value.utf8)
insert[kSecAttrAccess as String] = try XCTUnwrap(access)
XCTAssertEqual(SecItemAdd(insert as CFDictionary, nil), errSecSuccess)
self.created.append((service, account))
}
private func exists(service: String, account: String) -> Bool {
var search = self.query(service: service, account: account)
search[kSecReturnAttributes as String] = true
search[kSecMatchLimit as String] = kSecMatchLimitOne
return SecItemCopyMatching(search as CFDictionary, nil) == errSecSuccess
}
private func value(service: String, account: String) -> String? {
var search = self.query(service: service, account: account)
search[kSecReturnData as String] = true
search[kSecMatchLimit as String] = kSecMatchLimitOne
var result: CFTypeRef?
guard SecItemCopyMatching(search as CFDictionary, &result) == errSecSuccess, let data = result as? Data else {
return nil
}
return String(data: data, encoding: .utf8)
}
private func scoped(_ service: String) -> String {
return "org.telegram.scoped.\(self.scope)/" + service
}
private func migrate(environment: PasscodeEnvironment? = nil) -> PasscodeKeychainScope.Migration {
return PasscodeKeychainScope.migrateLegacyItems(passcodeService: self.passcodeService, walletServicePrefixes: [self.walletPrefix + "descriptor.", self.walletPrefix + "vault."], environment: environment ?? self.environment)
}
func testServicesCarryTheScope() throws {
XCTAssertEqual(PasscodeKeychainScope.service("svc", environment: self.environment), "org.telegram.scoped.\(self.scope)/svc")
XCTAssertEqual(PasscodeKeychainScope.service("svc", environment: try self.makeEnvironment(scope: nil)), "svc")
XCTAssertEqual(PasscodeKeychainScope.service("svc", environment: try self.makeEnvironment(scope: "")), "svc")
XCTAssertFalse(PasscodeKeychainScope.service("org.telegram.ton-wallet.vault", environment: self.environment).hasPrefix("org.telegram.ton-wallet."))
}
func testScopedNamesNeverMatchAnotherScopesFilter() throws {
let debug = try self.makeEnvironment(scope: "debug")
let release = try self.makeEnvironment(scope: nil)
let debugItem = PasscodeKeychainScope.service("org.telegram.ton-wallet.vault.v1.envelope.ns", environment: debug)
let releaseItem = PasscodeKeychainScope.service("org.telegram.ton-wallet.vault.v1.envelope.ns", environment: release)
let otherScopeItem = "org.telegram.scoped.other/org.telegram.ton-wallet.vault.v1.envelope.ns"
let debugFilter = PasscodeKeychainScope.service("org.telegram.ton-wallet.", environment: debug)
let releaseFilter = PasscodeKeychainScope.service("org.telegram.ton-wallet.", environment: release)
XCTAssertTrue(debugItem.hasPrefix(debugFilter))
XCTAssertFalse(releaseItem.hasPrefix(debugFilter))
XCTAssertFalse(otherScopeItem.hasPrefix(debugFilter))
XCTAssertTrue(releaseItem.hasPrefix(releaseFilter))
XCTAssertFalse(debugItem.hasPrefix(releaseFilter))
XCTAssertFalse(otherScopeItem.hasPrefix(releaseFilter))
XCTAssertFalse(PasscodeKeychainScope.service(PasscodeKeychainScope.legacyPasscodeService, environment: debug) == PasscodeKeychainScope.legacyPasscodeService)
}
func testPasscodeItemsUseTheScopedService() throws {
let keychain = PasscodeKeychain(environment: self.environment)
try keychain.write(Data("scoped".utf8), account: self.account)
XCTAssertEqual(self.value(service: self.scoped(PasscodeKeychainScope.legacyPasscodeService), account: self.account), "scoped")
XCTAssertNil(self.value(service: PasscodeKeychainScope.legacyPasscodeService, account: self.account))
XCTAssertEqual(try keychain.read(self.account), Data("scoped".utf8))
try keychain.remove(self.account)
XCTAssertNil(self.value(service: self.scoped(PasscodeKeychainScope.legacyPasscodeService), account: self.account))
}
func testBiometricItemsUseTheScopedService() throws {
let scopedQuery = try PasscodeKeychain(environment: self.environment).query("biometric.\(self.account)")
XCTAssertEqual(scopedQuery[kSecAttrService as String] as? String, self.scoped(self.biometricService))
let unscopedQuery = try PasscodeKeychain(environment: try self.makeEnvironment(scope: nil)).query("biometric.\(self.account)")
XCTAssertEqual(unscopedQuery[kSecAttrService as String] as? String, self.biometricService)
}
func testScopedReadNeverFallsBackToUnscopedItems() throws {
self.add("legacy", service: PasscodeKeychainScope.legacyPasscodeService, account: self.account)
let keychain = PasscodeKeychain(environment: self.environment)
XCTAssertNil(try keychain.read(self.account))
try keychain.write(Data("scoped".utf8), account: self.account)
try keychain.remove(self.account)
XCTAssertNil(try keychain.read(self.account))
XCTAssertEqual(self.value(service: PasscodeKeychainScope.legacyPasscodeService, account: self.account), "legacy")
}
func testMigrationCopiesTheCredentialSetAndWalletItemsAndKeepsTheOriginals() {
self.add("credential", service: self.passcodeService, account: "credential")
self.add("device", service: self.passcodeService, account: "device")
self.add("attempts", service: self.passcodeService, account: "attempts")
self.add("descriptor", service: self.walletPrefix + "descriptor.ns", account: "wallet")
self.add("envelope", service: self.walletPrefix + "vault.ns", account: "secret")
self.add("metadata", service: self.walletPrefix + "metadata.ns", account: "wallet")
XCTAssertEqual(self.migrate(), .migrated(4))
XCTAssertEqual(self.value(service: self.scoped(self.passcodeService), account: "credential"), "credential")
XCTAssertEqual(self.value(service: self.scoped(self.passcodeService), account: "device"), "device")
XCTAssertEqual(self.value(service: self.scoped(self.walletPrefix + "descriptor.ns"), account: "wallet"), "descriptor")
XCTAssertEqual(self.value(service: self.scoped(self.walletPrefix + "vault.ns"), account: "secret"), "envelope")
XCTAssertNil(self.value(service: self.scoped(self.passcodeService), account: "attempts"))
XCTAssertNil(self.value(service: self.scoped(self.walletPrefix + "metadata.ns"), account: "wallet"))
XCTAssertEqual(self.value(service: self.passcodeService, account: "credential"), "credential")
XCTAssertEqual(self.value(service: self.passcodeService, account: "device"), "device")
XCTAssertEqual(self.value(service: self.passcodeService, account: "attempts"), "attempts")
XCTAssertEqual(self.value(service: self.walletPrefix + "descriptor.ns", account: "wallet"), "descriptor")
XCTAssertEqual(self.value(service: self.walletPrefix + "vault.ns", account: "secret"), "envelope")
XCTAssertEqual(self.migrate(), .migrated(0))
}
func testMigrationLeavesAScopeWithItsOwnCredential() {
self.add("scoped", service: self.scoped(self.passcodeService), account: "credential")
self.add("credential", service: self.passcodeService, account: "credential")
self.add("device", service: self.passcodeService, account: "device")
self.add("envelope", service: self.walletPrefix + "vault.ns", account: "secret")
XCTAssertEqual(self.migrate(), .nothingToMigrate)
XCTAssertEqual(self.value(service: self.scoped(self.passcodeService), account: "credential"), "scoped")
XCTAssertNil(self.value(service: self.scoped(self.passcodeService), account: "device"))
XCTAssertNil(self.value(service: self.scoped(self.walletPrefix + "vault.ns"), account: "secret"))
XCTAssertEqual(self.value(service: self.walletPrefix + "vault.ns", account: "secret"), "envelope")
}
func testMigrationResumesAnInterruptedCopy() {
self.add("credential", service: self.passcodeService, account: "credential")
self.add("device", service: self.passcodeService, account: "device")
self.add("descriptor", service: self.walletPrefix + "descriptor.ns", account: "wallet")
self.add("envelope", service: self.walletPrefix + "vault.ns", account: "secret")
self.add("descriptor", service: self.scoped(self.walletPrefix + "descriptor.ns"), account: "wallet")
self.add("device", service: self.scoped(self.passcodeService), account: "device")
XCTAssertEqual(self.migrate(), .migrated(2))
XCTAssertEqual(self.value(service: self.scoped(self.passcodeService), account: "credential"), "credential")
XCTAssertEqual(self.value(service: self.scoped(self.walletPrefix + "vault.ns"), account: "secret"), "envelope")
}
func testMigrationLeavesAWalletTheScopeAlreadyHolds() {
self.add("credential", service: self.passcodeService, account: "credential")
self.add("device", service: self.passcodeService, account: "device")
self.add("descriptor", service: self.walletPrefix + "descriptor.ns", account: "wallet")
self.add("envelope", service: self.walletPrefix + "vault.ns", account: "secret")
self.add("watch-only", service: self.scoped(self.walletPrefix + "descriptor.ns"), account: "wallet")
XCTAssertEqual(self.migrate(), .migrated(2))
XCTAssertEqual(self.value(service: self.scoped(self.walletPrefix + "descriptor.ns"), account: "wallet"), "watch-only")
XCTAssertFalse(self.exists(service: self.scoped(self.walletPrefix + "vault.ns"), account: "secret"))
XCTAssertEqual(self.value(service: self.walletPrefix + "descriptor.ns", account: "wallet"), "descriptor")
XCTAssertEqual(self.value(service: self.walletPrefix + "vault.ns", account: "secret"), "envelope")
}
func testMigrationLeavesADifferentScopedDeviceAlone() {
self.add("credential", service: self.passcodeService, account: "credential")
self.add("device", service: self.passcodeService, account: "device")
self.add("other", service: self.scoped(self.passcodeService), account: "device")
XCTAssertEqual(self.migrate(), .nothingToMigrate)
XCTAssertNil(self.value(service: self.scoped(self.passcodeService), account: "credential"))
XCTAssertEqual(self.value(service: self.scoped(self.passcodeService), account: "device"), "other")
}
func testAWalletWithAnItemThatNeedsConfirmationIsNotCopiedOrTouched() throws {
self.add("credential", service: self.passcodeService, account: "credential")
self.add("device", service: self.passcodeService, account: "device")
self.add("descriptor", service: self.walletPrefix + "descriptor.ns", account: "wallet")
try self.addNeedingConfirmation("envelope", service: self.walletPrefix + "vault.ns", account: "secret")
self.add("other", service: self.walletPrefix + "descriptor.other", account: "wallet")
XCTAssertEqual(self.migrate(), .migrated(3))
XCTAssertFalse(self.exists(service: self.scoped(self.walletPrefix + "descriptor.ns"), account: "wallet"))
XCTAssertFalse(self.exists(service: self.scoped(self.walletPrefix + "vault.ns"), account: "secret"))
XCTAssertEqual(self.value(service: self.scoped(self.walletPrefix + "descriptor.other"), account: "wallet"), "other")
XCTAssertEqual(self.value(service: self.walletPrefix + "descriptor.ns", account: "wallet"), "descriptor")
XCTAssertTrue(self.exists(service: self.walletPrefix + "vault.ns", account: "secret"))
}
func testMigrationNeedsBothTheCredentialAndTheDevice() {
self.add("credential", service: self.passcodeService, account: "credential")
self.add("envelope", service: self.walletPrefix + "vault.ns", account: "secret")
XCTAssertEqual(self.migrate(), .nothingToMigrate)
XCTAssertNil(self.value(service: self.scoped(self.passcodeService), account: "credential"))
XCTAssertNil(self.value(service: self.scoped(self.walletPrefix + "vault.ns"), account: "secret"))
XCTAssertEqual(self.value(service: self.passcodeService, account: "credential"), "credential")
XCTAssertEqual(self.value(service: self.walletPrefix + "vault.ns", account: "secret"), "envelope")
}
func testMigrationWithoutAScopeDoesNothing() throws {
self.add("credential", service: self.passcodeService, account: "credential")
self.add("device", service: self.passcodeService, account: "device")
XCTAssertEqual(self.migrate(environment: try self.makeEnvironment(scope: nil)), .nothingToMigrate)
XCTAssertEqual(self.value(service: self.passcodeService, account: "credential"), "credential")
}
func testMigrationRestoresUserInteraction() {
self.add("credential", service: self.passcodeService, account: "credential")
self.add("device", service: self.passcodeService, account: "device")
var before: DarwinBoolean = false
XCTAssertEqual(SecKeychainGetUserInteractionAllowed(&before), errSecSuccess)
XCTAssertEqual(self.migrate(), .migrated(2))
var after: DarwinBoolean = false
XCTAssertEqual(SecKeychainGetUserInteractionAllowed(&after), errSecSuccess)
XCTAssertEqual(before.boolValue, after.boolValue)
}
}
#endif