Package freenet.crypt

Source Code of freenet.crypt.Yarrow

/* This code is part of Freenet. It is distributed under the GNU General
* Public License, version 2 (or at your option any later version). See
* http://www.gnu.org/ for further details of the GPL. */
package freenet.crypt;

import static java.util.concurrent.TimeUnit.HOURS;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;
import freenet.support.io.Closer;

/**
* An implementation of the Yarrow PRNG in Java.
* <p>
* This class implements Yarrow-160, a cryptraphically secure PRNG developed by
* John Kelsey, Bruce Schneier, and Neils Ferguson. It was designed to follow
* the specification (www.counterpane.com/labs) given in the paper by the same
* authors, with the following exceptions:
* </p>
* <ul>
* <li>Instead of 3DES as the output cipher, Rijndael was chosen. It was my
* belief that an AES candidate should be selected. Twofish was an alternate
* choice, but the AES implementation does not allow easy selection of a faster
* key-schedule, so twofish's severely impaired performance.</li>
* <li>h prime, described as a 'size adaptor' was not used, since its function
* is only to constrain the size of a byte array, our own key generation
* routine was used instead (See
* {@link freenet.crypt.Util#makeKey freenet.crypt.Util.makeKey})</li>
* <li>Our own entropy estimation routines are used, as they use a third-order
* delta calculation that is quite conservative. Still, its used along side the
* global multiplier and program- supplied guesses, as suggested.</li>
* </ul>
*
* @author Scott G. Miller <scgmille@indiana.edu>
*/
public class Yarrow extends RandomSource {

  private static final long serialVersionUID = -1;
  private static volatile boolean logMINOR;

  static {
    Logger.registerLogThresholdCallback(new LogThresholdCallback(){
      @Override
      public void shouldUpdate(){
        logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
      }
    });
  }
  /**
   * Security parameters
   */
  private static final boolean DEBUG = false;
  private static final int Pg = 10;
  private final SecureRandom sr;
  public final File seedfile; //A file to which seed data should be dumped periodically

  public Yarrow() {
    this("prng.seed", "SHA1", "Rijndael", true, true);
  }

  public Yarrow(boolean canBlock) {
    this("prng.seed", "SHA1", "Rijndael", true, canBlock);
  }

  public Yarrow(File seed) {
    this(seed, "SHA1", "Rijndael", true, true);
  }

  public Yarrow(String seed, String digest, String cipher, boolean updateSeed, boolean canBlock) {
    this(new File(seed), digest, cipher, updateSeed, canBlock);
  }

  public Yarrow(File seed, String digest, String cipher, boolean updateSeed, boolean canBlock) {
    this(seed, digest, cipher, updateSeed, canBlock, true);
  }

  // unset reseedOnStartup only in unit test
  Yarrow(File seed, String digest, String cipher, boolean updateSeed, boolean canBlock, boolean reseedOnStartup) {
    SecureRandom s;
    try {
      s = SecureRandom.getInstance("SHA1PRNG");
    } catch(NoSuchAlgorithmException e) {
      s = null;
    }
    sr = s;
    try {
      accumulator_init(digest);
      reseed_init(digest);
      generator_init(cipher);
    } catch(NoSuchAlgorithmException e) {
      Logger.error(this, "Could not init pools trying to getInstance(" + digest + "): " + e, e);
      throw new RuntimeException("Cannot initialize Yarrow!: " + e, e);
    }

    if(updateSeed && !(seed.toString()).equals("/dev/urandom")) //Dont try to update the seedfile if we know that it wont be possible anyways
      seedfile = seed;
    else
      seedfile = null;
    if(reseedOnStartup) {
      entropy_init(seed, reseedOnStartup);
      seedFromExternalStuff(canBlock);
      /**
       * If we don't reseed at this point, we will be predictable,
       * because the startup entropy won't cause a reseed.
       */
      fast_pool_reseed();
      slow_pool_reseed();
    } else {
      read_seed(seed);
    }
  }

