diff --git a/.github/workflows/experimental-release.yml b/.github/workflows/experimental-release.yml index a5a276a..3d18cd7 100644 --- a/.github/workflows/experimental-release.yml +++ b/.github/workflows/experimental-release.yml @@ -101,8 +101,8 @@ jobs: "workflow_url": os.environ["WORKFLOW_URL"], "xcode_version": os.environ["XCODE_VERSION"], "sqlcipher_source_repository": "https://github.com/sqlcipher/sqlcipher.git", - "sqlcipher_source_tag": "v4.16.0", - "sqlcipher_source_commit": "e2a6040f2ae5cfff2b3e08eb3320007d93cdf3fc", + "sqlcipher_source_tag": "v4.17.0", + "sqlcipher_source_commit": "810db22f575ee7cf94ea96a3e91622b5fcece3dc", "assets": [ "SQLCipher.xcframework.zip", "SQLCipher.xcframework.sha256", @@ -126,8 +126,8 @@ jobs: - Commit: \`$GITHUB_SHA\` - Built at (UTC): \`$BUILT_AT\` - Xcode: \`$XCODE_VERSION\` - - SQLCipher upstream tag: \`v4.16.0\` - - SQLCipher upstream commit: \`e2a6040f2ae5cfff2b3e08eb3320007d93cdf3fc\` + - SQLCipher upstream tag: \`v4.17.0\` + - SQLCipher upstream commit: \`810db22f575ee7cf94ea96a3e91622b5fcece3dc\` - Workflow: [$WORKFLOW_URL]($WORKFLOW_URL) Assets: diff --git a/.github/workflows/stable-release.yml b/.github/workflows/stable-release.yml index bdfbbb8..afda53a 100644 --- a/.github/workflows/stable-release.yml +++ b/.github/workflows/stable-release.yml @@ -38,7 +38,7 @@ jobs: echo "::error::Stable SQLCipher release workflow must run from a tag." exit 1 fi - if ! printf '%s' "$RELEASE_TAG" | grep -Eq '^sqlcipher-xcframework-v4\.16\.0-cypherair\.[0-9]+$'; then + if ! printf '%s' "$RELEASE_TAG" | grep -Eq '^sqlcipher-xcframework-v4\.17\.0-cypherair\.[0-9]+$'; then echo "::error::Unexpected stable SQLCipher release tag: $RELEASE_TAG" exit 1 fi @@ -128,8 +128,8 @@ jobs: "signer_workflow": f"{os.environ['GITHUB_REPOSITORY']}/.github/workflows/stable-release.yml", "xcode_version": os.environ["XCODE_VERSION"], "sqlcipher_source_repository": "https://github.com/sqlcipher/sqlcipher.git", - "sqlcipher_source_tag": "v4.16.0", - "sqlcipher_source_commit": "e2a6040f2ae5cfff2b3e08eb3320007d93cdf3fc", + "sqlcipher_source_tag": "v4.17.0", + "sqlcipher_source_commit": "810db22f575ee7cf94ea96a3e91622b5fcece3dc", "assets": [ "SQLCipher.xcframework.zip", "SQLCipher.xcframework.sha256", @@ -153,8 +153,8 @@ jobs: - Commit: \`$RELEASE_COMMIT\` - Built at (UTC): \`$BUILT_AT\` - Xcode: \`$XCODE_VERSION\` - - SQLCipher upstream tag: \`v4.16.0\` - - SQLCipher upstream commit: \`e2a6040f2ae5cfff2b3e08eb3320007d93cdf3fc\` + - SQLCipher upstream tag: \`v4.17.0\` + - SQLCipher upstream commit: \`810db22f575ee7cf94ea96a3e91622b5fcece3dc\` - Workflow: [$WORKFLOW_URL]($WORKFLOW_URL) Assets: diff --git a/README.md b/README.md index e0ae4f0..bf3d59a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This repository builds the CypherAir-owned `SQLCipher.xcframework` for Apple targets that need `arm64e` device slices. It is not a fork of SQLCipher core. The build script consumes the upstream -`sqlcipher/sqlcipher` `v4.16.0` tag, verifies the peeled commit, generates the +`sqlcipher/sqlcipher` `v4.17.0` tag, verifies the peeled commit, generates the SQLCipher amalgamation, and packages a static framework-shaped XCFramework for CypherAir app integration. @@ -13,8 +13,9 @@ CypherAir app integration. - Status: stable external binary dependency infrastructure for CypherAir, with separate experiment and drill channels for validation. - Upstream source: `https://github.com/sqlcipher/sqlcipher` -- Upstream tag: `v4.16.0` -- Expected peeled commit: `e2a6040f2ae5cfff2b3e08eb3320007d93cdf3fc` +- Upstream tag: `v4.17.0` +- Expected peeled commit: `810db22f575ee7cf94ea96a3e91622b5fcece3dc` +- SQLite baseline: `3.53.3` - Package shape: static `SQLCipher.framework` slices inside `SQLCipher.xcframework`. - Crypto provider: Apple CommonCrypto / Security framework @@ -77,6 +78,10 @@ SQLCipher.framework/ Xcode consume the artifact through the normal Frameworks phase while keeping CypherAir's static-linking intent. +Validation also executes a macOS encrypted-database round trip, verifies the +SQLCipher 4.17.0 and SQLite 3.53.3 runtime identities, and requires a wrong key +to fail with `SQLITE_NOTADB`. + Run validation again against an existing build: ```bash @@ -91,7 +96,7 @@ python3 scripts/validate-sqlcipher-xcframework.py \ ## Release Discipline Stable releases use SSH-signed annotated tags such as -`sqlcipher-xcframework-v4.16.0-cypherair.1`. The stable release workflow only +`sqlcipher-xcframework-v4.17.0-cypherair.1`. The stable release workflow only publishes from those tags, produces non-prerelease immutable releases, and verifies both GitHub release integrity and artifact attestations after publication. @@ -108,7 +113,7 @@ Experiment and drill releases remain available for validation work and publish channel-specific metadata such as `sqlcipher-xcframework-experiment.json`. Release assets must be immutable. If a stable artifact is wrong, publish a new -semantic release tag such as `sqlcipher-xcframework-v4.16.0-cypherair.2` +semantic release tag such as `sqlcipher-xcframework-v4.17.0-cypherair.2` instead of replacing existing assets. ## Licensing diff --git a/scripts/build-sqlcipher-xcframework.sh b/scripts/build-sqlcipher-xcframework.sh index dbf30ac..b87d772 100755 --- a/scripts/build-sqlcipher-xcframework.sh +++ b/scripts/build-sqlcipher-xcframework.sh @@ -9,8 +9,9 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" SQLCIPHER_REPOSITORY="${SQLCIPHER_REPOSITORY:-https://github.com/sqlcipher/sqlcipher.git}" -SQLCIPHER_TAG="${SQLCIPHER_TAG:-v4.16.0}" -SQLCIPHER_EXPECTED_COMMIT="${SQLCIPHER_EXPECTED_COMMIT:-e2a6040f2ae5cfff2b3e08eb3320007d93cdf3fc}" +SQLCIPHER_TAG="${SQLCIPHER_TAG:-v4.17.0}" +SQLCIPHER_EXPECTED_COMMIT="${SQLCIPHER_EXPECTED_COMMIT:-810db22f575ee7cf94ea96a3e91622b5fcece3dc}" +SQLCIPHER_VERSION="${SQLCIPHER_TAG#v}" SQLCIPHER_RELEASE_STATUS="${SQLCIPHER_RELEASE_STATUS:-experimental}" BUILD_DIR="${SQLCIPHER_BUILD_DIR:-$REPO_ROOT/build}" @@ -184,7 +185,7 @@ create_framework_bundle() { cp "$INCLUDE_DIR/module.modulemap" "$framework_dir/Modules/module.modulemap" cp "$SOURCE_DIR/sqlcipher-resources/PrivacyInfo.xcprivacy" "$framework_dir/PrivacyInfo.xcprivacy" - cat > "$framework_dir/Info.plist" <<'PLIST' + cat > "$framework_dir/Info.plist" < @@ -202,9 +203,9 @@ create_framework_bundle() { CFBundlePackageType FMWK CFBundleShortVersionString - 4.16.0 + $SQLCIPHER_VERSION CFBundleVersion - 4.16.0 + $SQLCIPHER_VERSION PLIST diff --git a/scripts/validate-sqlcipher-xcframework.py b/scripts/validate-sqlcipher-xcframework.py index 0c42775..c854070 100755 --- a/scripts/validate-sqlcipher-xcframework.py +++ b/scripts/validate-sqlcipher-xcframework.py @@ -46,6 +46,11 @@ REQUIRED_FRAMEWORK_FILES = ["Info.plist", "Modules/module.modulemap", "PrivacyInfo.xcprivacy"] COMPILE_OPTIONS = ["SQLITE_HAS_CODEC", "SQLITE_TEMP_STORE=2"] LINK_FRAMEWORKS = ["Security", "CoreFoundation", "Foundation"] +EXPECTED_SOURCE_REPOSITORY = "https://github.com/sqlcipher/sqlcipher.git" +EXPECTED_SOURCE_TAG = "v4.17.0" +EXPECTED_SOURCE_COMMIT = "810db22f575ee7cf94ea96a3e91622b5fcece3dc" +EXPECTED_SQLCIPHER_VERSION = "4.17.0" +EXPECTED_SQLITE_VERSION = "3.53.3" CFLAGS = [ "-DNDEBUG", "-DSQLCIPHER_CRYPTO_CC", @@ -68,9 +73,9 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--manifest-out", type=Path) parser.add_argument("--status", choices=["experimental", "stable"], default="experimental") parser.add_argument("--source-dir", type=Path) - parser.add_argument("--source-repository", default="https://github.com/sqlcipher/sqlcipher.git") - parser.add_argument("--source-tag", default="v4.16.0") - parser.add_argument("--source-commit", default="e2a6040f2ae5cfff2b3e08eb3320007d93cdf3fc") + parser.add_argument("--source-repository", default=EXPECTED_SOURCE_REPOSITORY) + parser.add_argument("--source-tag", default=EXPECTED_SOURCE_TAG) + parser.add_argument("--source-commit", default=EXPECTED_SOURCE_COMMIT) parser.add_argument("--xcframework-zip", type=Path) parser.add_argument("--checksum-file", type=Path) parser.add_argument("--privacy-manifest", type=Path) @@ -150,6 +155,11 @@ def validate_xcframework(xcframework: Path) -> list[dict]: raise ValidationError(f"{identifier}: SQLCipher.framework Info.plist must declare CFBundlePackageType=FMWK") if framework_info.get("CFBundleExecutable") != "SQLCipher": raise ValidationError(f"{identifier}: SQLCipher.framework Info.plist must declare CFBundleExecutable=SQLCipher") + for version_key in ("CFBundleShortVersionString", "CFBundleVersion"): + if framework_info.get(version_key) != EXPECTED_SQLCIPHER_VERSION: + raise ValidationError( + f"{identifier}: {version_key} must be {EXPECTED_SQLCIPHER_VERSION}" + ) headers = framework / "Headers" for header in REQUIRED_HEADERS: @@ -258,11 +268,12 @@ def smoke_test(xcframework: Path) -> dict: if (sqlite3_open(":memory:", &db) != SQLITE_OK) return 10; if (query_value(db, "PRAGMA cipher_version;", value, sizeof(value)) != SQLITE_OK) return 11; - if (strncmp(value, "4.16.0", 6) != 0 || strstr(value, "community") == NULL) return 12; + if (strncmp(value, "__SQLCIPHER_VERSION__", sizeof("__SQLCIPHER_VERSION__") - 1) != 0 || strstr(value, "community") == NULL) return 12; sqlite3_close(db); if (!sqlite3_compileoption_used("SQLITE_HAS_CODEC")) return 13; if (!sqlite3_compileoption_used("SQLITE_TEMP_STORE=2")) return 14; + if (strcmp(sqlite3_libversion(), "__SQLITE_VERSION__") != 0) return 15; remove(path); if (sqlite3_open(path, &db) != SQLITE_OK) return 20; @@ -279,14 +290,17 @@ def smoke_test(xcframework: Path) -> dict: if (sqlite3_open(path, &db) != SQLITE_OK) return 40; if (exec_sql(db, bad_key) != SQLITE_OK) return 41; - int wrong_key_rc = query_value(db, "SELECT v FROM t;", value, sizeof(value)); + int wrong_key_rc = exec_sql(db, "CREATE TABLE should_not_exist(v TEXT);"); sqlite3_close(db); remove(path); - if (wrong_key_rc == SQLITE_OK) return 42; + if (wrong_key_rc != SQLITE_NOTADB) return 42; return 0; } ''' + source = source.replace("__SQLCIPHER_VERSION__", EXPECTED_SQLCIPHER_VERSION).replace( + "__SQLITE_VERSION__", EXPECTED_SQLITE_VERSION + ) with tempfile.TemporaryDirectory() as temp_name: temp_dir = Path(temp_name) @@ -417,10 +431,16 @@ def validate_manifest(path: Path, expected_status: str) -> None: if payload.get("status") != expected_status: raise ValidationError(f"manifest status must be {expected_status}") source = payload.get("source") or {} - if source.get("tag") != "v4.16.0": - raise ValidationError("manifest source tag must be v4.16.0") - if source.get("resolvedCommit") != "e2a6040f2ae5cfff2b3e08eb3320007d93cdf3fc": + if source.get("repository") != EXPECTED_SOURCE_REPOSITORY: + raise ValidationError("manifest source repository is not the pinned SQLCipher repository") + if source.get("tag") != EXPECTED_SOURCE_TAG: + raise ValidationError(f"manifest source tag must be {EXPECTED_SOURCE_TAG}") + if source.get("resolvedCommit") != EXPECTED_SOURCE_COMMIT: raise ValidationError("manifest source commit is not the pinned SQLCipher commit") + if source.get("versionFile") != EXPECTED_SQLITE_VERSION: + raise ValidationError( + f"manifest SQLite version must be {EXPECTED_SQLITE_VERSION}" + ) def main() -> int: