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()10111213141516internal 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}585960616263internal 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}949596979899internal 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}