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 1 commit
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
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
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
12 changes: 5 additions & 7 deletions eclair-core/src/main/scala/fr/acinq/eclair/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,19 @@ package fr.acinq

import fr.acinq.bitcoin.Crypto.PrivateKey
import fr.acinq.bitcoin._
import fr.acinq.eclair.crypto.StrongRandom
import scodec.Attempt
import scodec.bits.{BitVector, ByteVector}

import java.security.SecureRandom
import scala.util.{Failure, Success, Try}

package object eclair {

/**
* 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/
*/
val secureRandom = new SecureRandom()
val randomGen = new StrongRandom()

def randomBytes(length: Int): ByteVector = {
val buffer = new Array[Byte](length)
secureRandom.nextBytes(buffer)
randomGen.nextBytes(buffer)
ByteVector.view(buffer)
}

Expand All @@ -44,6 +40,8 @@ package object eclair {

def randomKey: PrivateKey = PrivateKey(randomBytes32)

def randomLong: Long = randomGen.nextLong()

def toLongId(fundingTxHash: ByteVector32, fundingOutputIndex: Int): ByteVector32 = {
require(fundingOutputIndex < 65536, "fundingOutputIndex must not be greater than FFFF")
val channelId = ByteVector32(fundingTxHash.take(30) :+ (fundingTxHash(30) ^ (fundingOutputIndex >> 8)).toByte :+ (fundingTxHash(31) ^ fundingOutputIndex).toByte)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket
import fr.acinq.eclair.payment.{PaymentEvent, PaymentFailed, RemoteFailure}
import fr.acinq.eclair.router.{Announcements, Router}
import fr.acinq.eclair.wire.protocol.IncorrectOrUnknownPaymentDetails
import fr.acinq.eclair.{MilliSatoshiLong, NodeParams, randomBytes32, secureRandom}
import fr.acinq.eclair.{MilliSatoshiLong, NodeParams, randomBytes32, randomLong}

import scala.concurrent.duration._

Expand Down Expand Up @@ -96,7 +96,7 @@ object Autoprobe {
if (peers.isEmpty) {
None
} else {
peers.drop(secureRandom.nextInt(peers.size)).headOption
peers.drop(randomLong.toInt % peers.size).headOption
}
}

Expand Down
Loading