1package com.peachpasswords.sync23import org.bouncycastle.crypto.digests.Blake2sDigest4import org.bouncycastle.crypto.generators.HKDFBytesGenerator5import org.bouncycastle.crypto.modes.ChaCha20Poly13056import org.bouncycastle.crypto.params.AEADParameters7import org.bouncycastle.crypto.params.HKDFParameters8import org.bouncycastle.crypto.params.KeyParameter9import org.bouncycastle.crypto.params.X25519PrivateKeyParameters10import org.bouncycastle.crypto.params.X25519PublicKeyParameters11import java.nio.ByteBuffer12import java.nio.ByteOrder13import java.security.SecureRandom1415private const val NOISE_NK_PROTOCOL_NAME = "Noise_NK_25519_ChaChaPoly_BLAKE2s"1617interface NoiseNKSession {18 fun encrypt(plaintext: ByteArray): ByteArray19 fun decrypt(ciphertext: ByteArray): ByteArray20 val handshakeHash: ByteArray21}2223private data class NoiseNKKeyPair(24 val publicKey: ByteArray,25 val secretKey: ByteArray,26)2728private fun initHash(): ByteArray {29 val nameBytes = NOISE_NK_PROTOCOL_NAME.encodeToByteArray()30 if (nameBytes.size <= 32) {31 return ByteArray(32).also { nameBytes.copyInto(it) }32 }33 34 35 36 val blake = Blake2sDigest(256)37 blake.update(nameBytes, 0, nameBytes.size)38 val out = ByteArray(32)39 blake.doFinal(out, 0)40 return out41}4243private fun mixHash(h: ByteArray, data: ByteArray): ByteArray {44 val blake = Blake2sDigest(256)45 blake.update(h, 0, h.size)46 blake.update(data, 0, data.size)47 val out = ByteArray(32)48 blake.doFinal(out, 0)49 return out50}5152private fun hkdf2(ck: ByteArray, ikm: ByteArray): Pair<ByteArray, ByteArray> {53 54 val generator = HKDFBytesGenerator(Blake2sDigest(256))55 generator.init(HKDFParameters(ikm, ck, ByteArray(0)))56 val out = ByteArray(64)57 generator.generateBytes(out, 0, out.size)58 return out.copyOfRange(0, 32) to out.copyOfRange(32, 64)59}60616263internal fun noiseNKInitHashForTest(): ByteArray = initHash()64internal fun noiseNKHkdf2ForTest(ck: ByteArray, ikm: ByteArray): Pair<ByteArray, ByteArray> = hkdf2(ck, ikm)6566private fun nonceBytes(n: Long): ByteArray {67 val buf = ByteArray(12)68 ByteBuffer.wrap(buf).order(ByteOrder.LITTLE_ENDIAN).putLong(4, n)69 return buf70}7172private fun generateKeyPair(): NoiseNKKeyPair {73 val priv = X25519PrivateKeyParameters(SecureRandom())74 return NoiseNKKeyPair(75 publicKey = priv.generatePublicKey().encoded,76 secretKey = priv.encoded,77 )78}7980private fun sharedKey(privateKey: ByteArray, publicKey: ByteArray): ByteArray {81 val priv = X25519PrivateKeyParameters(privateKey, 0)82 val pub = X25519PublicKeyParameters(publicKey, 0)83 val out = ByteArray(32)84 priv.generateSecret(pub, out, 0)85 return out86}8788private fun chachaSeal(key: ByteArray, nonce: ByteArray, plaintext: ByteArray, aad: ByteArray): ByteArray {89 val cipher = ChaCha20Poly1305()90 cipher.init(true, AEADParameters(KeyParameter(key), 128, nonce, aad))91 val out = ByteArray(cipher.getOutputSize(plaintext.size))92 var len = cipher.processBytes(plaintext, 0, plaintext.size, out, 0)93 len += cipher.doFinal(out, len)94 return out.copyOf(len)95}9697private fun chachaOpen(key: ByteArray, nonce: ByteArray, ciphertext: ByteArray, aad: ByteArray): ByteArray? {98 return try {99 val cipher = ChaCha20Poly1305()100 cipher.init(false, AEADParameters(KeyParameter(key), 128, nonce, aad))101 val out = ByteArray(cipher.getOutputSize(ciphertext.size))102 var len = cipher.processBytes(ciphertext, 0, ciphertext.size, out, 0)103 len += cipher.doFinal(out, len)104 out.copyOf(len)105 } catch (_: Throwable) {106 null107 }108}109110suspend fun noiseNKInitiator(111 responderStaticPublicKey: ByteArray,112 send: suspend (ByteArray) -> Unit,113 receive: suspend () -> ByteArray,114): NoiseNKSession {115 var h = initHash()116 var ck = h.copyOf()117118 h = mixHash(h, responderStaticPublicKey)119120 val ephemeral = generateKeyPair()121 h = mixHash(h, ephemeral.publicKey)122 val es = sharedKey(ephemeral.secretKey, responderStaticPublicKey)123 val (ck1, k1) = hkdf2(ck, es)124 ck = ck1125126 val cipher1 = ChaCha20Poly1305()127 cipher1.init(true, AEADParameters(KeyParameter(k1), 128, nonceBytes(0L), h))128 val encrypted1 = ByteArray(cipher1.getOutputSize(0))129 cipher1.doFinal(encrypted1, 0)130 h = mixHash(h, encrypted1)131132 val msg1 = ByteArray(32 + encrypted1.size)133 ephemeral.publicKey.copyInto(msg1, 0)134 encrypted1.copyInto(msg1, 32)135 send(msg1)136137 val msg2 = receive()138 require(msg2.size >= 48) { "Noise NK: msg2 too short" }139 val remoteEphemeralPublic = msg2.copyOfRange(0, 32)140 val encrypted2 = msg2.copyOfRange(32, msg2.size)141142 h = mixHash(h, remoteEphemeralPublic)143 val ee = sharedKey(ephemeral.secretKey, remoteEphemeralPublic)144 val (ck2, k2) = hkdf2(ck, ee)145 ck = ck2146147 val decrypted2 = chachaOpen(k2, nonceBytes(0L), encrypted2, h)148 requireNotNull(decrypted2) { "Noise NK: msg2 auth failed — wrong responder key or MITM" }149 h = mixHash(h, encrypted2)150151 val (sendKey, recvKey) = hkdf2(ck, ByteArray(0))152 val sessionHash = h.copyOf()153154 var sendNonce = 0L155 var recvNonce = 0L156157 return object : NoiseNKSession {158 override fun encrypt(plaintext: ByteArray): ByteArray {159 return chachaSeal(sendKey, nonceBytes(sendNonce++), plaintext, ByteArray(0))160 }161162 override fun decrypt(ciphertext: ByteArray): ByteArray {163 return chachaOpen(recvKey, nonceBytes(recvNonce++), ciphertext, ByteArray(0))164 ?: throw IllegalStateException("Noise NK: decryption failed")165 }166167 override val handshakeHash: ByteArray = sessionHash168 }169}170171suspend fun noiseNKResponder(172 ourStaticPrivateKey: ByteArray,173 ourStaticPublicKey: ByteArray,174 send: suspend (ByteArray) -> Unit,175 receive: suspend () -> ByteArray,176): NoiseNKSession {177 var h = initHash()178 var ck = h.copyOf()179180 h = mixHash(h, ourStaticPublicKey)181182 val msg1 = receive()183 require(msg1.size >= 48) { "Noise NK: msg1 too short" }184 val remoteEphemeralPublic = msg1.copyOfRange(0, 32)185 val encrypted1 = msg1.copyOfRange(32, msg1.size)186187 h = mixHash(h, remoteEphemeralPublic)188 val es = sharedKey(ourStaticPrivateKey, remoteEphemeralPublic)189 val (ck1, k1) = hkdf2(ck, es)190 ck = ck1191192 val decrypted1 = chachaOpen(k1, nonceBytes(0L), encrypted1, h)193 requireNotNull(decrypted1) { "Noise NK: msg1 auth failed — wrong static key or MITM" }194 h = mixHash(h, encrypted1)195196 val ephemeral = generateKeyPair()197 h = mixHash(h, ephemeral.publicKey)198 val ee = sharedKey(ephemeral.secretKey, remoteEphemeralPublic)199 val (ck2, k2) = hkdf2(ck, ee)200 ck = ck2201202 val encrypted2 = chachaSeal(k2, nonceBytes(0L), ByteArray(0), h)203 h = mixHash(h, encrypted2)204205 val msg2 = ByteArray(32 + encrypted2.size)206 ephemeral.publicKey.copyInto(msg2, 0)207 encrypted2.copyInto(msg2, 32)208 send(msg2)209210 val (initiatorSendKey, responderSendKey) = hkdf2(ck, ByteArray(0))211 val sessionHash = h.copyOf()212213 var sendNonce = 0L214 var recvNonce = 0L215216 return object : NoiseNKSession {217 override fun encrypt(plaintext: ByteArray): ByteArray {218 return chachaSeal(responderSendKey, nonceBytes(sendNonce++), plaintext, ByteArray(0))219 }220221 override fun decrypt(ciphertext: ByteArray): ByteArray {222 return chachaOpen(initiatorSendKey, nonceBytes(recvNonce++), ciphertext, ByteArray(0))223 ?: throw IllegalStateException("Noise NK: decryption failed")224 }225226 override val handshakeHash: ByteArray = sessionHash227 }228}229230fun derivePairingSAS(handshakeHash: ByteArray): String {231 return SASDerivation.derive(handshakeHash)232}