Skip to content

Commit 76894bd

Browse files
authored
Add additional PRNG (#1774)
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.
1 parent a658fa2 commit 76894bd

File tree

78 files changed

+1114
-730
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+1114
-730
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ trait Eclair {
111111

112112
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]]
113113

114-
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]
114+
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]
115115

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

eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ object NodeParams extends Logging {
152152
migrateSeedFile(oldSeedPath, seedPath)
153153
readSeedFromFile(seedPath)
154154
} else {
155-
val randomSeed = randomBytes32
155+
val randomSeed = randomBytes32()
156156
writeSeedToFile(seedPath, randomSeed)
157157
randomSeed.bytes
158158
}

eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala

+5-4
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,9 @@
1717
package fr.acinq.eclair
1818

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

8788
datadir.mkdirs()
8889
val config = system.settings.config.getConfig("eclair")

eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
498498
LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil),
499499
localNextHtlcId = 0L, remoteNextHtlcId = 0L,
500500
originChannels = Map.empty,
501-
remoteNextCommitInfo = Right(randomKey.publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array,
501+
remoteNextCommitInfo = Right(randomKey().publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array,
502502
commitInput, ShaChain.init, channelId = channelId)
503503
peer ! ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId) // we notify the peer asap so it knows how to route messages
504504
txPublisher ! SetChannelId(remoteNodeId, channelId)
@@ -541,7 +541,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
541541
LocalChanges(Nil, Nil, Nil), RemoteChanges(Nil, Nil, Nil),
542542
localNextHtlcId = 0L, remoteNextHtlcId = 0L,
543543
originChannels = Map.empty,
544-
remoteNextCommitInfo = Right(randomKey.publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array
544+
remoteNextCommitInfo = Right(randomKey().publicKey), // we will receive their next per-commitment point in the next message, so we temporarily put a random byte array
545545
commitInput, ShaChain.init, channelId = channelId)
546546
val now = System.currentTimeMillis.milliseconds.toSeconds
547547
context.system.eventStream.publish(ChannelSignatureReceived(self, commitments))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/*
2+
* Copyright 2021 ACINQ SAS
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package fr.acinq.eclair.crypto
18+
19+
import fr.acinq.bitcoin.Protocol
20+
import org.bouncycastle.crypto.digests.SHA256Digest
21+
import org.bouncycastle.crypto.engines.ChaCha7539Engine
22+
import org.bouncycastle.crypto.params.{KeyParameter, ParametersWithIV}
23+
24+
import java.lang.management.ManagementFactory
25+
import java.nio.ByteOrder
26+
import java.security.SecureRandom
27+
28+
/**
29+
* Created by t-bast on 19/04/2021.
30+
*/
31+
32+
sealed trait EntropyCollector {
33+
/** External components may inject additional entropy to be added to the entropy pool. */
34+
def addEntropy(entropy: Array[Byte]): Unit
35+
}
36+
37+
sealed trait RandomGenerator {
38+
// @formatter:off
39+
def nextBytes(bytes: Array[Byte]): Unit
40+
def nextLong(): Long
41+
// @formatter:on
42+
}
43+
44+
sealed trait RandomGeneratorWithInit extends RandomGenerator {
45+
def init(): Unit
46+
}
47+
48+
/**
49+
* A weak pseudo-random number generator that regularly samples a few entropy sources to build a hash chain.
50+
* This should never be used alone but can be xor-ed with the OS random number generator in case it completely breaks.
51+
*/
52+
private class WeakRandom() extends RandomGenerator {
53+
54+
private val stream = new ChaCha7539Engine()
55+
private val seed = new Array[Byte](32)
56+
private var lastByte: Byte = 0
57+
private var opsSinceLastSample: Int = 0
58+
59+
private val memoryMXBean = ManagementFactory.getMemoryMXBean
60+
private val runtimeMXBean = ManagementFactory.getRuntimeMXBean
61+
private val threadMXBean = ManagementFactory.getThreadMXBean
62+
63+
// sample some initial entropy
64+
sampleEntropy()
65+
66+
private def feedDigest(sha: SHA256Digest, i: Int): Unit = {
67+
sha.update(i.toByte)
68+
sha.update((i >> 8).toByte)
69+
sha.update((i >> 16).toByte)
70+
sha.update((i >> 24).toByte)
71+
}
72+
73+
private def feedDigest(sha: SHA256Digest, l: Long): Unit = {
74+
sha.update(l.toByte)
75+
sha.update((l >> 8).toByte)
76+
sha.update((l >> 16).toByte)
77+
sha.update((l >> 24).toByte)
78+
sha.update((l >> 32).toByte)
79+
sha.update((l >> 40).toByte)
80+
}
81+
82+
/** The entropy pool is regularly enriched with newly sampled entropy. */
83+
private def sampleEntropy(): Unit = {
84+
opsSinceLastSample = 0
85+
86+
val sha = new SHA256Digest()
87+
sha.update(seed, 0, 32)
88+
feedDigest(sha, System.currentTimeMillis())
89+
feedDigest(sha, System.identityHashCode(new Array[Int](1)))
90+
feedDigest(sha, memoryMXBean.getHeapMemoryUsage.getUsed)
91+
feedDigest(sha, memoryMXBean.getNonHeapMemoryUsage.getUsed)
92+
feedDigest(sha, runtimeMXBean.getPid)
93+
feedDigest(sha, runtimeMXBean.getUptime)
94+
feedDigest(sha, threadMXBean.getCurrentThreadCpuTime)
95+
feedDigest(sha, threadMXBean.getCurrentThreadUserTime)
96+
feedDigest(sha, threadMXBean.getPeakThreadCount)
97+
98+
sha.doFinal(seed, 0)
99+
// NB: init internally resets the engine, no need to reset it explicitly ourselves.
100+
stream.init(true, new ParametersWithIV(new KeyParameter(seed), new Array[Byte](12)))
101+
}
102+
103+
/** We sample new entropy approximately every 32 operations and at most every 64 operations. */
104+
private def shouldSample(): Boolean = {
105+
opsSinceLastSample += 1
106+
val condition1 = -4 <= lastByte && lastByte <= 4
107+
val condition2 = opsSinceLastSample >= 64
108+
condition1 || condition2
109+
}
110+
111+
def addEntropy(entropy: Array[Byte]): Unit = synchronized {
112+
if (entropy.nonEmpty) {
113+
val sha = new SHA256Digest()
114+
sha.update(seed, 0, 32)
115+
sha.update(entropy, 0, entropy.length)
116+
sha.doFinal(seed, 0)
117+
// NB: init internally resets the engine, no need to reset it explicitly ourselves.
118+
stream.init(true, new ParametersWithIV(new KeyParameter(seed), new Array[Byte](12)))
119+
}
120+
}
121+
122+
def nextBytes(bytes: Array[Byte]): Unit = synchronized {
123+
if (shouldSample()) {
124+
sampleEntropy()
125+
}
126+
stream.processBytes(bytes, 0, bytes.length, bytes, 0)
127+
lastByte = bytes.last
128+
}
129+
130+
def nextLong(): Long = {
131+
val bytes = new Array[Byte](8)
132+
nextBytes(bytes)
133+
Protocol.uint64(bytes, ByteOrder.BIG_ENDIAN)
134+
}
135+
136+
}
137+
138+
class StrongRandom() extends RandomGeneratorWithInit with EntropyCollector {
139+
140+
/**
141+
* We are using 'new SecureRandom()' instead of 'SecureRandom.getInstanceStrong()' because the latter can hang on Linux
142+
* See http://bugs.java.com/view_bug.do?bug_id=6521844 and https://tersesystems.com/2015/12/17/the-right-way-to-use-securerandom/
143+
*/
144+
private val secureRandom = new SecureRandom()
145+
146+
/**
147+
* We're using an additional, weaker randomness source to protect against catastrophic failures of the SecureRandom
148+
* instance.
149+
*/
150+
private val weakRandom = new WeakRandom()
151+
152+
override def init(): Unit = {
153+
// this will force the secure random instance to initialize itself right now, making sure it doesn't hang later
154+
secureRandom.nextInt()
155+
}
156+
157+
override def addEntropy(entropy: Array[Byte]): Unit = {
158+
weakRandom.addEntropy(entropy)
159+
}
160+
161+
override def nextBytes(bytes: Array[Byte]): Unit = {
162+
secureRandom.nextBytes(bytes)
163+
val buffer = new Array[Byte](bytes.length)
164+
weakRandom.nextBytes(buffer)
165+
for (i <- bytes.indices) {
166+
bytes(i) = (bytes(i) ^ buffer(i)).toByte
167+
}
168+
}
169+
170+
override def nextLong(): Long = secureRandom.nextLong() ^ weakRandom.nextLong()
171+
172+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright 2021 ACINQ SAS
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package fr.acinq.eclair.crypto
18+
19+
import akka.actor.typed.Behavior
20+
import akka.actor.typed.eventstream.EventStream
21+
import akka.actor.typed.scaladsl.Behaviors
22+
import fr.acinq.bitcoin.Crypto.PublicKey
23+
import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Crypto}
24+
import fr.acinq.eclair.blockchain.NewBlock
25+
import fr.acinq.eclair.channel.ChannelSignatureReceived
26+
import fr.acinq.eclair.io.PeerConnected
27+
import fr.acinq.eclair.payment.ChannelPaymentRelayed
28+
import fr.acinq.eclair.router.NodeUpdated
29+
import scodec.bits.ByteVector
30+
31+
import scala.concurrent.duration.DurationInt
32+
33+
/**
34+
* Created by t-bast on 20/04/2021.
35+
*/
36+
37+
/**
38+
* This actor gathers entropy from several events and from the runtime, and regularly injects it into our [[WeakRandom]]
39+
* instance.
40+
*
41+
* Note that this isn't a strong entropy pool and shouldn't be trusted on its own but rather used as a safeguard against
42+
* failures in [[java.security.SecureRandom]].
43+
*/
44+
object WeakEntropyPool {
45+
46+
// @formatter:off
47+
sealed trait Command
48+
private case object FlushEntropy extends Command
49+
private case class WrappedNewBlock(block: Block) extends Command
50+
private case class WrappedPaymentRelayed(paymentHash: ByteVector32, relayedAt: Long) extends Command
51+
private case class WrappedPeerConnected(nodeId: PublicKey) extends Command
52+
private case class WrappedChannelSignature(wtxid: ByteVector32) extends Command
53+
private case class WrappedNodeUpdated(sig: ByteVector64) extends Command
54+
// @formatter:on
55+
56+
def apply(collector: EntropyCollector): Behavior[Command] = {
57+
Behaviors.setup { context =>
58+
context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[NewBlock](e => WrappedNewBlock(e.block)))
59+
context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[ChannelPaymentRelayed](e => WrappedPaymentRelayed(e.paymentHash, e.timestamp)))
60+
context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[PeerConnected](e => WrappedPeerConnected(e.nodeId)))
61+
context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[NodeUpdated](e => WrappedNodeUpdated(e.ann.signature)))
62+
context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[ChannelSignatureReceived](e => WrappedChannelSignature(e.commitments.localCommit.publishableTxs.commitTx.tx.wtxid)))
63+
Behaviors.withTimers { timers =>
64+
timers.startTimerWithFixedDelay(FlushEntropy, 30 seconds)
65+
collecting(collector, None)
66+
}
67+
}
68+
}
69+
70+
private def collecting(collector: EntropyCollector, entropy_opt: Option[ByteVector32]): Behavior[Command] = {
71+
Behaviors.receiveMessage {
72+
case FlushEntropy =>
73+
entropy_opt match {
74+
case Some(entropy) =>
75+
collector.addEntropy(entropy.toArray)
76+
collecting(collector, None)
77+
case None =>
78+
Behaviors.same
79+
}
80+
81+
case WrappedNewBlock(block) => collecting(collector, collect(entropy_opt, block.hash ++ ByteVector.fromLong(System.currentTimeMillis())))
82+
83+
case WrappedPaymentRelayed(paymentHash, relayedAt) => collecting(collector, collect(entropy_opt, paymentHash ++ ByteVector.fromLong(relayedAt)))
84+
85+
case WrappedPeerConnected(nodeId) => collecting(collector, collect(entropy_opt, nodeId.value ++ ByteVector.fromLong(System.currentTimeMillis())))
86+
87+
case WrappedNodeUpdated(sig) => collecting(collector, collect(entropy_opt, sig ++ ByteVector.fromLong(System.currentTimeMillis())))
88+
89+
case WrappedChannelSignature(wtxid) => collecting(collector, collect(entropy_opt, wtxid ++ ByteVector.fromLong(System.currentTimeMillis())))
90+
}
91+
}
92+
93+
private def collect(entropy_opt: Option[ByteVector32], additional: ByteVector): Option[ByteVector32] = {
94+
Some(Crypto.sha256(entropy_opt.map(_.bytes).getOrElse(ByteVector.empty) ++ additional))
95+
}
96+
97+
}

eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import fr.acinq.eclair.crypto.Monitoring.{Metrics, Tags}
2525
import fr.acinq.eclair.router.Announcements
2626
import fr.acinq.eclair.transactions.Transactions
2727
import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, TransactionWithInputInfo, TxOwner}
28-
import fr.acinq.eclair.{KamonExt, secureRandom}
28+
import fr.acinq.eclair.{KamonExt, randomLong}
2929
import grizzled.slf4j.Logging
3030
import kamon.tag.TagSet
3131
import scodec.bits.ByteVector
@@ -75,7 +75,7 @@ class LocalChannelKeyManager(seed: ByteVector, chainHash: ByteVector32) extends
7575
override def newFundingKeyPath(isFunder: Boolean): KeyPath = {
7676
val last = DeterministicWallet.hardened(if (isFunder) 1 else 0)
7777

78-
def next(): Long = secureRandom.nextInt() & 0xFFFFFFFFL
78+
def next(): Long = randomLong() & 0xFFFFFFFFL
7979

8080
DeterministicWallet.KeyPath(Seq(next(), next(), next(), next(), next(), next(), next(), next(), last))
8181
}

eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: EclairWa
128128
val channelVersion = ChannelVersion.pickChannelVersion(d.localFeatures, d.remoteFeatures)
129129
val (channel, localParams) = createNewChannel(nodeParams, d.localFeatures, funder = true, c.fundingSatoshis, origin_opt = Some(sender), channelVersion)
130130
c.timeout_opt.map(openTimeout => context.system.scheduler.scheduleOnce(openTimeout.duration, channel, Channel.TickChannelOpenTimeout)(context.dispatcher))
131-
val temporaryChannelId = randomBytes32
131+
val temporaryChannelId = randomBytes32()
132132
val channelFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, channelVersion, c.fundingSatoshis, None)
133133
val fundingTxFeeratePerKw = c.fundingTxFeeratePerKw_opt.getOrElse(nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget))
134134
log.info(s"requesting a new channel with fundingSatoshis=${c.fundingSatoshis}, pushMsat=${c.pushMsat} and fundingFeeratePerByte=${c.fundingTxFeeratePerKw_opt} temporaryChannelId=$temporaryChannelId localParams=$localParams")

0 commit comments

Comments
 (0)