  private void seedFromExternalStuff(boolean canBlock) {
    byte[] buf = new byte[32];
    if(File.separatorChar == '/') {
      DataInputStream dis = null;
      FileInputStream fis = null;
      File hwrng = new File("/dev/hwrng");
      if(hwrng.exists() && hwrng.canRead())
        try {
          fis = new FileInputStream(hwrng);
          dis = new DataInputStream(fis);
          dis.readFully(buf);
          consumeBytes(buf);
          dis.readFully(buf);
          consumeBytes(buf);
          dis.close();
        } catch(Throwable t) {
          Logger.normal(this, "Can't read /dev/hwrng even though exists and is readable: " + t, t);
        } finally {
          Closer.close(dis);
          Closer.close(fis);
        }

      // Read some bits from /dev/urandom
      try {
        fis = new FileInputStream("/dev/urandom");
        dis = new DataInputStream(fis);
        dis.readFully(buf);
        consumeBytes(buf);
        dis.readFully(buf);
        consumeBytes(buf);
      } catch(Throwable t) {
        Logger.normal(this, "Can't read /dev/urandom: " + t, t);
        // We can't read it; let's skip /dev/random and seed from SecureRandom.generateSeed()
        canBlock = true;
      } finally {
        Closer.close(dis);
        Closer.close(fis);
      }
      if(canBlock)
        // Read some bits from /dev/random
        try {
          fis = new FileInputStream("/dev/random");
          dis = new DataInputStream(fis);
          dis.readFully(buf);
          consumeBytes(buf);
          dis.readFully(buf);
          consumeBytes(buf);
        } catch(Throwable t) {
          Logger.normal(this, "Can't read /dev/random: " + t, t);
        } finally {
          Closer.close(dis);
          Closer.close(fis);
        }
      fis = null;
    } else
      // Force generateSeed(), since we can't read random data from anywhere else.
      // Anyway, Windows's CAPI won't block.
      canBlock = true;
    if(canBlock) {
      // SecureRandom hopefully acts as a proxy for CAPI on Windows
      buf = sr.generateSeed(32);
      consumeBytes(buf);
      buf = sr.generateSeed(32);
      consumeBytes(buf);
    }
    // A few more bits
    consumeString(Long.toHexString(Runtime.getRuntime().freeMemory()));
    consumeString(Long.toHexString(Runtime.getRuntime().totalMemory()));
  }

  private void entropy_init(File seed, boolean reseedOnStartup) {
    if(reseedOnStartup) {
      Properties sys = System.getProperties();
      EntropySource startupEntropy = new EntropySource();

      // Consume the system properties list
      for(Enumeration<?> enu = sys.propertyNames(); enu.hasMoreElements();) {
        String key = (String) enu.nextElement();
        consumeString(key);
        consumeString(sys.getProperty(key));
      }

      // Consume the local IP address
      try {
        consumeString(InetAddress.getLocalHost().toString());
      } catch(Exception e) {
        // Ignore
      }
      readStartupEntropy(startupEntropy);
    }

    read_seed(seed);
  }

  protected void readStartupEntropy(EntropySource startupEntropy) {
    // Consume the current time
    acceptEntropy(startupEntropy, System.currentTimeMillis(), 0);
    acceptEntropy(startupEntropy, System.nanoTime(), 0);
    // Free memory
    acceptEntropy(startupEntropy, Runtime.getRuntime().freeMemory(), 0);
    // Total memory
    acceptEntropy(startupEntropy, Runtime.getRuntime().totalMemory(), 0);
  }

  /**
   * Seed handling
   */
  private void read_seed(File filename) {
    FileInputStream fis = null;
    BufferedInputStream bis = null;
    DataInputStream dis = null;

    try {
      fis = new FileInputStream(filename);
      bis = new BufferedInputStream(fis);
      dis = new DataInputStream(bis);

      EntropySource seedFile = new EntropySource();
        for(int i = 0; i < 32; i++)
          acceptEntropy(seedFile, dis.readLong(), 64);
      dis.close();
    } catch(EOFException f) {
      // Okay.
    } catch(IOException e) {
      Logger.error(this, "IOE trying to read the seedfile from disk : " + e.getMessage());
    } finally {
      Closer.close(dis);
      Closer.close(bis);
      Closer.close(fis);
    }
    fast_pool_reseed();
  }
  private long timeLastWroteSeed = -1;

