diff --git a/projects/ios-readplace/Shared/TokenStore.swift b/projects/ios-readplace/Shared/TokenStore.swift index ab7ef6c9d..d9c1d560e 100644 --- a/projects/ios-readplace/Shared/TokenStore.swift +++ b/projects/ios-readplace/Shared/TokenStore.swift @@ -61,10 +61,23 @@ struct TokenStore { guard let url = Bundle.main.url(forResource: "embedded", withExtension: "mobileprovision"), let data = try? Data(contentsOf: url), + let group = parseAppGroupId(fromProvisioningProfile: data) + else { return AppConfig.appGroupId } + return group + }() + + /// Extracts the first application-group entitlement from an embedded + /// `.mobileprovision` blob, or nil when it can't be read. The profile is a + /// CMS-signed container with a plist inside; the signature isn't verified here + /// (the OS already did at install), so this just scans out the `` + /// slice and reads the entitlement. Pulled out of `resolvedAppGroupId` so the + /// parsing is unit-testable without a real `Bundle.main`. + static func parseAppGroupId(fromProvisioningProfile data: Data) -> String? { + guard let raw = String(data: data, encoding: .isoLatin1), let start = raw.range(of: "") - else { return AppConfig.appGroupId } + else { return nil } let plistString = String(raw[start.lowerBound.. Data { + // A .mobileprovision is a CMS blob with a plist inside; the parser only + // scans out the slice, so wrapping it in arbitrary bytes + // models the real container without needing a signed profile. + Data("....signature-bytes....\(innerPlist)....trailer....".utf8) + } + + func testParsesTheFirstApplicationGroupFromAProfile() { + let plist = """ + + + Entitlements + com.apple.security.application-groups + group.com.rewritten.readplacegroup.other + + """ + XCTAssertEqual( + TokenStore.parseAppGroupId(fromProvisioningProfile: profile(plist)), + "group.com.rewritten.readplace" + ) + } + + func testReturnsNilWhenTheProfileHasNoPlist() { + XCTAssertNil(TokenStore.parseAppGroupId(fromProvisioningProfile: Data("no plist here".utf8))) + } + + func testReturnsNilWhenTheProfileHasNoAppGroupEntitlement() { + let plist = """ + + + Entitlementsapplication-identifierABCDE.com.x + + """ + XCTAssertNil(TokenStore.parseAppGroupId(fromProvisioningProfile: profile(plist))) + } + + func testReturnsNilWhenTheAppGroupArrayIsEmpty() { + let plist = """ + + + Entitlements + com.apple.security.application-groups + + """ + XCTAssertNil(TokenStore.parseAppGroupId(fromProvisioningProfile: profile(plist))) + } } diff --git a/projects/ios-readplace/scripts/coverage-baseline.json b/projects/ios-readplace/scripts/coverage-baseline.json index 6529d41fe..6323ef461 100644 --- a/projects/ios-readplace/scripts/coverage-baseline.json +++ b/projects/ios-readplace/scripts/coverage-baseline.json @@ -24,14 +24,14 @@ "PKCE.swift": 95, "ReadplaceAPI.swift": 94, "URLDetection.swift": 94, - "ReadingListViewModel.swift": 93, + "ReadingListViewModel.swift": 94, "SaveSharedPage.swift": 93, "LoginView.swift": 91, "AffordancePresentation.swift": 91, - "OAuthService.swift": 89, - "TokenStore.swift": 85, - "AppSession.swift": 79, - "ReadplaceApp.swift": 64, - "KeychainTokenStorage.swift": 57 + "TokenStore.swift": 98, + "OAuthService.swift": 97, + "KeychainTokenStorage.swift": 97, + "AppSession.swift": 82, + "ReadplaceApp.swift": 64 } }