Skip to content

Commit

Permalink
Add additional PRNG (#1774)
Browse files Browse the repository at this point in the history
In case of catastrophic failures of the `SecureRandom` instance, we add
a secondary randomness source that we mix into the random stream.

This is a somewhat weak random source and should not be used on its own,
but it doesn't hurt to xor it with the output of `SecureRandom`.

We use an actor that listens to events in the system and inject them
in our weak pseudo-RNG.
  • Loading branch information
t-bast committed May 19, 2021
1 parent a658fa2 commit 76894bd
Show file tree
Hide file tree
Showing 78 changed files with 1,114 additions and 730 deletions.
2 changes: 1 addition & 1 deletion eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ trait Eclair {

def sendBlocking(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, invoice_opt: Option[PaymentRequest] = None, maxAttempts_opt: Option[Int] = None, feeThresholdSat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None)(implicit timeout: Timeout): Future[Either[PreimageReceived, PaymentEvent]]

def sendWithPreimage(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentPreimage: ByteVector32 = randomBytes32, maxAttempts_opt: Option[Int] = None, feeThresholdSat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None)(implicit timeout: Timeout): Future[UUID]
def sendWithPreimage(externalId_opt: Option[String], recipientNodeId: PublicKey, amount: MilliSatoshi, paymentPreimage: ByteVector32 = randomBytes32(), maxAttempts_opt: Option[Int] = None, feeThresholdSat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None)(implicit timeout: Timeout): Future[UUID]

def sentInfo(id: Either[UUID, ByteVector32])(implicit timeout: Timeout): Future[Seq[OutgoingPayment]]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ object NodeParams extends Logging {
migrateSeedFile(oldSeedPath, seedPath)
readSeedFromFile(seedPath)
} else {
val randomSeed = randomBytes32
val randomSeed = randomBytes32()
writeSeedToFile(seedPath, randomSeed)
randomSeed.bytes
}
Expand Down
9 changes: 5 additions & 4 deletions eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,9 @@
package fr.acinq.eclair

import akka.Done
import akka.actor.typed
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.scaladsl.adapter.ClassicActorSystemOps
import akka.actor.{ActorRef, ActorSystem, Props, SupervisorStrategy}
import akka.actor.{ActorRef, ActorSystem, Props, SupervisorStrategy, typed}
import akka.pattern.after
import akka.util.Timeout
import com.softwaremill.sttp.okhttp.OkHttpFutureBackend
Expand All @@ -32,6 +31,7 @@ import fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor
import fr.acinq.eclair.blockchain.bitcoind.{BitcoinCoreWallet, ZmqWatcher}
import fr.acinq.eclair.blockchain.fee._
import fr.acinq.eclair.channel.{Channel, Register}
import fr.acinq.eclair.crypto.WeakEntropyPool
import fr.acinq.eclair.crypto.keymanager.{LocalChannelKeyManager, LocalNodeKeyManager}
import fr.acinq.eclair.db.Databases.FileBackup
import fr.acinq.eclair.db.{Databases, DbEventHandler, FileBackupHandler}
Expand Down Expand Up @@ -81,8 +81,9 @@ class Setup(datadir: File,
logger.info(s"version=${Kit.getVersion} commit=${Kit.getCommit}")
logger.info(s"datadir=${datadir.getCanonicalPath}")
logger.info(s"initializing secure random generator")
// this will force the secure random instance to initialize itself right now, making sure it doesn't hang later (see comment in package.scala)
secureRandom.nextInt()
// this will force the secure random instance to initialize itself right now, making sure it doesn't hang later
randomGen.init()
system.spawn(Behaviors.supervise(WeakEntropyPool(randomGen)).onFailure(typed.SupervisorStrategy.restart), "entropy-pool")

datadir.mkdirs()
val config = system.settings.config.getConfig("eclair")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil),
localNextHtlcId = 0L, remoteNextHtlcId = 0L,
originChannels = Map.empty,
remoteNextCommitInfo = Right(randomKey.publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array,
remoteNextCommitInfo = Right(randomKey().publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array,
commitInput, ShaChain.init, channelId = channelId)
peer ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages
txPublisher ! SetChannelId(remoteNodeId, channelId)
Expand Down Expand Up @@ -541,7 +541,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil),
localNextHtlcId = 0L, remoteNextHtlcId = 0L,
originChannels = Map.empty,
remoteNextCommitInfo = Right(randomKey.publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array
remoteNextCommitInfo = Right(randomKey().publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array
commitInput, ShaChain.init, channelId = channelId)
val now = System.currentTimeMillis.milliseconds.toSeconds
context.system.eventStream.publish(ChannelSignatureReceived(self, commitments))
Expand Down
172 changes: 172 additions & 0 deletions eclair-core/src/main/scala/fr/acinq/eclair/crypto/Random.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/*
* Copyright 2021 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package fr.acinq.eclair.crypto

import fr.acinq.bitcoin.Protocol
import org.bouncycastle.crypto.digests.SHA256Digest
import org.bouncycastle.crypto.engines.ChaCha7539Engine
import org.bouncycastle.crypto.params.{KeyParameter, ParametersWithIV}

import java.lang.management.ManagementFactory
import java.nio.ByteOrder
import java.security.SecureRandom

/**
* Created by t-bast on 19/04/2021.
*/

sealed trait EntropyCollector {
/** External components may inject additional entropy to be added to the entropy pool. */
def addEntropy(entropy: Array[Byte]): Unit
}

sealed trait RandomGenerator {
// @formatter:off
def nextBytes(bytes: Array[Byte]): Unit
def nextLong(): Long
// @formatter:on
}

sealed trait RandomGeneratorWithInit extends RandomGenerator {
def init(): Unit
}

/**
* A weak pseudo-random number generator that regularly samples a few entropy sources to build a hash chain.
* This should never be used alone but can be xor-ed with the OS random number generator in case it completely breaks.
*/
private class WeakRandom() extends RandomGenerator {

private val stream = new ChaCha7539Engine()
private val seed = new Array[Byte](32)
private var lastByte: Byte = 0
private var opsSinceLastSample: Int = 0

private val memoryMXBean = ManagementFactory.getMemoryMXBean
private val runtimeMXBean = ManagementFactory.getRuntimeMXBean
private val threadMXBean = ManagementFactory.getThreadMXBean

// sample some initial entropy
sampleEntropy()

private def feedDigest(sha: SHA256Digest, i: Int): Unit = {
sha.update(i.toByte)
sha.update((i >> 8).toByte)
sha.update((i >> 16).toByte)
sha.update((i >> 24).toByte)
}

private def feedDigest(sha: SHA256Digest, l: Long): Unit = {
sha.update(l.toByte)
sha.update((l >> 8).toByte)
sha.update((l >> 16).toByte)
sha.update((l >> 24).toByte)
sha.update((l >> 32).toByte)
sha.update((l >> 40).toByte)
}

/** The entropy pool is regularly enriched with newly sampled entropy. */
private def sampleEntropy(): Unit = {
opsSinceLastSample = 0

val sha = new SHA256Digest()
sha.update(seed, 0, 32)
feedDigest(sha, System.currentTimeMillis())
feedDigest(sha, System.identityHashCode(new Array[Int](1)))
feedDigest(sha, memoryMXBean.getHeapMemoryUsage.getUsed)
feedDigest(sha, memoryMXBean.getNonHeapMemoryUsage.getUsed)
feedDigest(sha, runtimeMXBean.getPid)
feedDigest(sha, runtimeMXBean.getUptime)
feedDigest(sha, threadMXBean.getCurrentThreadCpuTime)
feedDigest(sha, threadMXBean.getCurrentThreadUserTime)
feedDigest(sha, threadMXBean.getPeakThreadCount)

sha.doFinal(seed, 0)
// NB: init internally resets the engine, no need to reset it explicitly ourselves.
stream.init(true, new ParametersWithIV(new KeyParameter(seed), new Array[Byte](12)))
}

/** We sample new entropy approximately every 32 operations and at most every 64 operations. */
private def shouldSample(): Boolean = {
opsSinceLastSample += 1
val condition1 = -4 <= lastByte && lastByte <= 4
val condition2 = opsSinceLastSample >= 64
condition1 || condition2
}

def addEntropy(entropy: Array[Byte]): Unit = synchronized {
if (entropy.nonEmpty) {
val sha = new SHA256Digest()
sha.update(seed, 0, 32)
sha.update(entropy, 0, entropy.length)
sha.doFinal(seed, 0)
// NB: init internally resets the engine, no need to reset it explicitly ourselves.
stream.init(true, new ParametersWithIV(new KeyParameter(seed), new Array[Byte](12)))
}
}

def nextBytes(bytes: Array[Byte]): Unit = synchronized {
if (shouldSample()) {
sampleEntropy()
}
stream.processBytes(bytes, 0, bytes.length, bytes, 0)
lastByte = bytes.last
}

def nextLong(): Long = {
val bytes = new Array[Byte](8)
nextBytes(bytes)
Protocol.uint64(bytes, ByteOrder.BIG_ENDIAN)
}

}

class StrongRandom() extends RandomGeneratorWithInit with EntropyCollector {

/**
* We are using 'new SecureRandom()' instead of 'SecureRandom.getInstanceStrong()' because the latter can hang on Linux
* See http://bugs.java.com/view_bug.do?bug_id=6521844 and https://tersesystems.com/2015/12/17/the-right-way-to-use-securerandom/
*/
private val secureRandom = new SecureRandom()

/**
* We're using an additional, weaker randomness source to protect against catastrophic failures of the SecureRandom
* instance.
*/
private val weakRandom = new WeakRandom()

override def init(): Unit = {
// this will force the secure random instance to initialize itself right now, making sure it doesn't hang later
secureRandom.nextInt()
}

override def addEntropy(entropy: Array[Byte]): Unit = {
weakRandom.addEntropy(entropy)
}

override def nextBytes(bytes: Array[Byte]): Unit = {
secureRandom.nextBytes(bytes)
val buffer = new Array[Byte](bytes.length)
weakRandom.nextBytes(buffer)
for (i <- bytes.indices) {
bytes(i) = (bytes(i) ^ buffer(i)).toByte
}
}

override def nextLong(): Long = secureRandom.nextLong() ^ weakRandom.nextLong()

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Copyright 2021 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package fr.acinq.eclair.crypto

import akka.actor.typed.Behavior
import akka.actor.typed.eventstream.EventStream
import akka.actor.typed.scaladsl.Behaviors
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Crypto}
import fr.acinq.eclair.blockchain.NewBlock
import fr.acinq.eclair.channel.ChannelSignatureReceived
import fr.acinq.eclair.io.PeerConnected
import fr.acinq.eclair.payment.ChannelPaymentRelayed
import fr.acinq.eclair.router.NodeUpdated
import scodec.bits.ByteVector

import scala.concurrent.duration.DurationInt

/**
* Created by t-bast on 20/04/2021.
*/

/**
* This actor gathers entropy from several events and from the runtime, and regularly injects it into our [[WeakRandom]]
* instance.
*
* Note that this isn't a strong entropy pool and shouldn't be trusted on its own but rather used as a safeguard against
* failures in [[java.security.SecureRandom]].
*/
object WeakEntropyPool {

// @formatter:off
sealed trait Command
private case object FlushEntropy extends Command
private case class WrappedNewBlock(block: Block) extends Command
private case class WrappedPaymentRelayed(paymentHash: ByteVector32, relayedAt: Long) extends Command
private case class WrappedPeerConnected(nodeId: PublicKey) extends Command
private case class WrappedChannelSignature(wtxid: ByteVector32) extends Command
private case class WrappedNodeUpdated(sig: ByteVector64) extends Command
// @formatter:on

def apply(collector: EntropyCollector): Behavior[Command] = {
Behaviors.setup { context =>
context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[NewBlock](e => WrappedNewBlock(e.block)))
context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[ChannelPaymentRelayed](e => WrappedPaymentRelayed(e.paymentHash, e.timestamp)))
context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[PeerConnected](e => WrappedPeerConnected(e.nodeId)))
context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[NodeUpdated](e => WrappedNodeUpdated(e.ann.signature)))
context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[ChannelSignatureReceived](e => WrappedChannelSignature(e.commitments.localCommit.publishableTxs.commitTx.tx.wtxid)))
Behaviors.withTimers { timers =>
timers.startTimerWithFixedDelay(FlushEntropy, 30 seconds)
collecting(collector, None)
}
}
}

