package com.peachpasswords.updates import java.util.Base64 import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters import org.bouncycastle.crypto.signers.Ed25519Signer import org.json.JSONObject /** * Public half of the Peach-vault-backed release metadata key. The private seed * is never shipped or read by the app. An empty value deliberately fails closed * until the owner provisions the generated public key in the release source. */ const val UPDATE_METADATA_PUBLIC_KEY_HEX = "d6a47f867d2add9c6490447c7a611c08a474a98ab7afa540f65d4347f26ba77c" internal fun canonicalReleaseMetadataPayload( version: String, platform: String, url: String, checksumHex: String, provenanceSource: String, ): ByteArray = "$version\n$platform\n$url\n$checksumHex\n$version\n$platform\n$checksumHex\n$provenanceSource" .toByteArray(Charsets.UTF_8) private fun decodeHex(value: String): ByteArray? { if (value.length != 64 || value.any { it !in "0123456789abcdefABCDEF" }) return null return ByteArray(32) { i -> value.substring(i * 2, i * 2 + 2).toInt(16).toByte() } } private fun verifySignature(publicKeyHex: String, payload: ByteArray, encoded: String): Boolean { val key = decodeHex(publicKeyHex) ?: return false val signature = runCatching { Base64.getDecoder().decode(encoded) }.getOrNull() ?: return false if (signature.size != 64) return false return runCatching { Ed25519Signer().apply { init(false, Ed25519PublicKeyParameters(key, 0)) update(payload, 0, payload.size) }.verifySignature(signature) }.getOrDefault(false) } /** * Authenticates the platform object before [parseReleaseManifest] trusts its * URL, checksum, version, or notes. JSON is inspected only to locate the * signed fields; all semantic validation remains in the existing parser. */ fun parseSignedReleaseManifest( body: String, platformKey: String = ANDROID_PLATFORM_KEY, publicKeyHex: String = UPDATE_METADATA_PUBLIC_KEY_HEX, ): ReleaseManifest { val root = runCatching { JSONObject(body) } .getOrElse { throw ManifestFormatException("manifest is not JSON") } val version = root.optTextOrNull("version") ?: throw ManifestFormatException("signed manifest has no version") val platform = root.optJSONObject("platforms")?.optJSONObject(platformKey) ?: throw ManifestFormatException("signed manifest has no '$platformKey' artifact") val url = platform.optTextOrNull("url") ?: throw ManifestFormatException("signed artifact has no url") val checksum = parseArtifactChecksum(platform) ?: throw ManifestFormatException("signed artifact has no checksum") val checksumParts = checksum.split(':', limit = 2) val checksumHex = checksumParts.getOrNull(1) ?: throw ManifestFormatException("signed artifact checksum is malformed") val provenance = platform.optJSONObject("provenance") ?: throw ManifestFormatException("signed artifact has no provenance") val provenanceVersion = provenance.optTextOrNull("version") ?: throw ManifestFormatException("signed artifact provenance has no version") val provenancePlatform = provenance.optTextOrNull("platform") ?: throw ManifestFormatException("signed artifact provenance has no platform") val provenanceChecksum = provenance.optTextOrNull("checksum") ?: throw ManifestFormatException("signed artifact provenance has no checksum") val source = provenance.optTextOrNull("source") ?: throw ManifestFormatException("signed artifact provenance has no source") val signature = platform.optTextOrNull("signature") ?: throw ManifestFormatException("signed artifact has no signature") if (provenanceVersion != version || provenancePlatform != platformKey || provenanceChecksum != checksum || checksumParts[0] != "blake3") { throw ManifestFormatException("signed artifact provenance does not match manifest") } if (!verifySignature( publicKeyHex, canonicalReleaseMetadataPayload(version, platformKey, url, checksumHex, source), signature, ) ) { throw ManifestFormatException("release metadata signature verification failed") } return parseReleaseManifest(body, platformKey).copy( signature = signature, provenanceSource = source, platformKey = platformKey, ) }