diff --git a/app.config.js b/app.config.js index 08714d55..e8c96160 100644 --- a/app.config.js +++ b/app.config.js @@ -57,12 +57,13 @@ module.exports = { supportsTablet: true, bundleIdentifier: 'org.bulwarkmail.mobile', buildNumber: IOS_BUILD_NUMBER, - config: { - // PGP and S/MIME (#58) add end-to-end encryption beyond HTTPS/TLS, - // so the app declares non-exempt encryption; App Store Connect asks - // for the export compliance documentation on upload. - usesNonExemptEncryption: true, - }, + // No usesNonExemptEncryption here. PGP and S/MIME (#58) are non-exempt + // encryption, but only standard algorithms from open-source libraries + // and the app is not on the French store, so App Store Connect keeps no + // declaration and has no ITSEncryptionExportComplianceCode to give. An + // Info.plist saying YES without that code fails upload validation + // (90592), so the key stays out and scripts/testflight-public.mjs + // answers the question for each build through the API. }, android: { adaptiveIcon: { diff --git a/scripts/testflight-public.mjs b/scripts/testflight-public.mjs index e9f5195b..5a15c48e 100644 --- a/scripts/testflight-public.mjs +++ b/scripts/testflight-public.mjs @@ -99,7 +99,7 @@ if (!group) { // ── Wait for the build ───────────────────────────────────────── const query = `/v1/builds?filter[app]=${app.id}&filter[version]=${encodeURIComponent(buildNumber)}` + - `&filter[preReleaseVersion.version]=${encodeURIComponent(version)}&fields[builds]=version,processingState`; + `&filter[preReleaseVersion.version]=${encodeURIComponent(version)}&fields[builds]=version,processingState,usesNonExemptEncryption`; const started = Date.now(); let build; for (;;) { @@ -121,6 +121,18 @@ for (;;) { } console.log(`Build ${version} (${buildNumber}) is processed.`); +// ── Export compliance ────────────────────────────────────────── +// The app uses non-exempt encryption (PGP and S/MIME, #58) with standard +// algorithms only and is not on the French store: App Store Connect needs no +// documentation, so Info.plist has no key and the answer is given here. +if (build.attributes.usesNonExemptEncryption == null) { + const answer = await api('PATCH', `/v1/builds/${build.id}`, { + data: { type: 'builds', id: build.id, attributes: { usesNonExemptEncryption: true } }, + }); + if (!answer.ok) fail('Answering export compliance', answer); + console.log('Export compliance answered: standard encryption, no documentation needed.'); +} + // ── Add it to the group ──────────────────────────────────────── const add = await api('POST', `/v1/betaGroups/${group.id}/relationships/builds`, { data: [{ type: 'builds', id: build.id }], @@ -150,7 +162,7 @@ const detail = await api('GET', `/v1/builds/${build.id}/buildBetaDetail`); if (!detail.ok) fail('Reading the build state', detail); const external = detail.json.data.attributes.externalBuildState; if (external === 'MISSING_EXPORT_COMPLIANCE') { - console.error('The build has no export compliance answer; set usesNonExemptEncryption in app.config.js.'); + console.error('The build still has no export compliance answer; answer it in App Store Connect > TestFlight.'); process.exit(1); } if (external === 'READY_FOR_BETA_SUBMISSION') { diff --git a/src/lib/__tests__/export-compliance.test.ts b/src/lib/__tests__/export-compliance.test.ts index d50a0134..f44f081f 100644 --- a/src/lib/__tests__/export-compliance.test.ts +++ b/src/lib/__tests__/export-compliance.test.ts @@ -2,13 +2,22 @@ import { describe, expect, it } from 'vitest'; import fs from 'node:fs'; import path from 'node:path'; -// With PGP and S/MIME in the app (#58) the encryption is no longer only -// HTTPS/TLS, so the App Store export compliance answer must say so. +// PGP and S/MIME (#58) are non-exempt encryption with standard algorithms, and +// the app is not on the French store, so App Store Connect has no declaration +// and no compliance code. Info.plist must not claim YES (upload validation +// rejects it without the code, 90592) nor NO (that would be untrue); the +// TestFlight script answers per build instead. + +const root = path.resolve(__dirname, '../../..'); describe('export compliance', () => { - it('declares non-exempt encryption for iOS', () => { - const src = fs.readFileSync(path.resolve(__dirname, '../../../app.config.js'), 'utf8'); - expect(src).toMatch(/usesNonExemptEncryption: true/); - expect(src).not.toMatch(/usesNonExemptEncryption: false/); + it('leaves the encryption key out of the iOS config', () => { + const src = fs.readFileSync(path.join(root, 'app.config.js'), 'utf8'); + expect(src).not.toMatch(/usesNonExemptEncryption\s*:/); + }); + + it('answers non-exempt encryption for each TestFlight build', () => { + const src = fs.readFileSync(path.join(root, 'scripts/testflight-public.mjs'), 'utf8'); + expect(src).toMatch(/attributes: \{ usesNonExemptEncryption: true \}/); }); });