  private void write_seed(File filename) {
    write_seed(filename, false);
  }

  public void write_seed(File filename, boolean force) {
    if(!force)
      synchronized(this) {
        long now = System.currentTimeMillis();
        if(now - timeLastWroteSeed <= HOURS.toMillis(1) /* once per hour */)
          return;
        else
          timeLastWroteSeed = now;
      }

    FileOutputStream fos = null;
    BufferedOutputStream bos = null;
    DataOutputStream dos = null;
    try {
      fos = new FileOutputStream(filename);
      bos = new BufferedOutputStream(fos);
      dos = new DataOutputStream(bos);

      for(int i = 0; i < 32; i++)
        dos.writeLong(nextLong());

      dos.flush();
      dos.close();
    } catch(IOException e) {
      Logger.error(this, "IOE while saving the seed file! : " + e.getMessage());
    } finally {
      Closer.close(dos);
      Closer.close(bos);
      Closer.close(fos);
    }
  }
  /**
   * 5.1 Generation Mechanism
   */
  private BlockCipher cipher_ctx;
  private byte[] output_buffer,  counter,  allZeroString,  tmp;
  private int output_count,  fetch_counter;

  private void generator_init(String cipher) {
    cipher_ctx = Util.getCipherByName(cipher);
    output_buffer = new byte[cipher_ctx.getBlockSize() / 8];
    counter = new byte[cipher_ctx.getBlockSize() / 8];
    allZeroString = new byte[cipher_ctx.getBlockSize() / 8];
    tmp = new byte[cipher_ctx.getKeySize() / 8];

    fetch_counter = output_buffer.length;
  }

  private void counterInc() {
    for(int i = counter.length - 1; i >= 0; i--)
      if(++counter[i] != 0)
        break;
  }

  private void generateOutput() {
    counterInc();

    output_buffer = new byte[counter.length];
    cipher_ctx.encipher(counter, output_buffer);

    if(output_count++ > Pg) {
      output_count = 0;
      nextBytes(tmp);
      rekey(tmp);
    }
  }

  private void rekey(byte[] key) {
    cipher_ctx.initialize(key);
    counter = new byte[allZeroString.length];
    cipher_ctx.encipher(allZeroString, counter);
    Arrays.fill(key, (byte) 0);
  }

  // Fetches count bytes of randomness into the shared buffer, returning
  // an offset to the bytes
  private synchronized int getBytes(int count) {

    if(fetch_counter + count > output_buffer.length) {
      fetch_counter = 0;
      generateOutput();
      return getBytes(count);
    }

    int rv = fetch_counter;
    fetch_counter += count;
    return rv;
  }
  static final int bitTable[][] = {{0, 0x0}, {
      1, 0x1
    }, {
      1, 0x3
    }, {
      1, 0x7
    }, {
      1, 0xf
    }, {
      1, 0x1f
    }, {
      1, 0x3f
    }, {
      1, 0x7f
    }, {
      1, 0xff
    }, {
      2, 0x1ff
    }, {
      2, 0x3ff
    }, {
      2, 0x7ff
    }, {
      2, 0xfff
    }, {
      2, 0x1fff
    }, {
      2, 0x3fff
    }, {
      2, 0x7fff
    }, {
      2, 0xffff
    }, {
      3, 0x1ffff
    }, {
      3, 0x3ffff
    }, {
      3, 0x7ffff
    }, {
      3, 0xfffff
    }, {
      3, 0x1fffff
    }, {
      3, 0x3fffff
    }, {
      3, 0x7fffff
    }, {
      3, 0xffffff
    }, {
      4, 0x1ffffff
    }, {
      4, 0x3ffffff
    }, {
      4, 0x7ffffff
    }, {
      4, 0xfffffff
    }, {
      4, 0x1fffffff
    }, {
      4, 0x3fffffff
    }, {
      4, 0x7fffffff
    }, {
      4, 0xffffffff
    }};

