diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/ChaCha20Poly1305.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/ChaCha20Poly1305.scala index b71fe8d1ee..da93d4cd7a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/ChaCha20Poly1305.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/ChaCha20Poly1305.scala @@ -19,6 +19,7 @@ package fr.acinq.eclair.crypto import java.nio.ByteOrder import fr.acinq.bitcoin.{ByteVector32, Protocol} +import grizzled.slf4j.Logger import grizzled.slf4j.Logging import org.spongycastle.crypto.engines.ChaCha7539Engine import org.spongycastle.crypto.params.{KeyParameter, ParametersWithIV} @@ -31,8 +32,8 @@ import scodec.bits.ByteVector object Poly1305 { /** * - * @param key input key - * @param data input data + * @param key input key + * @param datas input data * @return a 16 byte authentication tag */ def mac(key: ByteVector, datas: ByteVector*): ByteVector = { @@ -50,6 +51,8 @@ object Poly1305 { * see https://tools.ietf.org/html/rfc7539#section-2.5 */ object ChaCha20 { + // Whenever key rotation happens, we start with a nonce value of 0 and increment it for each message. + val ZeroNonce = ByteVector.fill(12)(0.byteValue) def encrypt(plaintext: ByteVector, key: ByteVector, nonce: ByteVector, counter: Int = 0): ByteVector = { val engine = new ChaCha7539Engine() @@ -94,6 +97,11 @@ object ChaCha20 { */ object ChaCha20Poly1305 extends Logging { + // This logger is used to dump encryption keys to enable traffic analysis by the lightning-dissector. + // See https://github.com/nayutaco/lightning-dissector for more details. + // It is disabled by default (in the logback.xml configuration file). + val keyLogger = Logger("keylog") + /** * * @param key 32 bytes encryption key @@ -106,7 +114,12 @@ object ChaCha20Poly1305 extends Logging { val polykey = ChaCha20.encrypt(ByteVector32.Zeroes, key, nonce) val ciphertext = ChaCha20.encrypt(plaintext, key, nonce, 1) val tag = Poly1305.mac(polykey, aad, pad16(aad), ciphertext, pad16(ciphertext), Protocol.writeUInt64(aad.length, ByteOrder.LITTLE_ENDIAN), Protocol.writeUInt64(ciphertext.length, ByteOrder.LITTLE_ENDIAN)) + logger.debug(s"encrypt($key, $nonce, $aad, $plaintext) = ($ciphertext, $tag)") + if (nonce === ChaCha20.ZeroNonce) { + keyLogger.debug(s"${tag.toHex} ${key.toHex}") + } + (ciphertext, tag) } @@ -124,7 +137,12 @@ object ChaCha20Poly1305 extends Logging { val tag = Poly1305.mac(polykey, aad, pad16(aad), ciphertext, pad16(ciphertext), Protocol.writeUInt64(aad.length, ByteOrder.LITTLE_ENDIAN), Protocol.writeUInt64(ciphertext.length, ByteOrder.LITTLE_ENDIAN)) require(tag == mac, "invalid mac") val plaintext = ChaCha20.decrypt(ciphertext, key, nonce, 1) + logger.debug(s"decrypt($key, $nonce, $aad, $ciphertext, $mac) = $plaintext") + if (nonce === ChaCha20.ZeroNonce) { + keyLogger.debug(s"${mac.toHex} ${key.toHex}") + } + plaintext } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Noise.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Noise.scala index fb85cf0216..10470c062b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Noise.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Noise.scala @@ -82,7 +82,7 @@ object Noise { trait CipherFunctions { def name: String - //Encrypts plaintext using the cipher key k of 32 bytes and an 8-byte unsigned integer nonce n which must be unique + // Encrypts plaintext using the cipher key k of 32 bytes and an 8-byte unsigned integer nonce n which must be unique // for the key k. Returns the ciphertext. Encryption must be done with an "AEAD" encryption mode with the associated // data ad (using the terminology from [1]) and returns a ciphertext that is the same size as the plaintext // plus 16 bytes for authentication data. The entire ciphertext must be indistinguishable from random if the key is secret. @@ -99,7 +99,7 @@ object Noise { // as specified in BOLT #8 def nonce(n: Long): ByteVector = ByteVector.fill(4)(0) ++ Protocol.writeUInt64(n, ByteOrder.LITTLE_ENDIAN) - //Encrypts plaintext using the cipher key k of 32 bytes and an 8-byte unsigned integer nonce n which must be unique + // Encrypts plaintext using the cipher key k of 32 bytes and an 8-byte unsigned integer nonce n which must be unique. override def encrypt(k: ByteVector, n: Long, ad: ByteVector, plaintext: ByteVector): ByteVector = { val (ciphertext, mac) = ChaCha20Poly1305.encrypt(k, nonce(n), plaintext, ad) ciphertext ++ mac @@ -329,7 +329,7 @@ object Noise { * contain 2 cipherstates than can be used to encrypt/decrypt further communication */ def write(payload: ByteVector): (HandshakeStateReader, ByteVector, Option[(CipherState, CipherState, ByteVector)]) = { - require(!messages.isEmpty) + require(messages.nonEmpty) logger.debug(s"write($payload)") val (writer1, buffer1) = messages.head.foldLeft(this -> ByteVector.empty) { @@ -377,7 +377,7 @@ object Noise { * * @param message input message * @return a (writer, payload, Option[(cipherstate, cipherstate)] tuple. - * The payload contains the original payload used be the sender and a writer that will be used to create the + * The payload contains the original payload used by the sender and a writer that will be used to create the * next message. When the handshake is over (i.e. there are no more handshake patterns to process) the last item will * contain 2 cipherstates than can be used to encrypt/decrypt further communication */ @@ -433,12 +433,12 @@ object Noise { def initializeWriter(handshakePattern: HandshakePattern, prologue: ByteVector, s: KeyPair, e: KeyPair, rs: ByteVector, re: ByteVector, dh: DHFunctions, cipher: CipherFunctions, hash: HashFunctions, byteStream: ByteStream = RandomBytes): HandshakeStateWriter = { val symmetricState = makeSymmetricState(handshakePattern, prologue, dh, cipher, hash) - val symmetricState1 = (handshakePattern.initiatorPreMessages).foldLeft(symmetricState) { + val symmetricState1 = handshakePattern.initiatorPreMessages.foldLeft(symmetricState) { case (state, E) => state.mixHash(e.pub) case (state, S) => state.mixHash(s.pub) case _ => throw new RuntimeException("invalid pre-message") } - val symmetricState2 = (handshakePattern.responderPreMessages).foldLeft(symmetricState1) { + val symmetricState2 = handshakePattern.responderPreMessages.foldLeft(symmetricState1) { case (state, E) => state.mixHash(re) case (state, S) => state.mixHash(rs) case _ => throw new RuntimeException("invalid pre-message") diff --git a/eclair-node/src/main/resources/logback.xml b/eclair-node/src/main/resources/logback.xml index 59f3871af9..ae7ccbb8a7 100644 --- a/eclair-node/src/main/resources/logback.xml +++ b/eclair-node/src/main/resources/logback.xml @@ -39,6 +39,13 @@ + + ${eclair.datadir:-${user.home}/.eclair}/keys.log + + %msg%n + + + @@ -52,6 +59,16 @@ + + + + +