1package com.peachpasswords.sync23import org.bouncycastle.crypto.InvalidCipherTextException4import org.bouncycastle.crypto.digests.Blake2sDigest5import org.bouncycastle.crypto.generators.HKDFBytesGenerator6import org.bouncycastle.crypto.modes.ChaCha20Poly13057import org.bouncycastle.crypto.params.AEADParameters8import org.bouncycastle.crypto.params.HKDFParameters9import org.bouncycastle.crypto.params.KeyParameter10import org.bouncycastle.crypto.params.X25519PrivateKeyParameters11import org.bouncycastle.crypto.params.X25519PublicKeyParameters12import java.nio.ByteBuffer13import java.nio.ByteOrder14import java.security.SecureRandom1516private const val PROTOCOL_NAME = "Noise_XX_25519_ChaChaPoly_BLAKE2s"1718private fun initHash(): ByteArray {19 val nameBytes = PROTOCOL_NAME.encodeToByteArray()20 if (nameBytes.size <= 32) {21 return ByteArray(32).also { nameBytes.copyInto(it) }22 }23 24 25 26 val blake = Blake2sDigest(256)27 blake.update(nameBytes, 0, nameBytes.size)28 val out = ByteArray(32)29 blake.doFinal(out, 0)30 return out31}3233private fun mixHash(h: ByteArray, data: ByteArray): ByteArray {34 val blake = Blake2sDigest(256)35 blake.update(h, 0, h.size)36 blake.update(data, 0, data.size)37 val out = ByteArray(32)38 blake.doFinal(out, 0)39 return out40}4142private fun hkdf2(ck: ByteArray, ikm: ByteArray): Pair<ByteArray, ByteArray> {43 44 val generator = HKDFBytesGenerator(Blake2sDigest(256))45 generator.init(HKDFParameters(ikm, ck, ByteArray(0)))46 val out = ByteArray(64)47 generator.generateBytes(out, 0, out.size)48 return out.copyOfRange(0, 32) to out.copyOfRange(32, 64)49}50515253internal fun noiseXXInitHashForTest(): ByteArray = initHash()54internal fun noiseXXHkdf2ForTest(ck: ByteArray, ikm: ByteArray): Pair<ByteArray, ByteArray> = hkdf2(ck, ikm)5556private fun nonceBytes(n: Long): ByteArray {57 val buf = ByteArray(12)58 ByteBuffer.wrap(buf).order(ByteOrder.LITTLE_ENDIAN).putLong(4, n)59 return buf60}6162interface NoiseXXSession {63 fun encrypt(plaintext: ByteArray): ByteArray64 fun decrypt(ciphertext: ByteArray): ByteArray65 val remoteStaticPublicKey: ByteArray66 val handshakeHash: ByteArray67}6869private data class X25519KeyPair(70 val publicKey: ByteArray,71 val secretKey: ByteArray,72)7374private fun generateKeyPair(): X25519KeyPair {75 val priv = X25519PrivateKeyParameters(SecureRandom())76 return X25519KeyPair(77 publicKey = priv.generatePublicKey().encoded,78 secretKey = priv.encoded,79 )80}8182private fun keyPairFromSeed(seed: ByteArray): X25519KeyPair {83 val priv = X25519PrivateKeyParameters(seed, 0)84 return X25519KeyPair(85 publicKey = priv.generatePublicKey().encoded,86 secretKey = priv.encoded,87 )88}8990private fun sharedKey(privateKey: ByteArray, publicKey: ByteArray): ByteArray {91 val priv = X25519PrivateKeyParameters(privateKey, 0)92 val pub = X25519PublicKeyParameters(publicKey, 0)93 val out = ByteArray(32)94 priv.generateSecret(pub, out, 0)95 return out96}9798private fun chachaSeal(key: ByteArray, nonce: ByteArray, plaintext: ByteArray, aad: ByteArray): ByteArray {99 val cipher = ChaCha20Poly1305()100 cipher.init(true, AEADParameters(KeyParameter(key), 128, nonce, aad))101 val out = ByteArray(cipher.getOutputSize(plaintext.size))102 var len = cipher.processBytes(plaintext, 0, plaintext.size, out, 0)103 len += cipher.doFinal(out, len)104 return out.copyOf(len)105}106107private fun chachaOpen(key: ByteArray, nonce: ByteArray, ciphertext: ByteArray, aad: ByteArray): ByteArray? {108 return try {109 val cipher = ChaCha20Poly1305()110 cipher.init(false, AEADParameters(KeyParameter(key), 128, nonce, aad))111 val out = ByteArray(cipher.getOutputSize(ciphertext.size))112 var len = cipher.processBytes(ciphertext, 0, ciphertext.size, out, 0)113 len += cipher.doFinal(out, len)114 out.copyOf(len)115 } catch (_: InvalidCipherTextException) {116 null117 }118}119120internal fun noiseXXDeterministicHandshakeHashForTest(): ByteArray {121 val initiatorStatic = keyPairFromSeed(ByteArray(32) { 0xa1.toByte() })122 val responderStatic = keyPairFromSeed(ByteArray(32) { 0xb2.toByte() })123 val initiatorEphemeral = keyPairFromSeed(ByteArray(32) { 0x01 })124 val responderEphemeral = keyPairFromSeed(ByteArray(32) { 0x02 })125126 var hI = initHash()127 var ckI = hI.copyOf()128 var hR = initHash()129 var ckR = hR.copyOf()130131 132 val msg1 = initiatorEphemeral.publicKey133 hI = mixHash(hI, msg1)134 hI = mixHash(hI, ByteArray(0))135 hR = mixHash(hR, msg1)136 hR = mixHash(hR, ByteArray(0))137138 139 hR = mixHash(hR, responderEphemeral.publicKey)140 val eeR = sharedKey(responderEphemeral.secretKey, msg1)141 val (ckR1, kR1) = hkdf2(ckR, eeR)142 ckR = ckR1143 val encryptedResponderStatic = chachaSeal(kR1, nonceBytes(0L), responderStatic.publicKey, hR)144 hR = mixHash(hR, encryptedResponderStatic)145146 val esR = sharedKey(responderStatic.secretKey, msg1)147 val (ckR2, kR2) = hkdf2(ckR, esR)148 ckR = ckR2149 val encryptedPayload = chachaSeal(kR2, nonceBytes(0L), ByteArray(0), hR)150 hR = mixHash(hR, encryptedPayload)151152 val remoteEphemeral = responderEphemeral.publicKey153 hI = mixHash(hI, remoteEphemeral)154 val eeI = sharedKey(initiatorEphemeral.secretKey, remoteEphemeral)155 val (ckI1, kI1) = hkdf2(ckI, eeI)156 ckI = ckI1157 val remoteStaticPlain = chachaOpen(kI1, nonceBytes(0L), encryptedResponderStatic, hI)158 ?: throw IllegalStateException("Noise XX KAT: msg2 static auth failed")159 hI = mixHash(hI, encryptedResponderStatic)160161 val esI = sharedKey(initiatorEphemeral.secretKey, remoteStaticPlain)162 val (ckI2, kI2) = hkdf2(ckI, esI)163 ckI = ckI2164 chachaOpen(kI2, nonceBytes(0L), encryptedPayload, hI)165 ?: throw IllegalStateException("Noise XX KAT: msg2 payload auth failed")166 hI = mixHash(hI, encryptedPayload)167168 169 val encryptedInitiatorStatic = chachaSeal(kI2, nonceBytes(1L), initiatorStatic.publicKey, hI)170 hI = mixHash(hI, encryptedInitiatorStatic)171 val seI = sharedKey(initiatorStatic.secretKey, remoteEphemeral)172 val (ckI3, kI3) = hkdf2(ckI, seI)173 ckI = ckI3174 val encryptedFinalPayload = chachaSeal(kI3, nonceBytes(0L), ByteArray(0), hI)175 hI = mixHash(hI, encryptedFinalPayload)176177 val initiatorStaticPlain = chachaOpen(kR2, nonceBytes(1L), encryptedInitiatorStatic, hR)178 ?: throw IllegalStateException("Noise XX KAT: msg3 static auth failed")179 hR = mixHash(hR, encryptedInitiatorStatic)180 val seR = sharedKey(responderEphemeral.secretKey, initiatorStaticPlain)181 val (ckR3, kR3) = hkdf2(ckR, seR)182 ckR = ckR3183 chachaOpen(kR3, nonceBytes(0L), encryptedFinalPayload, hR)184 ?: throw IllegalStateException("Noise XX KAT: msg3 final payload auth failed")185 hR = mixHash(hR, encryptedFinalPayload)186187 check(hI.contentEquals(hR)) { "Noise XX KAT sides diverged" }188 check(ckI.contentEquals(ckR)) { "Noise XX KAT chaining keys diverged" }189 return hI190}191192suspend fun noiseXXInitiator(193 ourStaticPrivateKey: ByteArray,194 ourStaticPublicKey: ByteArray,195 send: suspend (ByteArray) -> Unit,196 receive: suspend () -> ByteArray,197): NoiseXXSession {198 var h = initHash()199 var ck = h.copyOf()200201 val e = generateKeyPair()202 h = mixHash(h, e.publicKey)203 204 205 206 h = mixHash(h, ByteArray(0))207 send(e.publicKey)208209 val msg2 = receive()210 require(msg2.size >= 32 + 32 + 16 + 16) { "Noise XX: msg2 too short" }211 val remoteEphemeral = msg2.copyOfRange(0, 32)212 val encryptedStatic = msg2.copyOfRange(32, 32 + 32 + 16)213 val encryptedPayload = msg2.copyOfRange(32 + 32 + 16, msg2.size)214215 h = mixHash(h, remoteEphemeral)216 val ee = sharedKey(e.secretKey, remoteEphemeral)217 val (ck1, k1) = hkdf2(ck, ee)218 ck = ck1219220 val remoteStaticPlain = chachaOpen(k1, nonceBytes(0L), encryptedStatic, h)221 ?: throw IllegalStateException("Noise XX: msg2 static auth failed")222 require(remoteStaticPlain.size == 32) { "Noise XX: msg2 static auth failed" }223 h = mixHash(h, encryptedStatic)224225 val es = sharedKey(e.secretKey, remoteStaticPlain)226 val (ck2, k2) = hkdf2(ck, es)227 ck = ck2228229 val payloadPlain = chachaOpen(k2, nonceBytes(0L), encryptedPayload, h)230 ?: throw IllegalStateException("Noise XX: msg2 payload auth failed")231 h = mixHash(h, encryptedPayload)232 @Suppress("UNUSED_VARIABLE")233 val _payloadPlain = payloadPlain234235 val encryptedOurStatic = chachaSeal(k2, nonceBytes(1L), ourStaticPublicKey, h)236 h = mixHash(h, encryptedOurStatic)237238 val se = sharedKey(ourStaticPrivateKey, remoteEphemeral)239 val (ck3, k3) = hkdf2(ck, se)240 ck = ck3241242 val encryptedFinalPayload = chachaSeal(k3, nonceBytes(0L), ByteArray(0), h)243 h = mixHash(h, encryptedFinalPayload)244245 val msg3 = ByteArray(encryptedOurStatic.size + encryptedFinalPayload.size)246 encryptedOurStatic.copyInto(msg3, 0)247 encryptedFinalPayload.copyInto(msg3, encryptedOurStatic.size)248 send(msg3)249250 val (initSend, respSend) = hkdf2(ck, ByteArray(0))251 val sendCipher = initSend252 val recvCipher = respSend253 var sendNonce = 0L254 var recvNonce = 0L255256 return object : NoiseXXSession {257 override fun encrypt(plaintext: ByteArray): ByteArray {258 return chachaSeal(sendCipher, nonceBytes(sendNonce++), plaintext, ByteArray(0))259 }260261 override fun decrypt(ciphertext: ByteArray): ByteArray {262 return chachaOpen(recvCipher, nonceBytes(recvNonce++), ciphertext, ByteArray(0))263 ?: throw IllegalStateException("Noise XX: decryption failed")264 }265266 override val remoteStaticPublicKey: ByteArray = remoteStaticPlain267 override val handshakeHash: ByteArray = h.copyOf()268 }269}270271suspend fun noiseXXResponder(272 ourStaticPrivateKey: ByteArray,273 ourStaticPublicKey: ByteArray,274 send: suspend (ByteArray) -> Unit,275 receive: suspend () -> ByteArray,276): NoiseXXSession {277 var h = initHash()278 var ck = h.copyOf()279280 val msg1 = receive()281 require(msg1.size >= 32) { "Noise XX: msg1 too short" }282 val remoteEphemeral = msg1.copyOfRange(0, 32)283 h = mixHash(h, remoteEphemeral)284 285 h = mixHash(h, ByteArray(0))286287 val e = generateKeyPair()288 h = mixHash(h, e.publicKey)289 val ee = sharedKey(e.secretKey, remoteEphemeral)290 val (ck1, k1) = hkdf2(ck, ee)291 ck = ck1292293 val encryptedOurStatic = chachaSeal(k1, nonceBytes(0L), ourStaticPublicKey, h)294 h = mixHash(h, encryptedOurStatic)295296 val es = sharedKey(ourStaticPrivateKey, remoteEphemeral)297 val (ck2, k2) = hkdf2(ck, es)298 ck = ck2299300 val encryptedPayload = chachaSeal(k2, nonceBytes(0L), ByteArray(0), h)301 h = mixHash(h, encryptedPayload)302303 val msg2 = ByteArray(e.publicKey.size + encryptedOurStatic.size + encryptedPayload.size)304 e.publicKey.copyInto(msg2, 0)305 encryptedOurStatic.copyInto(msg2, e.publicKey.size)306 encryptedPayload.copyInto(msg2, e.publicKey.size + encryptedOurStatic.size)307 send(msg2)308309 val msg3 = receive()310 require(msg3.size >= 32 + 16 + 16) { "Noise XX: msg3 too short" }311 val encryptedRemoteStatic = msg3.copyOfRange(0, 32 + 16)312 val encryptedFinalPayload = msg3.copyOfRange(32 + 16, msg3.size)313314 val remoteStaticPlain = chachaOpen(k2, nonceBytes(1L), encryptedRemoteStatic, h)315 ?: throw IllegalStateException("Noise XX: msg3 static auth failed")316 require(remoteStaticPlain.size == 32) { "Noise XX: msg3 static auth failed" }317 h = mixHash(h, encryptedRemoteStatic)318319 val se = sharedKey(e.secretKey, remoteStaticPlain)320 val (ck3, k3) = hkdf2(ck, se)321 ck = ck3322323 val finalPayloadPlain = chachaOpen(k3, nonceBytes(0L), encryptedFinalPayload, h)324 ?: throw IllegalStateException("Noise XX: msg3 final payload auth failed")325 h = mixHash(h, encryptedFinalPayload)326 @Suppress("UNUSED_VARIABLE")327 val _finalPayloadPlain = finalPayloadPlain328329 val (initSend, respSend) = hkdf2(ck, ByteArray(0))330 val sendCipher = respSend331 val recvCipher = initSend332 var sendNonce = 0L333 var recvNonce = 0L334335 return object : NoiseXXSession {336 override fun encrypt(plaintext: ByteArray): ByteArray {337 return chachaSeal(sendCipher, nonceBytes(sendNonce++), plaintext, ByteArray(0))338 }339340 override fun decrypt(ciphertext: ByteArray): ByteArray {341 return chachaOpen(recvCipher, nonceBytes(recvNonce++), ciphertext, ByteArray(0))342 ?: throw IllegalStateException("Noise XX: decryption failed")343 }344345 override val remoteStaticPublicKey: ByteArray = remoteStaticPlain346 override val handshakeHash: ByteArray = h.copyOf()347 }348}