  // This may *look* more complicated than in is, but in fact it is
  // loop unrolled, cache and operation optimized.
  // So don't try to simplify it... Thanks. :)
  // When this was not synchronized, we were getting repeats...
  @Override
  protected synchronized int next(int bits) {
    int[] parameters = bitTable[bits];
    int offset = getBytes(parameters[0]);

    int val = output_buffer[offset];

    if(parameters[0] == 4)
      val += (output_buffer[offset + 1] << 24) + (output_buffer[offset + 2] << 16) + (output_buffer[offset + 3] << 8);
    else if(parameters[0] == 3)
      val += (output_buffer[offset + 1] << 16) + (output_buffer[offset + 2] << 8);
    else if(parameters[0] == 2)
      val += output_buffer[offset + 2] << 8;

    return val & parameters[1];
  }
  /**
   * 5.2 Entropy Accumulator
   */
  private MessageDigest fast_pool,  slow_pool;
  private int fast_entropy,  slow_entropy;
  private boolean fast_select;
  private Map<EntropySource, int[]> entropySeen;

  private void accumulator_init(String digest) throws NoSuchAlgorithmException {
    fast_pool = MessageDigest.getInstance(digest, Util.mdProviders.get(digest));
    slow_pool = MessageDigest.getInstance(digest, Util.mdProviders.get(digest));
    entropySeen = new HashMap<EntropySource, int[]>();
  }

  @Override
  public int acceptEntropy(EntropySource source, long data, int entropyGuess) {
    return acceptEntropy(source, data, entropyGuess, 1.0);
  }

  @Override
  public int acceptEntropyBytes(EntropySource source, byte[] buf, int offset,
    int length, double bias) {
    int totalRealEntropy = 0;
    for(int i = 0; i < length; i += 8) {
      long thingy = 0;
      int bytes = 0;
      for(int j = 0; j < Math.min(length, i + 8); j++) {
        thingy = (thingy << 8) + (buf[j] & 0xFF);
        bytes++;
      }
      totalRealEntropy += acceptEntropy(source, thingy, bytes * 8, bias);
    }
    return totalRealEntropy;
  }

  private int acceptEntropy(
    EntropySource source,
    long data,
    int entropyGuess,
    double bias) {
    return accept_entropy(
      data,
      source,
      (int) (bias * Math.min(
      32,
      Math.min(estimateEntropy(source, data), entropyGuess))));
  }

  private int accept_entropy(long data, EntropySource source, int actualEntropy) {

    boolean performedPoolReseed = false;
    byte[] b = new byte[] {
        (byte) data,
        (byte) (data >> 8),
        (byte) (data >> 16),
        (byte) (data >> 24),
        (byte) (data >> 32),
        (byte) (data >> 40),
        (byte) (data >> 48),
        (byte) (data >> 56)
    };

    synchronized(this) {
      fast_select = !fast_select;
      MessageDigest pool = (fast_select ? fast_pool : slow_pool);
      pool.update(b);

      if(fast_select) {
        fast_entropy += actualEntropy;
        if(fast_entropy > FAST_THRESHOLD) {
          fast_pool_reseed();
          performedPoolReseed = true;
        }
      } else {
        slow_entropy += actualEntropy;

        if(source != null) {
          int[] contributedEntropy = entropySeen.get(source);
          if(contributedEntropy == null) {
            contributedEntropy = new int[] { actualEntropy };
            entropySeen.put(source, contributedEntropy);
          } else
            contributedEntropy[0]+=actualEntropy;

          if(slow_entropy >= (SLOW_THRESHOLD * 2)) {
            int kc = 0;
            for(Map.Entry<EntropySource, int[]> e : entropySeen.entrySet()) {
              EntropySource key = e.getKey();
              int[] v = e.getValue();
              if(DEBUG)
                Logger.normal(this, "Key: <" + key + "> " + v);
              if(v[0] > SLOW_THRESHOLD) {
                kc++;
                if(kc >= SLOW_K) {
                  slow_pool_reseed();
                  performedPoolReseed = true;
                  break;
                }
              }
            }
          }
        }
      }
      if(DEBUG)
        //      Core.logger.log(this,"Fast pool: "+fast_entropy+"\tSlow pool:
        // "+slow_entropy, LogLevel.NORMAL);
        System.err.println("Fast pool: " + fast_entropy + "\tSlow pool: " + slow_entropy);
    }
    if(performedPoolReseed && (seedfile != null)) {
      //Dont do this while synchronized on 'this' since
      //opening a file seems to be suprisingly slow on windows
      if(logMINOR)
        Logger.minor(this, "Writing seedfile");
      write_seed(seedfile);
      if(logMINOR)
        Logger.minor(this, "Written seedfile");
    }

    return actualEntropy;
  }

