mirror of
https://github.com/ichmagmaus111/ghostgram.git
synced 2026-11-04 14:47:17 +01:00
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
270 lines
17 KiB
Swift
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
|