1 /*
  2  * Copyright (c) 2003, 2024, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.  Oracle designates this
  8  * particular file as subject to the "Classpath" exception as provided
  9  * by Oracle in the LICENSE file that accompanied this code.
 10  *
 11  * This code is distributed in the hope that it will be useful, but WITHOUT
 12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 14  * version 2 for more details (a copy is included in the LICENSE file that
 15  * accompanied this code).
 16  *
 17  * You should have received a copy of the GNU General Public License version
 18  * 2 along with this work; if not, write to the Free Software Foundation,
 19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 20  *
 21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 22  * or visit www.oracle.com if you need additional information or have any
 23  * questions.
 24  */
 25 
 26 package sun.security.provider;
 27 
 28 import java.io.*;
 29 import java.net.*;
 30 import java.security.*;
 31 import java.util.Arrays;
 32 
 33 import sun.security.util.Debug;
 34 
 35 /**
 36  * Native PRNG implementation for Linux/MacOS.
 37  * <p>
 38  * It obtains seed and random numbers by reading system files such as
 39  * the special device files /dev/random and /dev/urandom.  This
 40  * implementation respects the {@code securerandom.source} Security
 41  * property and {@code java.security.egd} System property for obtaining
 42  * seed material.  If the file specified by the properties does not
 43  * exist, /dev/random is the default seed source.  /dev/urandom is
 44  * the default source of random numbers.
 45  * <p>
 46  * On some Unix platforms, /dev/random may block until enough entropy is
 47  * available, but that may negatively impact the perceived startup
 48  * time.  By selecting these sources, this implementation tries to
 49  * strike a balance between performance and security.
 50  * <p>
 51  * generateSeed() and setSeed() attempt to directly read/write to the seed
 52  * source. However, this file may only be writable by root in many
 53  * configurations. Because we cannot just ignore bytes specified via
 54  * setSeed(), we keep a SHA1PRNG around in parallel.
 55  * <p>
 56  * nextBytes() reads the bytes directly from the source of random
 57  * numbers (and then mixes them with bytes from the SHA1PRNG for the
 58  * reasons explained above). Reading bytes from the random generator means
 59  * that we are generally getting entropy from the operating system. This
 60  * is a notable advantage over the SHA1PRNG model, which acquires
 61  * entropy only initially during startup although the VM may be running
 62  * for months.
 63  * <p>
 64  * Also note for nextBytes() that we do not need any initial pure random
 65  * seed from /dev/random. This is an advantage because on some versions
 66  * of Linux entropy can be exhausted very quickly and could thus impact
 67  * startup time.
 68  * <p>
 69  * Finally, note that we use a singleton for the actual work (RandomIO)
 70  * to avoid having to open and close /dev/[u]random constantly. However,
 71  * there may be many NativePRNG instances created by the JCA framework.
 72  *
 73  * @since   1.5
 74  * @author  Andreas Sterbenz
 75  */
 76 public final class NativePRNG extends SecureRandomSpi {
 77 
 78     private static final long serialVersionUID = -6599091113397072932L;
 79 
 80     private static final Debug debug = Debug.getInstance("provider");
 81 
 82     // name of the pure random file (also used for setSeed())
 83     private static final String NAME_RANDOM = "/dev/random";
 84     // name of the pseudo random file
 85     private static final String NAME_URANDOM = "/dev/urandom";
 86 
 87     // which kind of RandomIO object are we creating?
 88     private enum Variant {
 89         MIXED, BLOCKING, NONBLOCKING
 90     }
 91 
 92     // singleton instance or null if not available
 93     private static final RandomIO INSTANCE = initIO(Variant.MIXED);
 94 
 95     /**
 96      * Get the System egd source (if defined).  We only allow "file:"
 97      * URLs for now. If there is a egd value, parse it.
 98      *
 99      * @return the URL or null if not available.
100      */
101     private static URL getEgdUrl() {
102         // This will return "" if nothing was set.
103         String egdSource = SunEntries.getSeedSource();
104         URL egdUrl;
105 
106         if (egdSource.length() != 0) {
107             if (debug != null) {
108                 debug.println("NativePRNG egdUrl: " + egdSource);
109             }
110             try {
111                 @SuppressWarnings("deprecation")
112                 var _unused = egdUrl = new URL(egdSource);
113                 if (!egdUrl.getProtocol().equalsIgnoreCase("file")) {
114                     return null;
115                 }
116             } catch (MalformedURLException e) {
117                 return null;
118             }
119         } else {
120             egdUrl = null;
121         }
122 
123         return egdUrl;
124     }
125 
126     /**
127      * Create a RandomIO object for all I/O of this Variant type.
128      */
129     @SuppressWarnings("removal")
130     private static RandomIO initIO(final Variant v) {
131         return AccessController.doPrivileged(
132             new PrivilegedAction<>() {
133                 @Override
134                 public RandomIO run() {
135 
136                     File seedFile;
137                     File nextFile;
138 
139                     switch(v) {
140                     case MIXED:
141                         URL egdUrl;
142                         File egdFile = null;
143 
144                         if ((egdUrl = getEgdUrl()) != null) {
145                             try {
146                                 egdFile = SunEntries.getDeviceFile(egdUrl);
147                             } catch (IOException e) {
148                                 // Swallow, seedFile is still null
149                             }
150                         }
151 
152                         // Try egd first.
153                         if ((egdFile != null) && egdFile.canRead()) {
154                             seedFile = egdFile;
155                         } else {
156                             // fall back to /dev/random.
157                             seedFile = new File(NAME_RANDOM);
158                         }
159                         nextFile = new File(NAME_URANDOM);
160                         break;
161 
162                     case BLOCKING:
163                         seedFile = new File(NAME_RANDOM);
164                         nextFile = new File(NAME_RANDOM);
165                         break;
166 
167                     case NONBLOCKING:
168                         seedFile = new File(NAME_URANDOM);
169                         nextFile = new File(NAME_URANDOM);
170                         break;
171 
172                     default:
173                         // Shouldn't happen!
174                         return null;
175                     }
176 
177                     if (debug != null) {
178                         debug.println("NativePRNG." + v +
179                             " seedFile: " + seedFile +
180                             " nextFile: " + nextFile);
181                     }
182 
183                     if (!seedFile.canRead() || !nextFile.canRead()) {
184                         if (debug != null) {
185                             debug.println("NativePRNG." + v +
186                                 " Couldn't read Files.");
187                         }
188                         return null;
189                     }
190 
191                     try {
192                         return new RandomIO(seedFile, nextFile);
193                     } catch (Exception e) {
194                         return null;
195                     }
196                 }
197         });
198     }
199 
200     // return whether the NativePRNG is available
201     static boolean isAvailable() {
202         return INSTANCE != null;
203     }
204 
205     // constructor, called by the JCA framework
206     public NativePRNG(SecureRandomParameters params) {
207         if (INSTANCE == null) {
208             throw new AssertionError("NativePRNG not available");
209         }
210         if (params != null) {
211             throw new IllegalArgumentException("Unsupported params: " + params.getClass());
212         }
213     }
214 
215     // set the seed
216     @Override
217     protected void engineSetSeed(byte[] seed) {
218         INSTANCE.implSetSeed(seed);
219     }
220 
221     // get pseudo random bytes
222     @Override
223     protected void engineNextBytes(byte[] bytes) {
224         INSTANCE.implNextBytes(bytes);
225     }
226 
227     // get true random bytes
228     @Override
229     protected byte[] engineGenerateSeed(int numBytes) {
230         return INSTANCE.implGenerateSeed(numBytes);
231     }
232 
233     /**
234      * A NativePRNG-like class that uses /dev/random for both
235      * seed and random material.
236      *
237      * Note that it does not respect the egd properties, since we have
238      * no way of knowing what those qualities are.
239      *
240      * This is very similar to the outer NativePRNG class, minimizing any
241      * breakage to the serialization of the existing implementation.
242      *
243      * @since   1.8
244      */
245     public static final class Blocking extends SecureRandomSpi {
246         private static final long serialVersionUID = -6396183145759983347L;
247 
248         private static final RandomIO INSTANCE = initIO(Variant.BLOCKING);
249 
250         // return whether this is available
251         static boolean isAvailable() {
252             return INSTANCE != null;
253         }
254 
255         // constructor, called by the JCA framework
256         public Blocking(SecureRandomParameters params) {
257             if (INSTANCE == null) {
258                 throw new AssertionError("NativePRNG$Blocking not available");
259             }
260             if (params != null) {
261                 throw new IllegalArgumentException("Unsupported params: " + params.getClass());
262             }
263         }
264 
265         // set the seed
266         @Override
267         protected void engineSetSeed(byte[] seed) {
268             INSTANCE.implSetSeed(seed);
269         }
270 
271         // get pseudo random bytes
272         @Override
273         protected void engineNextBytes(byte[] bytes) {
274             INSTANCE.implNextBytes(bytes);
275         }
276 
277         // get true random bytes
278         @Override
279         protected byte[] engineGenerateSeed(int numBytes) {
280             return INSTANCE.implGenerateSeed(numBytes);
281         }
282     }
283 
284     /**
285      * A NativePRNG-like class that uses /dev/urandom for both
286      * seed and random material.
287      *
288      * Note that it does not respect the egd properties, since we have
289      * no way of knowing what those qualities are.
290      *
291      * This is very similar to the outer NativePRNG class, minimizing any
292      * breakage to the serialization of the existing implementation.
293      *
294      * @since   1.8
295      */
296     public static final class NonBlocking extends SecureRandomSpi {
297         private static final long serialVersionUID = -1102062982994105487L;
298 
299         private static final RandomIO INSTANCE = initIO(Variant.NONBLOCKING);
300 
301         // return whether this is available
302         static boolean isAvailable() {
303             return INSTANCE != null;
304         }
305 
306         // constructor, called by the JCA framework
307         public NonBlocking(SecureRandomParameters params) {
308             if (INSTANCE == null) {
309                 throw new AssertionError(
310                     "NativePRNG$NonBlocking not available");
311             }
312             if (params != null) {
313                 throw new IllegalArgumentException("Unsupported params: " + params.getClass());
314             }
315         }
316 
317         // set the seed
318         @Override
319         protected void engineSetSeed(byte[] seed) {
320             INSTANCE.implSetSeed(seed);
321         }
322 
323         // get pseudo random bytes
324         @Override
325         protected void engineNextBytes(byte[] bytes) {
326             INSTANCE.implNextBytes(bytes);
327         }
328 
329         // get true random bytes
330         @Override
331         protected byte[] engineGenerateSeed(int numBytes) {
332             return INSTANCE.implGenerateSeed(numBytes);
333         }
334     }
335 
336     /**
337      * Nested class doing the actual work. Singleton, see INSTANCE above.
338      */
339     private static class RandomIO {
340 
341         // we buffer data we read from the "next" file for efficiency,
342         // but we limit the lifetime to avoid using stale bits
343         // lifetime in ms, currently 100 ms (0.1 s)
344         private static final long MAX_BUFFER_TIME = 100;
345 
346         // size of the "next" buffer
347         private static final int MAX_BUFFER_SIZE = 65536;
348         private static final int MIN_BUFFER_SIZE = 32;
349         private int bufferSize = 256;
350 
351         // Holder for the seedFile.  Used if we ever add seed material.
352         File seedFile;
353 
354         // In/OutputStream for "seed" and "next"
355         private final InputStream seedIn, nextIn;
356         private OutputStream seedOut;
357 
358         // flag indicating if we have tried to open seedOut yet
359         private boolean seedOutInitialized;
360 
361         // SHA1PRNG instance for mixing
362         // initialized lazily on demand to avoid problems during startup
363         private volatile sun.security.provider.SecureRandom mixRandom;
364 
365         // buffer for next bits
366         private byte[] nextBuffer;
367 
368         // number of bytes left in nextBuffer
369         private int buffered;
370 
371         // time we read the data into the nextBuffer
372         private long lastRead;
373 
374         // Count for the number of buffer size changes requests
375         // Positive value in increase size, negative to lower it.
376         private int change_buffer = 0;
377 
378         // Request limit to trigger an increase in nextBuffer size
379         private static final int REQ_LIMIT_INC = 1000;
380 
381         // Request limit to trigger a decrease in nextBuffer size
382         private static final int REQ_LIMIT_DEC = -100;
383 
384         // mutex lock for nextBytes()
385         private final Object LOCK_GET_BYTES = new Object();
386 
387         // mutex lock for generateSeed()
388         private final Object LOCK_GET_SEED = new Object();
389 
390         // mutex lock for setSeed()
391         private final Object LOCK_SET_SEED = new Object();
392 
393         // constructor, called only once from initIO()
394         private RandomIO(File seedFile, File nextFile) throws IOException {
395             this.seedFile = seedFile;
396             seedIn = FileInputStreamPool.getInputStream(seedFile);
397             nextIn = FileInputStreamPool.getInputStream(nextFile);
398             nextBuffer = new byte[bufferSize];
399         }
400 
401         // get the SHA1PRNG for mixing
402         // initialize if not yet created
403         private sun.security.provider.SecureRandom getMixRandom() {
404             sun.security.provider.SecureRandom r = mixRandom;
405             if (r == null) {
406                 synchronized (LOCK_GET_BYTES) {
407                     r = mixRandom;
408                     if (r == null) {
409                         r = new sun.security.provider.SecureRandom();
410                         try {
411                             byte[] b = new byte[20];
412                             readFully(nextIn, b);
413                             r.engineSetSeed(b);
414                         } catch (IOException e) {
415                             throw new ProviderException("init failed", e);
416                         }
417                         mixRandom = r;
418                     }
419                 }
420             }
421             return r;
422         }
423 
424         // read data.length bytes from in
425         // These are not normal files, so we need to loop the read.
426         // just keep trying as long as we are making progress
427         private static void readFully(InputStream in, byte[] data)
428                 throws IOException {
429             int len = data.length;
430             int ofs = 0;
431             while (len > 0) {
432                 int k = in.read(data, ofs, len);
433                 if (k <= 0) {
434                     throw new EOFException("File(s) closed?");
435                 }
436                 ofs += k;
437                 len -= k;
438             }
439             if (len > 0) {
440                 throw new IOException("Could not read from file(s)");
441             }
442         }
443 
444         // get true random bytes, just read from "seed"
445         private byte[] implGenerateSeed(int numBytes) {
446             synchronized (LOCK_GET_SEED) {
447                 try {
448                     byte[] b = new byte[numBytes];
449                     readFully(seedIn, b);
450                     return b;
451                 } catch (IOException e) {
452                     throw new ProviderException("generateSeed() failed", e);
453                 }
454             }
455         }
456 
457         // supply random bytes to the OS
458         // write to "seed" if possible
459         // always add the seed to our mixing random
460         @SuppressWarnings("removal")
461         private void implSetSeed(byte[] seed) {
462             synchronized (LOCK_SET_SEED) {
463                 if (seedOutInitialized == false) {
464                     seedOutInitialized = true;
465                     seedOut = AccessController.doPrivileged(
466                             new PrivilegedAction<>() {
467                         @Override
468                         public OutputStream run() {
469                             try {
470                                 return new FileOutputStream(seedFile, true);
471                             } catch (Exception e) {
472                                 return null;
473                             }
474                         }
475                     });
476                 }
477                 if (seedOut != null) {
478                     try {
479                         seedOut.write(seed);
480                     } catch (IOException e) {
481                         // Ignored. On Mac OS X, /dev/urandom can be opened
482                         // for write, but actual write is not permitted.
483                     }
484                 }
485                 getMixRandom().engineSetSeed(seed);
486             }
487         }
488 
489         // ensure that there is at least one valid byte in the buffer
490         // if not, read new bytes
491         private void ensureBufferValid() throws IOException {
492             long time = System.currentTimeMillis();
493             int new_buffer_size = 0;
494 
495             // Check if buffer has bytes available that are not too old
496             if (buffered > 0) {
497                 if (time - lastRead < MAX_BUFFER_TIME) {
498                     return;
499                 } else {
500                     // byte is old, so subtract from counter to shrink buffer
501                     change_buffer--;
502                 }
503             } else {
504                 // No bytes available, so add to count to increase buffer
505                 change_buffer++;
506             }
507 
508             // If counter has it a limit, increase or decrease size
509             if (change_buffer > REQ_LIMIT_INC) {
510                 new_buffer_size = nextBuffer.length * 2;
511             } else if (change_buffer < REQ_LIMIT_DEC) {
512                 new_buffer_size = nextBuffer.length / 2;
513             }
514 
515             // If buffer size is to be changed, replace nextBuffer.
516             if (new_buffer_size > 0) {
517                 if (new_buffer_size <= MAX_BUFFER_SIZE &&
518                         new_buffer_size >= MIN_BUFFER_SIZE) {
519                     nextBuffer = new byte[new_buffer_size];
520                     if (debug != null) {
521                         debug.println("Buffer size changed to " +
522                                 new_buffer_size);
523                     }
524                 } else {
525                     if (debug != null) {
526                         debug.println("Buffer reached limit: " +
527                                 nextBuffer.length);
528                     }
529                 }
530                 change_buffer = 0;
531             }
532 
533             // Load fresh random bytes into nextBuffer
534             lastRead = time;
535             readFully(nextIn, nextBuffer);
536             buffered = nextBuffer.length;
537         }
538 
539         // get pseudo random bytes
540         // read from "next" and XOR with bytes generated by the
541         // mixing SHA1PRNG
542         private void implNextBytes(byte[] data) {
543                 try {
544                     getMixRandom().engineNextBytes(data);
545                     int data_len = data.length;
546                     int ofs = 0;
547                     int len;
548                     int buf_pos;
549                     int localofs;
550                     byte[] localBuffer;
551 
552                     while (data_len > 0) {
553                         synchronized (LOCK_GET_BYTES) {
554                             ensureBufferValid();
555                             buf_pos = nextBuffer.length - buffered;
556                             if (data_len > buffered) {
557                                 len = buffered;
558                                 buffered = 0;
559                             } else {
560                                 len = data_len;
561                                 buffered -= len;
562                             }
563                             localBuffer = Arrays.copyOfRange(nextBuffer, buf_pos,
564                                     buf_pos + len);
565                         }
566                         localofs = 0;
567                         while (len > localofs) {
568                             data[ofs] ^= localBuffer[localofs];
569                             ofs++;
570                             localofs++;
571                         }
572                     data_len -= len;
573                     }
574                 } catch (IOException e){
575                     throw new ProviderException("nextBytes() failed", e);
576                 }
577         }
578         }
579 }