1package com.peachpasswords.sync23import org.bouncycastle.crypto.digests.Blake2sDigest4import org.bouncycastle.crypto.generators.HKDFBytesGenerator5import org.bouncycastle.crypto.params.HKDFParameters6import org.bouncycastle.crypto.params.X25519PrivateKeyParameters7import org.bouncycastle.crypto.params.X25519PublicKeyParameters89private const val NOISE_NK_PROTOCOL_NAME = "Noise_NK_25519_ChaChaPoly_BLAKE2s"1011interface NoiseNKSession : AutoCloseable {12 fun encrypt(plaintext: ByteArray): ByteArray13 fun decrypt(ciphertext: ByteArray): ByteArray14 val handshakeHash: ByteArray1516 fun rekeyOutgoing() {17 throw UnsupportedOperationException("Noise session does not support rekeying")18 }1920 fun rekeyIncoming() {21 throw UnsupportedOperationException("Noise session does not support rekeying")22 }2324 override fun close() = Unit25}2627private fun initHash(): ByteArray {28 val nameBytes = NOISE_NK_PROTOCOL_NAME.encodeToByteArray()29 if (nameBytes.size <= 32) {30 return ByteArray(32).also { nameBytes.copyInto(it) }31 }32 return Blake2sDigest(256).run {33 update(nameBytes, 0, nameBytes.size)34 ByteArray(32).also { doFinal(it, 0) }35 }36}3738private fun hkdf2(chainingKey: ByteArray, input: ByteArray): Pair<ByteArray, ByteArray> {39 val generator = HKDFBytesGenerator(Blake2sDigest(256))40 generator.init(HKDFParameters(input, chainingKey, ByteArray(0)))41 val output = ByteArray(64)42 generator.generateBytes(output, 0, output.size)43 return output.copyOfRange(0, 32) to output.copyOfRange(32, 64)44}45464748internal fun noiseNKInitHashForTest(): ByteArray = initHash()49internal fun noiseNKHkdf2ForTest(50 chainingKey: ByteArray,51 input: ByteArray,52): Pair<ByteArray, ByteArray> = hkdf2(chainingKey, input)5354internal fun x25519SharedKey(privateKey: ByteArray, publicKey: ByteArray): ByteArray {55 require(privateKey.size == 32 && publicKey.size == 32) { "X25519 keys must be 32 bytes" }56 val private = X25519PrivateKeyParameters(privateKey, 0)57 val public = X25519PublicKeyParameters(publicKey, 0)58 val output = ByteArray(32)59 private.generateSecret(public, output, 0)60 require(output.any { it.toInt() != 0 }) { "X25519 low-order point rejected" }61 return output62}6364suspend fun noiseNKInitiator(65 responderStaticPublicKey: ByteArray,66 send: suspend (ByteArray) -> Unit,67 receive: suspend () -> ByteArray,68 ephemeralPrivateKeyForTest: ByteArray? = null,69): NoiseNKSession {70 val native = RustNoiseSession(71 if (ephemeralPrivateKeyForTest == null) {72 RustNoiseNative.nativeCreateNkInitiator(responderStaticPublicKey)73 } else {74 RustNoiseNative.nativeCreateNkInitiatorForTest(75 responderStaticPublicKey,76 ephemeralPrivateKeyForTest,77 )78 },79 )80 return try {81 send(native.writeHandshake())82 native.readHandshake(receive())83 native84 } catch (error: Throwable) {85 native.close()86 throw error87 }88}8990suspend fun noiseNKResponder(91 ourStaticPrivateKey: ByteArray,92 ourStaticPublicKey: ByteArray,93 send: suspend (ByteArray) -> Unit,94 receive: suspend () -> ByteArray,95 ephemeralPrivateKeyForTest: ByteArray? = null,96): NoiseNKSession {97 val native = RustNoiseSession(98 if (ephemeralPrivateKeyForTest == null) {99 RustNoiseNative.nativeCreateNkResponder(100 ourStaticPrivateKey,101 ourStaticPublicKey,102 )103 } else {104 RustNoiseNative.nativeCreateNkResponderForTest(105 ourStaticPrivateKey,106 ourStaticPublicKey,107 ephemeralPrivateKeyForTest,108 )109 },110 )111 return try {112 native.readHandshake(receive())113 send(native.writeHandshake())114 native115 } catch (error: Throwable) {116 native.close()117 throw error118 }119}120121fun derivePairingSAS(handshakeHash: ByteArray): String =122 SASDerivation.derive(handshakeHash)123