Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add additional PRNG #1774

Merged
merged 3 commits into from
May 19, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading