NoiseNK.kt

4.1 KB · raw

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}4546// Primitive-only compatibility seams. The production Noise transcript and47// transport state are exclusively owned by the shared Rust core.48internal 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