PairingAuth.kt

4.7 KB · raw

1package com.peachpasswords.sync23import org.json.JSONObject4import java.security.MessageDigest5import java.security.SecureRandom6import javax.crypto.Mac7import javax.crypto.spec.SecretKeySpec89private val PAIRING_AUTH_DOMAIN = "peach-sync-v1-initiator-auth".encodeToByteArray()1011/**12 * E1 post-NK proof:13 * HMAC-SHA256(X25519(initiatorPriv, responderPub),14 *   handshakeHash || domain || initiatorPub || responderPub || nonce)15 */16internal fun computeInitiatorAuthProof(17    initiatorPrivateKey: ByteArray,18    initiatorPublicKey: ByteArray,19    responderPublicKey: ByteArray,20    nonce: ByteArray,21    handshakeHash: ByteArray,22): ByteArray {23    require(initiatorPrivateKey.size == 32) { "initiator private key must be 32 bytes" }24    require(initiatorPublicKey.size == 32) { "initiator public key must be 32 bytes" }25    require(responderPublicKey.size == 32) { "responder public key must be 32 bytes" }26    require(nonce.size == 32) { "auth nonce must be 32 bytes" }27    require(handshakeHash.size == 32) { "handshake hash must be 32 bytes" }2829    return computeAuthProofForPeer(30        localPrivateKey = initiatorPrivateKey,31        remotePublicKey = responderPublicKey,32        initiatorPublicKey = initiatorPublicKey,33        responderPublicKey = responderPublicKey,34        nonce = nonce,35        handshakeHash = handshakeHash,36    )37}3839private fun computeAuthProofForPeer(40    localPrivateKey: ByteArray,41    remotePublicKey: ByteArray,42    initiatorPublicKey: ByteArray,43    responderPublicKey: ByteArray,44    nonce: ByteArray,45    handshakeHash: ByteArray,46): ByteArray {47    val dh = x25519SharedKey(localPrivateKey, remotePublicKey)48    return Mac.getInstance("HmacSHA256").run {49        init(SecretKeySpec(dh, "HmacSHA256"))50        update(handshakeHash)51        update(PAIRING_AUTH_DOMAIN)52        update(initiatorPublicKey)53        update(responderPublicKey)54        update(nonce)55        doFinal()56    }57}5859/**60 * Browser/Android initiator half, useful for native-to-native first pairing.61 * The challenge's responder key must match the key already authenticated by NK.62 */63internal suspend fun performInitiatorPairingAuth(64    session: NoiseNKSession,65    ourPrivateKey: ByteArray,66    ourPublicKey: ByteArray,67    responderPublicKey: ByteArray,68    sendEncryptedJson: suspend (ByteArray) -> Unit,69    receiveEncryptedJson: suspend () -> ByteArray,70) {71    val challenge = JSONObject(String(receiveEncryptedJson(), Charsets.UTF_8))72    require(challenge.optString("type") == "CHALLENGE") { "expected CHALLENGE frame" }73    val nonce = hexToPubkey(challenge.getString("nonce_hex"))74    val challengedResponder = hexToPubkey(challenge.getString("responder_public_key_hex"))75    require(MessageDigest.isEqual(challengedResponder, responderPublicKey)) {76        "responder key in challenge does not match Noise handshake"77    }78    val proof = computeInitiatorAuthProof(79        ourPrivateKey,80        ourPublicKey,81        responderPublicKey,82        nonce,83        session.handshakeHash,84    )85    sendEncryptedJson(86        JSONObject()87            .put("type", "PROOF")88            .put("initiator_public_key_hex", pubkeyToHex(ourPublicKey))89            .put("proof_hex", pubkeyToHex(proof))90            .toString()91            .toByteArray(Charsets.UTF_8),92    )93}9495/**96 * Android responder half. Returns the initiator key whose private half was97 * proven, so the caller can bind it to the following PAIR_IDENTITY frame.98 */99internal suspend fun performResponderPairingAuth(100    session: NoiseNKSession,101    ourPrivateKey: ByteArray,102    ourPublicKey: ByteArray,103    sendEncryptedJson: suspend (ByteArray) -> Unit,104    receiveEncryptedJson: suspend () -> ByteArray,105    random: SecureRandom = SecureRandom(),106): ByteArray {107    val nonce = ByteArray(32).also(random::nextBytes)108    sendEncryptedJson(109        JSONObject()110            .put("type", "CHALLENGE")111            .put("nonce_hex", pubkeyToHex(nonce))112            .put("responder_public_key_hex", pubkeyToHex(ourPublicKey))113            .toString()114            .toByteArray(Charsets.UTF_8),115    )116117    val frame = JSONObject(String(receiveEncryptedJson(), Charsets.UTF_8))118    require(frame.optString("type") == "PROOF") { "expected PROOF frame" }119    val initiatorPublicKey = hexToPubkey(frame.getString("initiator_public_key_hex"))120    val claimedProof = hexToPubkey(frame.getString("proof_hex"))121    val expectedProof = computeAuthProofForPeer(122        localPrivateKey = ourPrivateKey,123        remotePublicKey = initiatorPublicKey,124        initiatorPublicKey = initiatorPublicKey,125        responderPublicKey = ourPublicKey,126        nonce = nonce,127        handshakeHash = session.handshakeHash,128    )129    require(MessageDigest.isEqual(expectedProof, claimedProof)) { "auth proof did not verify" }130    return initiatorPublicKey131}