private def collecting(collector: EntropyCollector, entropy_opt: Option[ByteVector32]): Behavior[Command] = {
Behaviors.receiveMessage {
case FlushEntropy =>
entropy_opt match {
case Some(entropy) =>
collector.addEntropy(entropy.toArray)
collecting(collector, None)
case None =>
Behaviors.same
}

case WrappedNewBlock(block) => collecting(collector, collect(entropy_opt, block.hash ++ ByteVector.fromLong(System.currentTimeMillis())))

case WrappedPaymentRelayed(paymentHash, relayedAt) => collecting(collector, collect(entropy_opt, paymentHash ++ ByteVector.fromLong(relayedAt)))

case WrappedPeerConnected(nodeId) => collecting(collector, collect(entropy_opt, nodeId.value ++ ByteVector.fromLong(System.currentTimeMillis())))

case WrappedNodeUpdated(sig) => collecting(collector, collect(entropy_opt, sig ++ ByteVector.fromLong(System.currentTimeMillis())))

case WrappedChannelSignature(wtxid) => collecting(collector, collect(entropy_opt, wtxid ++ ByteVector.fromLong(System.currentTimeMillis())))
}
}

private def collect(entropy_opt: Option[ByteVector32], additional: ByteVector): Option[ByteVector32] = {
Some(Crypto.sha256(entropy_opt.map(_.bytes).getOrElse(ByteVector.empty) ++ additional))
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import fr.acinq.eclair.crypto.Monitoring.{Metrics, Tags}
import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.transactions.Transactions
import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, TransactionWithInputInfo, TxOwner}
import fr.acinq.eclair.{KamonExt, secureRandom}
import fr.acinq.eclair.{KamonExt, randomLong}
import grizzled.slf4j.Logging
import kamon.tag.TagSet
import scodec.bits.ByteVector
Expand Down Expand Up @@ -75,7 +75,7 @@ class LocalChannelKeyManager(seed: ByteVector, chainHash: ByteVector32) extends
override def newFundingKeyPath(isFunder: Boolean): KeyPath = {
val last = DeterministicWallet.hardened(if (isFunder) 1 else 0)

def next(): Long = secureRandom.nextInt() & 0xFFFFFFFFL
def next(): Long = randomLong() & 0xFFFFFFFFL

DeterministicWallet.KeyPath(Seq(next(), next(), next(), next(), next(), next(), next(), next(), last))
}
Expand Down
2 changes: 1 addition & 1 deletion eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: EclairWa
val channelVersion = ChannelVersion.pickChannelVersion(d.localFeatures, d.remoteFeatures)
val (channel, localParams) = createNewChannel(nodeParams, d.localFeatures, funder = true, c.fundingSatoshis, origin_opt = Some(sender), channelVersion)
c.timeout_opt.map(openTimeout => context.system.scheduler.scheduleOnce(openTimeout.duration, channel, Channel.TickChannelOpenTimeout)(context.dispatcher))
val temporaryChannelId = randomBytes32
val temporaryChannelId = randomBytes32()
val channelFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, channelVersion, c.fundingSatoshis, None)
val fundingTxFeeratePerKw = c.fundingTxFeeratePerKw_opt.getOrElse(nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget))
log.info(s"requesting a new channel with fundingSatoshis=${c.fundingSatoshis}, pushMsat=${c.pushMsat} and fundingFeeratePerByte=${c.fundingTxFeeratePerKw_opt} temporaryChannelId=$temporaryChannelId localParams=$localParams")
Expand Down

0 comments on commit 76894bd

Please sign in to comment.