{ "_comment": "Cross-platform test vectors for the sealed-answer crypto protocol.", "_note": "Values are stable inputs and commitments. The ciphertext fields are illustrative — run the regeneration script to produce real ciphertexts for CI.", "_regenerate": "cd app && ./gradlew :app:testDebugUnitTest --tests 'app.closer.crypto.SealedAnswerTestVectorGenerator'", "protocol_version": "v1", "commitment": { "_spec": "SHA-256(\"v1|{coupleId}|{questionId}|{userId}|{canonicalJson}\"), urlsafe-base64-no-padding", "_canonical_json_key_order": ["scaleValue", "selectedOptionIds", "writtenText"], "_selectedOptionIds_order": "lexicographic ascending", "vectors": [ { "id": "commit-001", "input": { "coupleId": "couple-abc", "questionId": "q-001", "userId": "user-alice", "writtenText": "I love rainy evenings at home.", "selectedOptionIds": ["opt-b", "opt-a"], "scaleValue": 7 }, "canonical_json": "{\"scaleValue\":7,\"selectedOptionIds\":[\"opt-a\",\"opt-b\"],\"writtenText\":\"I love rainy evenings at home.\"}", "commitment_hash": "sha256:REGENERATE_WITH_SCRIPT" }, { "id": "commit-002-nulls", "input": { "coupleId": "couple-abc", "questionId": "q-002", "userId": "user-bob", "writtenText": null, "selectedOptionIds": [], "scaleValue": null }, "canonical_json": "{\"scaleValue\":null,\"selectedOptionIds\":[],\"writtenText\":null}", "commitment_hash": "sha256:REGENERATE_WITH_SCRIPT" }, { "id": "commit-003-special-chars", "input": { "coupleId": "couple-xyz", "questionId": "q-003", "userId": "user-alice", "writtenText": "she said \"hello\"\nand left.\ttab here", "selectedOptionIds": [], "scaleValue": null }, "canonical_json": "{\"scaleValue\":null,\"selectedOptionIds\":[],\"writtenText\":\"she said \\\"hello\\\"\\nand left.\\ttab here\"}", "commitment_hash": "sha256:REGENERATE_WITH_SCRIPT" } ] }, "sealed_payload": { "_spec": "AES-256-GCM via Tink AEAD. AAD = UTF-8('{coupleId}|{questionId}|{userId}'). Wire format: 'sealed:v1:{urlsafe-base64-no-padding}'.", "_note": "Ciphertext is non-deterministic (random nonce per encrypt call). Test by decrypt-round-trip, not by fixed ciphertext.", "aad_format": "{coupleId}|{questionId}|{userId}", "payload_format": "{\"scaleValue\":{int_or_null},\"selectedOptionIds\":[...sorted...],\"writtenText\":{string_or_null}}" }, "release_key": { "_spec": "ECIES-P256-HKDF-HMAC-SHA256-AES128-GCM via Tink HybridEncrypt. Context info = UTF-8('{coupleId}|{questionId}|{senderUserId}|{recipientUserId}'). Wire format: 'keybox:v1:{urlsafe-base64-no-padding}'.", "context_info_format": "{coupleId}|{questionId}|{senderUserId}|{recipientUserId}" }, "public_key": { "_spec": "Tink ECIES public keyset serialised as JSON, then urlsafe-base64-no-padding. Wire format: 'pub:v1:{base64}'.", "algorithm": "ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM" }, "ios_compat_notes": [ "Use urlsafe Base64 (no padding) for all wire formats.", "Canonical JSON key order is fixed: scaleValue, selectedOptionIds, writtenText.", "selectedOptionIds must be sorted lexicographically before hashing and before encryption.", "String encoding is always UTF-8.", "SHA-256 input is UTF-8 bytes of the full prefix+canonical-json string.", "ECIES ciphertext prefix format is Tink's standard; iOS must use Tink or a compatible library." ] }