  private int estimateEntropy(EntropySource source, long newVal) {
    int delta = (int) (newVal - source.lastVal);
    int delta2 = delta - source.lastDelta;
    source.lastDelta = delta;

    int delta3 = delta2 - source.lastDelta2;
    source.lastDelta2 = delta2;

    if(delta < 0)
      delta = -delta;
    if(delta2 < 0)
      delta2 = -delta2;
    if(delta3 < 0)
      delta3 = -delta3;
    if(delta > delta2)
      delta = delta2;
    if(delta > delta3)
      delta = delta3;

    /*
     * delta is now minimum absolute delta. Round down by 1 bit on general
     * principles, and limit entropy entimate to 12 bits.
     */
    delta >>= 1;
    delta &= (1 << 12) - 1;

    /* Smear msbit right to make an n-bit mask */
    delta |= delta >> 8;
    delta |= delta >> 4;
    delta |= delta >> 2;
    delta |= delta >> 1;
    /* Remove one bit to make this a logarithm */
    delta >>= 1;
    /* Count the bits set in the word */
    delta -= (delta >> 1) & 0x555;
    delta = (delta & 0x333) + ((delta >> 2) & 0x333);
    delta += (delta >> 4);
    delta += (delta >> 8);

    source.lastVal = newVal;

    return delta & 15;
  }

  @Override
  public int acceptTimerEntropy(EntropySource timer) {
    return acceptTimerEntropy(timer, 1.0);
  }

  @Override
  public int acceptTimerEntropy(EntropySource timer, double bias) {
    long now = System.currentTimeMillis();
    return acceptEntropy(timer, now - timer.lastVal, 32, bias);
  }

  /**
   * If entropy estimation is supported, this method will block until the
   * specified number of bits of entropy are available. If estimation isn't
   * supported, this method will return immediately.
   */
  @Override
  public void waitForEntropy(int bits) {
  }
  /**
   * 5.3 Reseed mechanism
   */
  private static final int Pt = 5;
  private MessageDigest reseed_ctx;

  private void reseed_init(String digest) throws NoSuchAlgorithmException {
    reseed_ctx = MessageDigest.getInstance(digest, Util.mdProviders.get(digest));
  }

  private void fast_pool_reseed() {
    long startTime = System.currentTimeMillis();
    byte[] v0 = fast_pool.digest();
    byte[] vi = v0;

    for(byte i = 0; i < Pt; i++) {
      reseed_ctx.update(vi, 0, vi.length);
      reseed_ctx.update(v0, 0, v0.length);
      reseed_ctx.update(i);
      vi = reseed_ctx.digest();
    }

    // vPt=vi
    Util.makeKey(vi, tmp, 0, tmp.length);
    rekey(tmp);
    Arrays.fill(v0, (byte) 0); // blank out for security
    fast_entropy = 0;
    if (DEBUG) {
      long endTime = System.currentTimeMillis();
      if(endTime - startTime > 5000)
        Logger.normal(this, "Fast pool reseed took " + (endTime - startTime) + "ms");
    }
  }

  private void slow_pool_reseed() {
    byte[] slow_hash = slow_pool.digest();
    fast_pool.update(slow_hash, 0, slow_hash.length);

    fast_pool_reseed();
    slow_entropy = 0;

    entropySeen.clear();
  }
  /**
   * 5.4 Reseed Control parameters
   */
  private static final int FAST_THRESHOLD = 100,  SLOW_THRESHOLD = 160,  SLOW_K = 2;

  /**
   * If the RandomSource has any resources it wants to close, it can do so
   * when this method is called
   */
  @Override
  public void close() {
  }

