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
+ }
0 commit comments