1package com.peachpasswords.updates23import java.util.Base644import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters5import org.bouncycastle.crypto.signers.Ed25519Signer6import org.json.JSONObject78910111213const val UPDATE_METADATA_PUBLIC_KEY_HEX = "d6a47f867d2add9c6490447c7a611c08a474a98ab7afa540f65d4347f26ba77c"1415internal fun canonicalReleaseMetadataPayload(16 version: String,17 platform: String,18 url: String,19 checksumHex: String,20 provenanceSource: String,21): ByteArray = "$version\n$platform\n$url\n$checksumHex\n$version\n$platform\n$checksumHex\n$provenanceSource"22 .toByteArray(Charsets.UTF_8)2324private fun decodeHex(value: String): ByteArray? {25 if (value.length != 64 || value.any { it !in "0123456789abcdefABCDEF" }) return null26 return ByteArray(32) { i ->27 value.substring(i * 2, i * 2 + 2).toInt(16).toByte()28 }29}3031private fun verifySignature(publicKeyHex: String, payload: ByteArray, encoded: String): Boolean {32 val key = decodeHex(publicKeyHex) ?: return false33 val signature = runCatching { Base64.getDecoder().decode(encoded) }.getOrNull() ?: return false34 if (signature.size != 64) return false35 return runCatching {36 Ed25519Signer().apply {37 init(false, Ed25519PublicKeyParameters(key, 0))38 update(payload, 0, payload.size)39 }.verifySignature(signature)40 }.getOrDefault(false)41}42434445464748fun parseSignedReleaseManifest(49 body: String,50 platformKey: String = ANDROID_PLATFORM_KEY,51 publicKeyHex: String = UPDATE_METADATA_PUBLIC_KEY_HEX,52): ReleaseManifest {53 val root = runCatching { JSONObject(body) }54 .getOrElse { throw ManifestFormatException("manifest is not JSON") }55 val version = root.optTextOrNull("version")56 ?: throw ManifestFormatException("signed manifest has no version")57 val platform = root.optJSONObject("platforms")?.optJSONObject(platformKey)58 ?: throw ManifestFormatException("signed manifest has no '$platformKey' artifact")59 val url = platform.optTextOrNull("url")60 ?: throw ManifestFormatException("signed artifact has no url")61 val checksum = parseArtifactChecksum(platform)62 ?: throw ManifestFormatException("signed artifact has no checksum")63 val checksumParts = checksum.split(':', limit = 2)64 val checksumHex = checksumParts.getOrNull(1)65 ?: throw ManifestFormatException("signed artifact checksum is malformed")66 val provenance = platform.optJSONObject("provenance")67 ?: throw ManifestFormatException("signed artifact has no provenance")68 val provenanceVersion = provenance.optTextOrNull("version")69 ?: throw ManifestFormatException("signed artifact provenance has no version")70 val provenancePlatform = provenance.optTextOrNull("platform")71 ?: throw ManifestFormatException("signed artifact provenance has no platform")72 val provenanceChecksum = provenance.optTextOrNull("checksum")73 ?: throw ManifestFormatException("signed artifact provenance has no checksum")74 val source = provenance.optTextOrNull("source")75 ?: throw ManifestFormatException("signed artifact provenance has no source")76 val signature = platform.optTextOrNull("signature")77 ?: throw ManifestFormatException("signed artifact has no signature")78 if (provenanceVersion != version || provenancePlatform != platformKey ||79 provenanceChecksum != checksum || checksumParts[0] != "blake3") {80 throw ManifestFormatException("signed artifact provenance does not match manifest")81 }82 if (!verifySignature(83 publicKeyHex,84 canonicalReleaseMetadataPayload(version, platformKey, url, checksumHex, source),85 signature,86 )87 ) {88 throw ManifestFormatException("release metadata signature verification failed")89 }90 return parseReleaseManifest(body, platformKey).copy(91 signature = signature,92 provenanceSource = source,93 platformKey = platformKey,94 )95}