  /**
   * Test routine
   */
  public static void main(String[] args) throws Exception {
    Yarrow r = new Yarrow(new File("/dev/urandom"), "SHA1", "Rijndael", true, false);

    byte[] b = new byte[1024];

    if((args.length == 0) || args[0].equalsIgnoreCase("latency")) {
      if(args.length == 2)
        b = new byte[Integer.parseInt(args[1])];
      long start = System.currentTimeMillis();
      for(int i = 0; i < 100; i++)
        r.nextBytes(b);
      System.out.println(
        (double) (System.currentTimeMillis() - start) / (100 * b.length) * 1024 + " ms/k");
      start = System.currentTimeMillis();
      for(int i = 0; i < 1000; i++)
        r.nextInt();
      System.out.println(
        (double) (System.currentTimeMillis() - start) / 1000 + " ms/int");
      start = System.currentTimeMillis();
      for(int i = 0; i < 1000; i++)
        r.nextLong();
      System.out.println(
        (double) (System.currentTimeMillis() - start) / 1000 + " ms/long");
    } else if(args[0].equalsIgnoreCase("randomness")) {
      int kb = Integer.parseInt(args[1]);
      for(int i = 0; i < kb; i++) {
        r.nextBytes(b);
        System.out.write(b);
      }
    } else if(args[0].equalsIgnoreCase("gathering")) {
      System.gc();
      EntropySource t = new EntropySource();
      long start = System.currentTimeMillis();
      for(int i = 0; i < 100000; i++)
        r.acceptEntropy(t, System.currentTimeMillis(), 32);
      System.err.println(
        (double) (System.currentTimeMillis() - start) / 100000);
      System.gc();
      start = System.currentTimeMillis();
      for(int i = 0; i < 100000; i++)
        r.acceptTimerEntropy(t);
      System.err.println(
        (double) (System.currentTimeMillis() - start) / 100000);
    } else if(args[0].equalsIgnoreCase("volume")) {
      b = new byte[1020];
      long duration =
        System.currentTimeMillis() + Integer.parseInt(args[1]);
      while(System.currentTimeMillis() < duration) {
        r.nextBytes(b);
        System.out.write(b);
      }
//    } else if (args[0].equals("stream")) {
//      RandFile f = new RandFile(args[1]);
//      EntropySource rf = new EntropySource();
//      byte[] buffer = new byte[131072];
//      while (true) {
//        r.acceptEntropy(rf, f.nextLong(), 32);
//        r.nextBytes(buffer);
//        System.out.write(buffer);
//      }
    } else if(args[0].equalsIgnoreCase("bitstream"))
      while(true) {
        int v = r.nextInt();
        for(int i = 0; i < 32; i++) {
          if(((v >> i) & 1) == 1)
            System.out.print('1');
          else
            System.out.print('0');
        }
      }
    else if(args[0].equalsIgnoreCase("sample"))
      if((args.length == 1) || args[1].equals("general")) {
        System.out.println("nextInt(): ");
        for(int i = 0; i < 3; i++)
          System.out.println(r.nextInt());
        System.out.println("nextLong(): ");
        for(int i = 0; i < 3; i++)
          System.out.println(r.nextLong());
        System.out.println("nextFloat(): ");
        for(int i = 0; i < 3; i++)
          System.out.println(r.nextFloat());
        System.out.println("nextDouble(): ");
        for(int i = 0; i < 3; i++)
          System.out.println(r.nextDouble());
        System.out.println("nextFullFloat(): ");
        for(int i = 0; i < 3; i++)
          System.out.println(r.nextFullFloat());
        System.out.println("nextFullDouble(): ");
        for(int i = 0; i < 3; i++)
          System.out.println(r.nextFullDouble());
      } else if(args[1].equals("normalized"))
        for(int i = 0; i < 20; i++)
          System.out.println(r.nextDouble());
  }

  private void consumeString(String str) {
    byte[] b;
    try {
      b = str.getBytes("UTF-8");
    } catch(UnsupportedEncodingException e) {
      throw new Error("Impossible: JVM doesn't support UTF-8: " + e, e);
    }
    consumeBytes(b);
  }

  private void consumeBytes(byte[] bytes) {
    if(fast_select)
      fast_pool.update(bytes, 0, bytes.length);
    else
      slow_pool.update(bytes, 0, bytes.length);
    fast_select = !fast_select;
  }
}
TOP

Related Classes of freenet.crypt.Yarrow

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.