From aab4f8f906fde6c73cae6fb9cea6171622ed3987 Mon Sep 17 00:00:00 2001 From: nathan Date: Wed, 20 Aug 2014 23:45:14 +0200 Subject: [PATCH] added utilities --- Dorkbox-Util/.classpath | 28 + Dorkbox-Util/.gitignore | 1 + Dorkbox-Util/.project | 17 + Dorkbox-Util/Test - Dorkbox Util.launch | 15 + Dorkbox-Util/scripts/certutil_x64 | Bin 0 -> 151576 bytes Dorkbox-Util/scripts/certutil_x86 | Bin 0 -> 141656 bytes Dorkbox-Util/scripts/incert.bat | 9 + Dorkbox-Util/scripts/incert.sh | 71 + .../scripts/win_xp_codesigningx86.exe | Bin 0 -> 126752 bytes .../src/dorkbox/util/ClassHelper.java | 154 ++ .../src/dorkbox/util/CountingLatch.java | 74 + Dorkbox-Util/src/dorkbox/util/DelayTimer.java | 72 + Dorkbox-Util/src/dorkbox/util/FileUtil.java | 688 +++++++ .../src/dorkbox/util/InputConsole.java | 342 ++++ Dorkbox-Util/src/dorkbox/util/RegExp.java | 38 + Dorkbox-Util/src/dorkbox/util/Storage.java | 336 ++++ Dorkbox-Util/src/dorkbox/util/Sys.java | 657 ++++++ .../src/dorkbox/util/bytes/BigEndian.java | 120 ++ .../src/dorkbox/util/bytes/ByteBuffer2.java | 318 +++ .../src/dorkbox/util/bytes/LittleEndian.java | 345 ++++ .../src/dorkbox/util/bytes/OptimizeUtils.java | 541 +++++ .../util/bytes/OptimizeUtilsByteBuf.java | 297 +++ .../src/dorkbox/util/crypto/BCrypt.java | 782 ++++++++ .../src/dorkbox/util/crypto/Crypto.java | 1761 +++++++++++++++++ .../src/dorkbox/util/crypto/CryptoX509.java | 1292 ++++++++++++ .../bouncycastle/GCMBlockCipher_ByteBuf.java | 420 ++++ .../crypto/bouncycastle/GCMUtil_ByteBuf.java | 155 ++ .../Tables8kGCMMultiplier_ByteBuf.java | 139 ++ .../EccPrivateKeySerializer.java | 241 +++ .../serialization/EccPublicKeySerializer.java | 94 + .../IesParametersSerializer.java | 59 + .../IesWithCipherParametersSerializer.java | 66 + .../RsaPrivateKeySerializer.java | 142 ++ .../serialization/RsaPublicKeySerializer.java | 58 + .../signers/BcECDSAContentSignerBuilder.java | 24 + ...BcECDSAContentVerifierProviderBuilder.java | 36 + .../dorkbox/util/gwt/GwtSymbolMapParser.java | 118 ++ .../util/process/JavaProcessBuilder.java | 244 +++ .../util/process/LauncherProcessBuilder.java | 136 ++ .../util/process/NullOutputStream.java | 25 + .../dorkbox/util/process/ProcessProxy.java | 61 + .../util/process/ShellProcessBuilder.java | 303 +++ .../util/properties/PropertiesProvider.java | 127 ++ .../util/properties/SortedProperties.java | 32 + .../dsa/BCDSAPublicKeyAccessor.java | 10 + .../rsa/BCRSAPublicKeyAccessor.java | 10 + .../asymmetric/x509/X509Accessor.java | 21 + .../org/bouncycastle/math/ec/ECAccessor.java | 8 + .../test/dorkbox/util/Base64FastTest.java | 28 + .../test/dorkbox/util/StorageTest.java | 221 +++ .../dorkbox/util/crypto/AesByteBufTest.java | 325 +++ .../test/dorkbox/util/crypto/AesTest.java | 328 +++ .../test/dorkbox/util/crypto/DsaTest.java | 136 ++ .../test/dorkbox/util/crypto/EccTest.java | 317 +++ .../test/dorkbox/util/crypto/RsaTest.java | 127 ++ .../test/dorkbox/util/crypto/SCryptTest.java | 59 + .../test/dorkbox/util/crypto/x509Test.java | 155 ++ 57 files changed, 12183 insertions(+) create mode 100644 Dorkbox-Util/.classpath create mode 100644 Dorkbox-Util/.gitignore create mode 100644 Dorkbox-Util/.project create mode 100644 Dorkbox-Util/Test - Dorkbox Util.launch create mode 100644 Dorkbox-Util/scripts/certutil_x64 create mode 100644 Dorkbox-Util/scripts/certutil_x86 create mode 100644 Dorkbox-Util/scripts/incert.bat create mode 100644 Dorkbox-Util/scripts/incert.sh create mode 100644 Dorkbox-Util/scripts/win_xp_codesigningx86.exe create mode 100644 Dorkbox-Util/src/dorkbox/util/ClassHelper.java create mode 100644 Dorkbox-Util/src/dorkbox/util/CountingLatch.java create mode 100644 Dorkbox-Util/src/dorkbox/util/DelayTimer.java create mode 100644 Dorkbox-Util/src/dorkbox/util/FileUtil.java create mode 100644 Dorkbox-Util/src/dorkbox/util/InputConsole.java create mode 100644 Dorkbox-Util/src/dorkbox/util/RegExp.java create mode 100644 Dorkbox-Util/src/dorkbox/util/Storage.java create mode 100644 Dorkbox-Util/src/dorkbox/util/Sys.java create mode 100644 Dorkbox-Util/src/dorkbox/util/bytes/BigEndian.java create mode 100644 Dorkbox-Util/src/dorkbox/util/bytes/ByteBuffer2.java create mode 100644 Dorkbox-Util/src/dorkbox/util/bytes/LittleEndian.java create mode 100644 Dorkbox-Util/src/dorkbox/util/bytes/OptimizeUtils.java create mode 100644 Dorkbox-Util/src/dorkbox/util/bytes/OptimizeUtilsByteBuf.java create mode 100644 Dorkbox-Util/src/dorkbox/util/crypto/BCrypt.java create mode 100644 Dorkbox-Util/src/dorkbox/util/crypto/Crypto.java create mode 100644 Dorkbox-Util/src/dorkbox/util/crypto/CryptoX509.java create mode 100644 Dorkbox-Util/src/dorkbox/util/crypto/bouncycastle/GCMBlockCipher_ByteBuf.java create mode 100644 Dorkbox-Util/src/dorkbox/util/crypto/bouncycastle/GCMUtil_ByteBuf.java create mode 100644 Dorkbox-Util/src/dorkbox/util/crypto/bouncycastle/Tables8kGCMMultiplier_ByteBuf.java create mode 100644 Dorkbox-Util/src/dorkbox/util/crypto/serialization/EccPrivateKeySerializer.java create mode 100644 Dorkbox-Util/src/dorkbox/util/crypto/serialization/EccPublicKeySerializer.java create mode 100644 Dorkbox-Util/src/dorkbox/util/crypto/serialization/IesParametersSerializer.java create mode 100644 Dorkbox-Util/src/dorkbox/util/crypto/serialization/IesWithCipherParametersSerializer.java create mode 100644 Dorkbox-Util/src/dorkbox/util/crypto/serialization/RsaPrivateKeySerializer.java create mode 100644 Dorkbox-Util/src/dorkbox/util/crypto/serialization/RsaPublicKeySerializer.java create mode 100644 Dorkbox-Util/src/dorkbox/util/crypto/signers/BcECDSAContentSignerBuilder.java create mode 100644 Dorkbox-Util/src/dorkbox/util/crypto/signers/BcECDSAContentVerifierProviderBuilder.java create mode 100644 Dorkbox-Util/src/dorkbox/util/gwt/GwtSymbolMapParser.java create mode 100644 Dorkbox-Util/src/dorkbox/util/process/JavaProcessBuilder.java create mode 100644 Dorkbox-Util/src/dorkbox/util/process/LauncherProcessBuilder.java create mode 100644 Dorkbox-Util/src/dorkbox/util/process/NullOutputStream.java create mode 100644 Dorkbox-Util/src/dorkbox/util/process/ProcessProxy.java create mode 100644 Dorkbox-Util/src/dorkbox/util/process/ShellProcessBuilder.java create mode 100644 Dorkbox-Util/src/dorkbox/util/properties/PropertiesProvider.java create mode 100644 Dorkbox-Util/src/dorkbox/util/properties/SortedProperties.java create mode 100644 Dorkbox-Util/src/org/bouncycastle/jcajce/provider/asymmetric/dsa/BCDSAPublicKeyAccessor.java create mode 100644 Dorkbox-Util/src/org/bouncycastle/jcajce/provider/asymmetric/rsa/BCRSAPublicKeyAccessor.java create mode 100644 Dorkbox-Util/src/org/bouncycastle/jcajce/provider/asymmetric/x509/X509Accessor.java create mode 100644 Dorkbox-Util/src/org/bouncycastle/math/ec/ECAccessor.java create mode 100644 Dorkbox-Util/test/dorkbox/util/Base64FastTest.java create mode 100644 Dorkbox-Util/test/dorkbox/util/StorageTest.java create mode 100644 Dorkbox-Util/test/dorkbox/util/crypto/AesByteBufTest.java create mode 100644 Dorkbox-Util/test/dorkbox/util/crypto/AesTest.java create mode 100644 Dorkbox-Util/test/dorkbox/util/crypto/DsaTest.java create mode 100644 Dorkbox-Util/test/dorkbox/util/crypto/EccTest.java create mode 100644 Dorkbox-Util/test/dorkbox/util/crypto/RsaTest.java create mode 100644 Dorkbox-Util/test/dorkbox/util/crypto/SCryptTest.java create mode 100644 Dorkbox-Util/test/dorkbox/util/crypto/x509Test.java diff --git a/Dorkbox-Util/.classpath b/Dorkbox-Util/.classpath new file mode 100644 index 0000000..0e81c85 --- /dev/null +++ b/Dorkbox-Util/.classpath @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Dorkbox-Util/.gitignore b/Dorkbox-Util/.gitignore new file mode 100644 index 0000000..840e7d3 --- /dev/null +++ b/Dorkbox-Util/.gitignore @@ -0,0 +1 @@ +/classes/ diff --git a/Dorkbox-Util/.project b/Dorkbox-Util/.project new file mode 100644 index 0000000..0e6cf73 --- /dev/null +++ b/Dorkbox-Util/.project @@ -0,0 +1,17 @@ + + + Dorkbox-Util + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/Dorkbox-Util/Test - Dorkbox Util.launch b/Dorkbox-Util/Test - Dorkbox Util.launch new file mode 100644 index 0000000..f895eb0 --- /dev/null +++ b/Dorkbox-Util/Test - Dorkbox Util.launch @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/Dorkbox-Util/scripts/certutil_x64 b/Dorkbox-Util/scripts/certutil_x64 new file mode 100644 index 0000000000000000000000000000000000000000..451f7739be40fe89b1147588ccb4ff279b7119d0 GIT binary patch literal 151576 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 z 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ᾉt)jvjLS)JgF|-{gSo}68{Wiu|6+%3` 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�utz3rwuw^jKD4gMxHLs0Rey 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 getGenericParameterAsClassForSuperClass(Class clazz, int genericParameterToGet) { + Class classToCheck = clazz; + + // case of multiple inheritance, we are trying to get the first available generic info + // don't check for Object.class (this is where superclass is null) + while (classToCheck != Object.class) { + // check to see if we have what we are looking for on our CURRENT class + Type superClassGeneric = classToCheck.getGenericSuperclass(); + if (superClassGeneric instanceof ParameterizedType) { + Type[] actualTypeArguments = ((ParameterizedType) superClassGeneric).getActualTypeArguments(); + // is it possible? + if (actualTypeArguments.length > genericParameterToGet) { + Class rawTypeAsClass = ClassHelper.getRawTypeAsClass(actualTypeArguments[genericParameterToGet]); + return rawTypeAsClass; + } else { + // record the parameters. + + } + } + + // NO MATCH, so walk up. + classToCheck = classToCheck.getSuperclass(); + } + + classToCheck = clazz; + + // NOTHING! now check interfaces! + classToCheck = clazz; + while (classToCheck != Object.class) { + // check to see if we have what we are looking for on our CURRENT class interfaces + Type[] genericInterfaces = classToCheck.getGenericInterfaces(); + for (Type genericInterface : genericInterfaces) { + if (genericInterface instanceof ParameterizedType) { + Type[] actualTypeArguments = ((ParameterizedType) genericInterface).getActualTypeArguments(); + // is it possible? + if (actualTypeArguments.length > genericParameterToGet) { + Class rawTypeAsClass = ClassHelper.getRawTypeAsClass(actualTypeArguments[genericParameterToGet]); + return rawTypeAsClass; + } else { + // record the parameters. + + } + } + } + + + // NO MATCH, so walk up. + classToCheck = classToCheck.getSuperclass(); + } + + + // couldn't find it. + return null; + } + + /** + * Return the class that is this type. + */ + public final static Class getRawTypeAsClass(Type type) { + if (type instanceof Class) { + Class class1 = (Class)type; + +// if (class1.isArray()) { +// System.err.println("CLASS IS ARRAY TYPE: SHOULD WE DO ANYTHING WITH IT? " + class1.getSimpleName()); +// return class1.getComponentType(); +// } else { + return class1; +// } + } else if (type instanceof GenericArrayType) { + // note: cannot have primitive types here, only objects that are arrays (byte[], Integer[], etc) + Type type2 = ((GenericArrayType) type).getGenericComponentType(); + Class rawType = getRawTypeAsClass(type2); + + return Array.newInstance(rawType, 0).getClass(); + } else if (type instanceof ParameterizedType) { + // we cannot use parameterized types, because java can't go between classes and ptypes - and this + // going "in-between" is the magic -- and value -- of this entire infrastructure. + // return the type. + + return (Class) ((ParameterizedType) type).getRawType(); + +// Type[] actualTypeArguments = ((ParameterizedType) type).getActualTypeArguments(); +// return (Class) actualTypeArguments[0]; + } else if (type instanceof TypeVariable) { + // we have a COMPLEX type parameter + Type[] bounds = ((TypeVariable)type).getBounds(); + if (bounds.length > 0) { + return getRawTypeAsClass(bounds[0]); + } + } + + throw new RuntimeException("Unknown/messed up type parameter . Can't figure it out... Quit being complex!"); + } + + /** + * Check to see if clazz or interface directly has one of the interfaces defined by clazzItMustHave + *

+ * If the class DOES NOT directly have the interface it will fail. the PARENT class is not checked. + */ + public final static boolean hasInterface(Class clazzItMustHave, Class clazz) { + if (clazzItMustHave == clazz) { + return true; + } + + Class[] interfaces = clazz.getInterfaces(); + for (Class iface : interfaces) { + if (iface == clazzItMustHave) { + return true; + } + } + // now walk up to see if we can find it. + for (Class iface : interfaces) { + return hasInterface(clazzItMustHave, iface); + } + + // if we don't find it. + return false; + } + + /** + * Checks to see if the clazz is a subclass of a parent class. + * @param baseClass + * @param genericClass + */ + public static boolean hasParentClass(Class parentClazz, Class clazz) { + Class superClass = clazz.getSuperclass(); + if (parentClazz == superClass) { + return true; + } + + if (superClass != null && superClass != Object.class) { + return hasParentClass(parentClazz, superClass); + } + + return false; + } +} diff --git a/Dorkbox-Util/src/dorkbox/util/CountingLatch.java b/Dorkbox-Util/src/dorkbox/util/CountingLatch.java new file mode 100644 index 0000000..a1f7b9f --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/CountingLatch.java @@ -0,0 +1,74 @@ +package dorkbox.util; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.AbstractQueuedSynchronizer; + +public class CountingLatch { + /** + * Synchronization control for CountingLatch. Uses AQS state to represent + * count. + */ + private static final class Sync extends AbstractQueuedSynchronizer { + private static final long serialVersionUID = -2911206339865903403L; + + private Sync() {} + + private Sync(final int initialState) { + setState(initialState); + } + + int getCount() { + return getState(); + } + + @Override + protected int tryAcquireShared(final int acquires) { + return getState() == 0 ? 1 : -1; + } + + @Override + protected boolean tryReleaseShared(final int delta) { + // Decrement count; signal when transition to zero + for (;;) { + final int c = getState(); + final int nextc = c + delta; + if (nextc < 0) { + return false; + } + if (compareAndSetState(c, nextc)) { + return nextc == 0; + } + } + } + } + + private final Sync sync; + + public CountingLatch() { + this.sync = new Sync(); + } + + public CountingLatch(final int initialCount) { + this.sync = new Sync(initialCount); + } + + public void increment() { + this.sync.releaseShared(1); + } + + public int getCount() { + return this.sync.getCount(); + } + + public void decrement() { + this.sync.releaseShared(-1); + } + + public void await() throws InterruptedException { + this.sync.acquireSharedInterruptibly(1); + } + + public boolean await(final long timeout) throws InterruptedException { + return this.sync.tryAcquireSharedNanos(1, TimeUnit.MILLISECONDS.toNanos(timeout)); + } +} \ No newline at end of file diff --git a/Dorkbox-Util/src/dorkbox/util/DelayTimer.java b/Dorkbox-Util/src/dorkbox/util/DelayTimer.java new file mode 100644 index 0000000..5e5b2c5 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/DelayTimer.java @@ -0,0 +1,72 @@ +package dorkbox.util; + + +import java.util.Timer; +import java.util.TimerTask; + + +public class DelayTimer { + public interface Callback { + public void execute(); + } + + private final String name; + private final Callback listener; + + private Timer timer; + + public DelayTimer(Callback listener) { + this(null, listener); + } + + public DelayTimer(String name, Callback listener) { + this.name = name; + this.listener = listener; + } + + /** + * @return true if this timer is still waiting to run. + */ + public synchronized boolean isWaiting() { + return this.timer != null; + } + + /** + * Cancel the delay timer! + */ + public synchronized void cancel() { + if (this.timer != null) { + this.timer.cancel(); + this.timer.purge(); + this.timer = null; + } + } + + /** + * @param delay milliseconds to wait + */ + public synchronized void delay(long delay) { + cancel(); + + if (delay > 0) { + if (this.name != null) { + this.timer = new Timer(this.name, true); + } else { + this.timer = new Timer(true); + } + + + TimerTask t = new TimerTask() { + @Override + public void run() { + DelayTimer.this.listener.execute(); + cancel(); + } + }; + this.timer.schedule(t, delay); + } else { + this.listener.execute(); + this.timer = null; + } + } +} diff --git a/Dorkbox-Util/src/dorkbox/util/FileUtil.java b/Dorkbox-Util/src/dorkbox/util/FileUtil.java new file mode 100644 index 0000000..53e15f4 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/FileUtil.java @@ -0,0 +1,688 @@ +package dorkbox.util; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.io.Reader; +import java.nio.channels.FileChannel; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * File related utilities. + */ +public class FileUtil { + private static final Logger logger = LoggerFactory.getLogger(FileUtil.class); + + public static byte[] ZIP_HEADER = { 'P', 'K', 0x3, 0x4 }; + + /** + * Renames a file. Windows has all sorts of problems which are worked around. + * + * @return true if successful, false otherwise + */ + public static boolean renameTo(File source, File dest) { + // if we're on a civilized operating system we may be able to simple + // rename it + if (source.renameTo(dest)) { + return true; + } + + // fall back to trying to rename the old file out of the way, rename the + // new file into + // place and then delete the old file + if (dest.exists()) { + File temp = new File(dest.getPath() + "_old"); + if (temp.exists()) { + if (!temp.delete()) { + logger.warn("Failed to delete old intermediate file {}.", temp); + // the subsequent code will probably fail + } + } + if (dest.renameTo(temp)) { + if (source.renameTo(dest)) { + if (temp.delete()) { + logger.warn("Failed to delete intermediate file {}.", temp); + } + return true; + } + } + } + + // as a last resort, try copying the old data over the new + FileInputStream fin = null; + FileOutputStream fout = null; + try { + fin = new FileInputStream(source); + fout = new FileOutputStream(dest); + Sys.copyStream(fin, fout); + if (!source.delete()) { + logger.warn("Failed to delete {} after brute force copy to {}.", source, dest); + } + return true; + + } catch (IOException ioe) { + logger.warn("Failed to copy {} to {}.", source, dest, ioe); + return false; + + } finally { + Sys.close(fin); + Sys.close(fout); + } + } + + /** + * Reads the contents of the supplied input stream into a list of lines. + * Closes the reader on successful or failed completion. + */ + public static List readLines(Reader in) throws IOException { + List lines = new ArrayList(); + try { + BufferedReader bin = new BufferedReader(in); + for (String line = null; (line = bin.readLine()) != null; lines.add(line)) {} + } finally { + Sys.close(in); + } + return lines; + } + + /** + * Copies a files from one location to another. Overwriting any existing file at the destination. + */ + public static File copyFile(String in, String out) throws IOException { + return copyFile(new File(in), new File(out)); + } + + /** + * Copies a files from one location to another. Overwriting any existing file at the destination. + */ + public static File copyFile(File in, File out) throws IOException { + if (in == null) { + throw new IllegalArgumentException("in cannot be null."); + } + if (out == null) { + throw new IllegalArgumentException("out cannot be null."); + } + + + String normalizedIn = in.getCanonicalFile().getAbsolutePath(); + String normalizedout = out.getCanonicalFile().getAbsolutePath(); + + // if out doesn't exist, then create it. + File parentOut = out.getParentFile(); + if (!parentOut.canWrite()) { + parentOut.mkdirs(); + } + + logger.trace("Copying file: {} --> {}", in, out); + + FileChannel sourceChannel = null; + FileChannel destinationChannel = null; + try { + sourceChannel = new FileInputStream(normalizedIn).getChannel(); + destinationChannel = new FileOutputStream(normalizedout).getChannel(); + sourceChannel.transferTo(0, sourceChannel.size(), destinationChannel); + } finally { + try { + if (sourceChannel != null) { + sourceChannel.close(); + } + } catch (Exception ignored) { + } + try { + if (destinationChannel != null) { + destinationChannel.close(); + } + } catch (Exception ignored) { + } + } + + out.setLastModified(in.lastModified()); + + return out; + } + + + /** + * Moves a file, overwriting any existing file at the destination. + */ + public static File moveFile(String in, String out) throws IOException { + if (in == null || in.isEmpty()) { + throw new IllegalArgumentException("in cannot be null."); + } + if (out == null || out.isEmpty()) { + throw new IllegalArgumentException("out cannot be null."); + } + + return moveFile(new File(in), new File(out)); + } + + /** + * Moves a file, overwriting any existing file at the destination. + */ + public static File moveFile(File in, File out) throws IOException { + if (in == null) { + throw new IllegalArgumentException("in cannot be null."); + } + if (out == null) { + throw new IllegalArgumentException("out cannot be null."); + } + + System.err.println("\t\t: Moving file"); + System.err.println("\t\t: " + in.getAbsolutePath()); + System.err.println("\t\t: " + out.getAbsolutePath()); + + if (out.canRead()) { + out.delete(); + } + + boolean renameSuccess = renameTo(in, out); + if (!renameSuccess) { + throw new RuntimeException("Unable to move file: '" + in.getAbsolutePath() + "' -> '" + out.getAbsolutePath() + "'"); + } + return out; + } + + /** + * Copies a directory from one location to another + */ + public static void copyDirectory(String src, String dest, String... dirNamesToIgnore) throws IOException { + copyDirectory(new File(src), new File(dest), dirNamesToIgnore); + } + + + /** + * Copies a directory from one location to another + */ + public static void copyDirectory(File src, File dest, String... dirNamesToIgnore) throws IOException { + if (dirNamesToIgnore.length > 0) { + String name = src.getName(); + for (String ignore : dirNamesToIgnore) { + if (name.equals(ignore)) { + return; + } + } + } + + + if (src.isDirectory()) { + // if directory not exists, create it + if (!dest.exists()) { + dest.mkdir(); + logger.trace("Directory copied from {} --> {}", src, dest); + } + + // list all the directory contents + String files[] = src.list(); + + for (String file : files) { + // construct the src and dest file structure + File srcFile = new File(src, file); + File destFile = new File(dest, file); + + // recursive copy + copyDirectory(srcFile, destFile, dirNamesToIgnore); + } + } else { + // if file, then copy it + copyFile(src, dest); + } + } + + /** + * Safely moves a directory from one location to another (by copying it first, then deleting the original). + */ + public static void moveDirectory(String src, String dest, String... dirNamesToIgnore) throws IOException { + moveDirectory(new File(src), new File(dest), dirNamesToIgnore); + } + + /** + * Safely moves a directory from one location to another (by copying it first, then deleting the original). + */ + public static void moveDirectory(File src, File dest, String... dirNamesToIgnore) throws IOException { + if (dirNamesToIgnore.length > 0) { + String name = src.getName(); + for (String ignore : dirNamesToIgnore) { + if (name.equals(ignore)) { + return; + } + } + } + + + if (src.isDirectory()) { + // if directory not exists, create it + if (!dest.exists()) { + dest.mkdir(); + logger.trace("Directory copied from {} --> {}", src, dest); + } + + // list all the directory contents + String files[] = src.list(); + + for (String file : files) { + // construct the src and dest file structure + File srcFile = new File(src, file); + File destFile = new File(dest, file); + + // recursive copy + moveDirectory(srcFile, destFile, dirNamesToIgnore); + } + } else { + // if file, then copy it + moveFile(src, dest); + } + } + + /** + * Deletes a file or directory and all files and sub-directories under it. + */ + public static boolean delete(String fileName) { + if (fileName == null) { + throw new IllegalArgumentException("fileName cannot be null."); + } + + return delete(new File(fileName)); + } + + /** + * Deletes a file or directory and all files and sub-directories under it. + */ + public static boolean delete(File file) { + if (file.exists() && file.isDirectory()) { + File[] files = file.listFiles(); + for (int i = 0, n = files.length; i < n; i++) { + if (files[i].isDirectory()) { + delete(files[i].getAbsolutePath()); + } else { + logger.trace("Deleting file: {}", files[i]); + files[i].delete(); + } + } + } + logger.trace("Deleting file: {}", file); + + return file.delete(); + } + + + /** + * Creates the directories in the specified location. + */ + public static String mkdir(File location) { + if (location == null) { + throw new IllegalArgumentException("fileDir cannot be null."); + } + + String path = location.getAbsolutePath(); + if (location.mkdirs()) { + logger.trace("Created directory: {}", path); + } + + return path; + } + + /** + * Creates the directories in the specified location. + */ + public static String mkdir(String location) { + if (location == null) { + throw new IllegalArgumentException("path cannot be null."); + } + + return mkdir(new File(location)); + } + + + /** + * Creates a temp file + */ + public static File tempFile(String fileName) throws IOException { + if (fileName == null) { + throw new IllegalArgumentException("fileName cannot be null"); + } + + return File.createTempFile(fileName, null).getAbsoluteFile(); + } + + /** + * Creates a temp directory + */ + public static String tempDirectory(String directoryName) throws IOException { + if (directoryName == null) { + throw new IllegalArgumentException("directoryName cannot be null"); + } + + File file = File.createTempFile(directoryName, null); + if (!file.delete()) { + throw new IOException("Unable to delete temp file: " + file); + } + + if (!file.mkdir()) { + throw new IOException("Unable to create temp directory: " + file); + } + + return file.getAbsolutePath(); + } + + /** + * @return true if the inputStream is a zip/jar stream. DOES NOT CLOSE THE STREAM + */ + public static boolean isZipStream(InputStream in) { + if (!in.markSupported()) { + in = new BufferedInputStream(in); + } + boolean isZip = true; + try { + in.mark(ZIP_HEADER.length); + for (int i = 0; i < ZIP_HEADER.length; i++) { + if (ZIP_HEADER[i] != (byte) in.read()) { + isZip = false; + break; + } + } + in.reset(); + } catch (Exception e) { + isZip = false; + } + + return isZip; + } + + /** + * @return true if the named file is a zip/jar file + */ + public static boolean isZipFile(String fileName) { + if (fileName == null) { + throw new IllegalArgumentException("fileName cannot be null"); + } + + return isZipFile(new File(fileName)); + } + + /** + * @return true if the file is a zip/jar file + */ + public static boolean isZipFile(File file) { + boolean isZip = true; + byte[] buffer = new byte[ZIP_HEADER.length]; + + RandomAccessFile raf = null; + try { + raf = new RandomAccessFile(file, "r"); + raf.readFully(buffer); + for (int i = 0; i < ZIP_HEADER.length; i++) { + if (buffer[i] != ZIP_HEADER[i]) { + isZip = false; + break; + } + } + } catch (Exception e) { + isZip = false; + } finally { + if (raf != null) { + try { + raf.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return isZip; + } + + + /** + * Unzips a ZIP file + * + * @return The path to the output directory. + */ + public static void unzip(String zipFile, String outputDir) throws IOException { + unzipJar(zipFile, outputDir, true); + } + + /** + * Unzips a ZIP file + * + * @return The path to the output directory. + */ + public static void unzip(File zipFile, File outputDir) throws IOException { + unzipJar(zipFile, outputDir, true); + } + + /** + * Unzips a ZIP file + * + * @return The path to the output directory. + */ + public static void unzipJar(String zipFile, String outputDir, boolean extractManifest) throws IOException { + if (zipFile == null) { + throw new IllegalArgumentException("zipFile cannot be null."); + } + if (outputDir == null) { + throw new IllegalArgumentException("outputDir cannot be null."); + } + + unjarzip0(new File(zipFile), new File(outputDir), extractManifest); + } + + /** + * Unzips a ZIP file + * + * @return The path to the output directory. + */ + public static void unzipJar(File zipFile, File outputDir, boolean extractManifest) throws IOException { + if (zipFile == null) { + throw new IllegalArgumentException("zipFile cannot be null."); + } + if (outputDir == null) { + throw new IllegalArgumentException("outputDir cannot be null."); + } + + unjarzip0(zipFile, outputDir, extractManifest); + } + + + + /** + * Unzips a ZIP or JAR file (and handles the manifest if requested) + */ + private static void unjarzip0(File zipFile, File outputDir, boolean extractManifest) throws IOException { + if (zipFile == null) { + throw new IllegalArgumentException("zipFile cannot be null."); + } + if (outputDir == null) { + throw new IllegalArgumentException("outputDir cannot be null."); + } + + long fileLength = zipFile.length(); + if (fileLength > Integer.MAX_VALUE - 1) { + throw new RuntimeException("Source filesize is too large!"); + } + + + ZipInputStream inputStrem = new ZipInputStream(new FileInputStream(zipFile)); + try { + while (true) { + ZipEntry entry = inputStrem.getNextEntry(); + if (entry == null) { + break; + } + + String name = entry.getName(); + + if (!extractManifest && name.startsWith("META-INF/")) { + continue; + } + + File file = new File(outputDir, name); + if (entry.isDirectory()) { + mkdir(file.getPath()); + continue; + } + mkdir(file.getParent()); + + + FileOutputStream output = new FileOutputStream(file); + try { + Sys.copyStream(inputStrem, output); + } finally { + output.close(); + } + } + } finally { + inputStrem.close(); + } + } + + + /** + * Parses the specified root directory for ALL files that are in it. All of the sub-directories are searched as well. + *

+ * This is different, in that it returns ALL FILES, instead of ones that just match a specific extension. + * @return the list of all files in the root+sub-dirs. + */ + public static List parseDir(File rootDirectory) { + return parseDir(rootDirectory, (String) null); + } + + /** + * Parses the specified root directory for files that end in the extension to match. All of the sub-directories are searched as well. + * @return the list of all files in the root+sub-dirs that match the given extension. + */ + public static List parseDir(File rootDirectory, String... extensionsToMatch) { + List jarList = new LinkedList(); + LinkedList directories = new LinkedList(); + + if (rootDirectory.isDirectory()) { + directories.add(rootDirectory); + + while (directories.peek() != null) { + File dir = directories.poll(); + File[] listFiles = dir.listFiles(); + for (File file : listFiles) { + if (file.isDirectory()) { + directories.add(file); + } else { + if (extensionsToMatch == null) { + jarList.add(file); + } else { + for (String e : extensionsToMatch) { + if (file.getAbsolutePath().endsWith(e)) { + jarList.add(file); + } + } + } + } + } + } + } else { + System.err.println("Cannot search dependencies, if it's a file name!"); + } + + + return jarList; + } + + /** + * Gets the relative path of a file to a specific directory in it's hierarchy. + * + * For example: getRelativeToDir("/a/b/c/d/e.bah", "c") -> "d/e.bah" + */ + public static String getRelativeToDir(String fileName, String dirInHeirarchy) { + if (fileName == null || fileName.isEmpty()) { + throw new IllegalArgumentException("fileName cannot be null."); + } + + return getRelativeToDir(new File(fileName), dirInHeirarchy); + } + + /** + * Gets the relative path of a file to a specific directory in it's hierarchy. + * + * For example: getRelativeToDir("/a/b/c/d/e.bah", "c") -> "d/e.bah" + * @return null if it cannot be found + */ + public static String getRelativeToDir(File file, String dirInHeirarchy) { + if (file == null) { + throw new IllegalArgumentException("file cannot be null."); + } + + String absolutePath = file.getAbsolutePath(); + + File parent = file; + String parentName; + while ((parent = parent.getParentFile()) != null) { + parentName = parent.getName(); + + if (parentName.equals(dirInHeirarchy)) { + parentName = parent.getAbsolutePath(); + + return absolutePath.substring(parentName.length() + 1); + } + } + + return null; + } + + /** + * Extracts a file from a zip into a TEMP file, if possible. The TEMP file is deleted upon JVM exit. + * + * @throws IOException + * @return the location of the extracted file, or NULL if the file cannot be extracted or doesn't exist. + */ + public static String extractFromZip(String zipFile, String fileToExtract) throws IOException { + if (zipFile == null) { + throw new IllegalArgumentException("file cannot be null."); + } + + if (fileToExtract == null) { + throw new IllegalArgumentException("fileToExtract cannot be null."); + } + + ZipInputStream inputStrem = new ZipInputStream(new FileInputStream(zipFile)); + try { + while (true) { + ZipEntry entry = inputStrem.getNextEntry(); + if (entry == null) { + break; + } + + String name = entry.getName(); + if (entry.isDirectory()) { + continue; + } + + if (name.equals(fileToExtract)) { + File tempFile = FileUtil.tempFile(name); + tempFile.deleteOnExit(); + + FileOutputStream output = new FileOutputStream(tempFile); + try { + Sys.copyStream(inputStrem, output); + } finally { + output.close(); + } + + return tempFile.getAbsolutePath(); + } + } + } finally { + inputStrem.close(); + } + + return null; + } +} diff --git a/Dorkbox-Util/src/dorkbox/util/InputConsole.java b/Dorkbox-Util/src/dorkbox/util/InputConsole.java new file mode 100644 index 0000000..c9c4e16 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/InputConsole.java @@ -0,0 +1,342 @@ +package dorkbox.util; + + +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.util.concurrent.atomic.AtomicBoolean; + +import jline.IDE_Terminal; +import jline.Terminal; +import jline.console.ConsoleReader; + +import org.fusesource.jansi.Ansi; +import org.fusesource.jansi.AnsiConsole; + +public class InputConsole { + private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(InputConsole.class); + private static final InputConsole consoleProxyReader; + private static final char[] emptyLine = new char[0]; + + static { + consoleProxyReader = new InputConsole(); + // setup (if necessary) the JLINE console logger. + // System.setProperty("jline.internal.Log.trace", "TRUE"); + // System.setProperty("jline.internal.Log.debug", "TRUE"); + } + + /** + * empty method to allow code to initialize the input console. + */ + public static void init() { + } + + public static final void destroy() { + consoleProxyReader.destroy0(); + } + + /** return null if no data */ + public static final String readLine() { + char[] line = consoleProxyReader.readLine0(); + return new String(line); + } + + /** return -1 if no data */ + public static final int read() { + return consoleProxyReader.read0(); + } + + /** return null if no data */ + public static final char[] readLinePassword() { + return consoleProxyReader.readLinePassword0(); + } + + public static InputStream getInputStream() { + return new InputStream() { + @Override + public int read() throws IOException { + return consoleProxyReader.read0(); + } + + @Override + public void close() throws IOException { + consoleProxyReader.release0(); + } + }; + } + + public static void echo(boolean enableEcho) { + consoleProxyReader.echo0(enableEcho); + } + + public static boolean echo() { + return consoleProxyReader.echo0(); + } + + + private ConsoleReader jlineReader; + + private final Object inputLockSingle = new Object(); + private final Object inputLockLine = new Object(); + + private AtomicBoolean isRunning = new AtomicBoolean(false); + private AtomicBoolean isInShutdown = new AtomicBoolean(false); + private volatile char[] readLine = null; + private volatile int readChar = -1; + + private boolean isIDE; + + // the streams are ALREADY buffered! + // + private InputConsole() { + try { + this.jlineReader = new ConsoleReader(); + + Terminal terminal = this.jlineReader.getTerminal(); + terminal.setEchoEnabled(true); + this.isIDE = terminal instanceof IDE_Terminal; + + if (this.isIDE) { + logger.debug("Terminal is in IDE (best guess). Unable to support single key input. Only line input available."); + } else { + logger.debug("Terminal Type: {}", terminal.getClass().getSimpleName()); + } + } catch (UnsupportedEncodingException ignored) { + } catch (IOException ignored) { + } + } + + /** + * make sure the input console reader thread is started. + */ + private void startInputConsole() { + // protected by atomic! + if (!this.isRunning.compareAndSet(false, true) || this.isInShutdown.get()) { + return; + } + + Thread consoleThread = new Thread(new Runnable() { + @Override + public void run() { + consoleProxyReader.run(); + } + }); + consoleThread.setDaemon(true); + consoleThread.setName("Console Input Reader"); + + consoleThread.start(); + } + + private void destroy0() { + // Don't change this, because we don't want to enable reading, etc from this once it's destroyed. + // isRunning.set(false); + + if (this.isInShutdown.compareAndSet(true, true)) { + return; + } + + synchronized (this.inputLockSingle) { + this.inputLockSingle.notifyAll(); + } + + synchronized (this.inputLockLine) { + this.inputLockLine.notifyAll(); + } + + // we want to make sure this happens in a new thread, since this can BLOCK our main event dispatch thread + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + InputConsole.this.jlineReader.shutdown(); + InputConsole.this.jlineReader = null; + }}); + thread.setDaemon(true); + thread.setName("Console Input Shutdown"); + thread.start(); + } + + private void echo0(boolean enableEcho) { + if (this.jlineReader != null) { + Terminal terminal = this.jlineReader.getTerminal(); + if (terminal != null) { + terminal.setEchoEnabled(enableEcho); + } + } + } + + private boolean echo0() { + if (this.jlineReader != null) { + Terminal terminal = this.jlineReader.getTerminal(); + if (terminal != null) { + return terminal.isEchoEnabled(); + } + } + return false; + } + + + /** return null if no data */ + private final char[] readLine0() { + startInputConsole(); + + synchronized (this.inputLockLine) { + try { + this.inputLockLine.wait(); + } catch (InterruptedException e) { + return emptyLine; + } + } + return this.readLine; + } + + /** return null if no data */ + private final char[] readLinePassword0() { + startInputConsole(); + + // don't bother in an IDE. it won't work. + return readLine0(); + } + + /** return -1 if no data */ + private final int read0() { + startInputConsole(); + + synchronized (this.inputLockSingle) { + try { + this.inputLockSingle.wait(); + } catch (InterruptedException e) { + return -1; + } + } + return this.readChar; + } + + /** + * releases any thread still waiting. + */ + private void release0() { + synchronized (this.inputLockSingle) { + this.inputLockSingle.notifyAll(); + } + + synchronized (this.inputLockLine) { + this.inputLockLine.notifyAll(); + } + } + + private final void run() { + if (this.jlineReader == null) { + logger.error("Unable to start Console Reader"); + return; + } + + // if we are eclipse, we MUST do this per line! (per character DOESN'T work.) + if (this.isIDE) { + try { + while ((this.readLine = this.jlineReader.readLine()) != null) { + // notify everyone waiting for a line of text. + synchronized (this.inputLockSingle) { + if (this.readLine.length > 0) { + this.readChar = this.readLine[0]; + } else { + this.readChar = -1; + } + this.inputLockSingle.notifyAll(); + } + synchronized (this.inputLockLine) { + this.inputLockLine.notifyAll(); + } + } + } catch (Exception ignored) { + ignored.printStackTrace(); + } + } else { + + try { + final boolean ansiEnabled = Ansi.isEnabled(); + Ansi ansi = Ansi.ansi(); + + int typedChar; + StringBuilder buf = new StringBuilder(); + + // don't type ; in a bash shell, it quits everything + // \n is replaced by \r in unix terminal? + while ((typedChar = this.jlineReader.readCharacter()) != -1) { + char asChar = (char) typedChar; + + logger.trace("READ: {} ({})", asChar, typedChar); + + // notify everyone waiting for a character. + synchronized (this.inputLockSingle) { + this.readChar = typedChar; + this.inputLockSingle.notifyAll(); + } + + // if we type a backspace key, swallow it + previous in READLINE. READCHAR will have it passed. + if (typedChar == 127) { + int position = 0; + + // clear ourself + one extra. + if (ansiEnabled) { + int amtToBackspace = 2; // ConsoleReader.getPrintableCharacters(typedChar).length(); + int length = buf.length(); + if (length > 1) { + char charAt = buf.charAt(length-1); + amtToBackspace += ConsoleReader.getPrintableCharacters(charAt).length(); + buf.delete(length-1, length); + + length--; + + // now figure out where the cursor is at. + for (int i=0;i 0) { + this.readLine = new char[length]; + buf.getChars(0, length, this.readLine, 0); + } else { + this.readLine = emptyLine; + } + + this.inputLockLine.notifyAll(); + } + + // dump the characters in the backing array (slightly safer for passwords when using this method) + if (length > 0) { + buf.delete(0, buf.length()); + } + } else if (asChar != '\r') { + // only append if we are not a new line. + buf.append(asChar); + } + } + } catch (IOException ignored) { + } + } + } +} \ No newline at end of file diff --git a/Dorkbox-Util/src/dorkbox/util/RegExp.java b/Dorkbox-Util/src/dorkbox/util/RegExp.java new file mode 100644 index 0000000..3f73f90 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/RegExp.java @@ -0,0 +1,38 @@ +package dorkbox.util; + +public class RegExp { + private static final String whitespace_chars = "" /* dummy empty string for homogeneity */ + + "\\u0009" // CHARACTER TABULATION + + "\\u000A" // LINE FEED (LF) + + "\\u000B" // LINE TABULATION + + "\\u000C" // FORM FEED (FF) + + "\\u000D" // CARRIAGE RETURN (CR) + + "\\u0020" // SPACE + + "\\u0085" // NEXT LINE (NEL) + + "\\u00A0" // NO-BREAK SPACE + + "\\u1680" // OGHAM SPACE MARK + + "\\u180E" // MONGOLIAN VOWEL SEPARATOR + + "\\u2000" // EN QUAD + + "\\u2001" // EM QUAD + + "\\u2002" // EN SPACE + + "\\u2003" // EM SPACE + + "\\u2004" // THREE-PER-EM SPACE + + "\\u2005" // FOUR-PER-EM SPACE + + "\\u2006" // SIX-PER-EM SPACE + + "\\u2007" // FIGURE SPACE + + "\\u2008" // PUNCTUATION SPACE + + "\\u2009" // THIN SPACE + + "\\u200A" // HAIR SPACE + + "\\u2028" // LINE SEPARATOR + + "\\u2029" // PARAGRAPH SEPARATOR + + "\\u202F" // NARROW NO-BREAK SPACE + + "\\u205F" // MEDIUM MATHEMATICAL SPACE + + "\\u3000" // IDEOGRAPHIC SPACE + ; + + /* A \s that actually works for Java’s native character set: Unicode */ + public static final String whitespace_charclass = "[" + whitespace_chars + "]"; + + /* A \S that actually works for Java’s native character set: Unicode */ + public static final String not_whitespace_charclass = "[^" + whitespace_chars + "]"; +} diff --git a/Dorkbox-Util/src/dorkbox/util/Storage.java b/Dorkbox-Util/src/dorkbox/util/Storage.java new file mode 100644 index 0000000..e211327 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/Storage.java @@ -0,0 +1,336 @@ +package dorkbox.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.lang.ref.WeakReference; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; + +/** + * Nothing spectacular about this storage -- it allows for persistent storage of objects to disk. + */ +public class Storage { + // TODO: add snappy compression to storage objects?? + + private static final Logger logger = LoggerFactory.getLogger(Storage.class); + + private static Map storages = new HashMap(1); + + private final File file; + + private long milliSeconds = 3000L; + private DelayTimer timer; + private WeakReference objectReference; + + @SuppressWarnings({"rawtypes","unchecked"}) + public static Storage load(File file, Object loadIntoObject) { + if (file == null) { + throw new IllegalArgumentException("file cannot be null!"); + } + + if (loadIntoObject == null) { + throw new IllegalArgumentException("loadIntoObject cannot be null!"); + } + + + // if we load from a NEW storage at the same location as an ALREADY EXISTING storage, + // without saving the existing storage first --- whoops! + synchronized (storages) { + Storage storage = storages.get(file); + if (storage != null) { + boolean waiting = storage.timer.isWaiting(); + if (waiting) { + storage.saveNow(); + } + + // why load it from disk again? just copy out the values! + synchronized (storage) { + // have to load from disk! + Object source = load(file, loadIntoObject.getClass()); + + Object orig = storage.objectReference.get(); + if (orig != null) { + if (orig != loadIntoObject) { + storage.objectReference = new WeakReference(loadIntoObject); + } + + } else { + // whoopie - the old one got GC'd! (for whatever reason, it can be legit) + storage.objectReference = new WeakReference(loadIntoObject); + } + + if (source != null) { + copyFields(source, loadIntoObject); + } + } + } else { + // this will load it from disk again, if necessary + storage = new Storage(file, loadIntoObject); + storages.put(file, storage); + + // have to load from disk! + Object source = load(file, loadIntoObject.getClass()); + if (source != null) { + copyFields(source, loadIntoObject); + } + } + return storage; + } + } + + + /** + * Also loads the saved object into the passed-in object. This is sorta slow (nothing is cached for speed!) + * + * If the saved object has more fields than the loadIntoObject, only the fields in loadIntoObject will be + * populated. If the loadIntoObject has more fields than the saved object, then the loadIntoObject will not + * have those fields changed. + */ + @SuppressWarnings({"rawtypes","unchecked"}) + private Storage(File file, Object loadIntoObject) { + this.file = file.getAbsoluteFile(); + File parentFile = this.file.getParentFile(); + if (parentFile != null) { + parentFile.mkdirs(); + } + + this.objectReference = new WeakReference(loadIntoObject); + this.timer = new DelayTimer("Storage Writer", new DelayTimer.Callback() { + @Override + public void execute() { + save0(); + } + }); + } + + /** + * Loads the saved object into the passed-in object. This is sorta slow (nothing is cached for speed!) + * + * If the saved object has more fields than the loadIntoObject, only the fields in loadIntoObject will be + * populated. If the loadIntoObject has more fields than the saved object, then the loadIntoObject will not + * have those fields changed. + */ + public final void load(Object loadIntoObject) { + if (loadIntoObject == null) { + throw new IllegalArgumentException("loadIntoObject cannot be null!"); + } + + + // if we load from a NEW storage at the same location as an ALREADY EXISTING storage, + // without saving the existing storage first --- whoops! + synchronized (storages) { + File file2 = this.file; + + Storage storage = storages.get(file2); + Object source = null; + if (storage != null) { + boolean waiting = storage.timer.isWaiting(); + if (waiting) { + storage.saveNow(); + } + + // why load it from disk again? just copy out the values! + source = storage.objectReference.get(); + if (source == null) { + // have to load from disk! + source = load(file2, loadIntoObject.getClass()); + } + } + + if (source != null) { + copyFields(source, loadIntoObject); + } + } + } + + /** + * @param delay milliseconds to wait + */ + public final void setSaveDelay(long milliSeconds) { + this.milliSeconds = milliSeconds; + } + + /** + * Immediately save the storage to disk + */ + public final synchronized void saveNow() { + this.timer.delay(0L); + } + + /** + * Save the storage to disk, once xxxx milli-seconds have passed. + * This is to help prevent thrashing the disk, or wearing it out on multiple, rapid, changes. + */ + public final synchronized void save() { + this.timer.delay(this.milliSeconds); + } + + private synchronized void save0() { + Object object = Storage.this.objectReference.get(); + + if (object == null) { + Storage.logger.error("Object has been erased and is no longer available to save!"); + return; + } + + RandomAccessFile raf = null; + Output output = null; + try { + raf = new RandomAccessFile(this.file, "rw"); + FileOutputStream outputStream = new FileOutputStream(raf.getFD()); + output = new Output(outputStream, 1024); // write 1024 at a time + + + Kryo kryo = new Kryo(); + kryo.setRegistrationRequired(false); + kryo.writeObject(output, object); + output.flush(); + } catch (Exception e) { + Storage.logger.error("Error saving the data!", e); + } finally { + if (output != null) { + output.close(); + } + if (raf != null) { + try { + raf.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + @SuppressWarnings("unchecked") + private static T load(File file, Class clazz) { + if (file.length() == 0) { + return null; + } + + RandomAccessFile raf = null; + Input input = null; + try { + raf = new RandomAccessFile(file, "r"); + input = new Input(new FileInputStream(raf.getFD()), 1024); // read 1024 at a time + + Kryo kryo = new Kryo(); + kryo.setRegistrationRequired(false); + Object readObject = kryo.readObject(input, clazz); + return (T) readObject; + } catch (Exception e) { + logger.error("Error reading from '{}'! Perhaps the file is corrupt?", file.getAbsolutePath()); + return null; + } finally { + if (input != null) { + input.close(); + } + if (raf != null) { + try { + raf.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (this.file == null ? 0 : this.file.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Storage other = (Storage) obj; + if (this.file == null) { + if (other.file != null) { + return false; + } + } else if (!this.file.equals(other.file)) { + return false; + } + return true; + } + + + @Override + public String toString() { + return "Storage [" + this.file + "]"; + } + + + private static void copyFields(Object source, Object dest) { + Class sourceClass = source.getClass(); + Field[] destFields = dest.getClass().getDeclaredFields(); + + for (Field destField : destFields) { + String name = destField.getName(); + try { + Field sourceField = sourceClass.getDeclaredField(name); + destField.setAccessible(true); + sourceField.setAccessible(true); + + Object sourceObj = sourceField.get(source); + + if (sourceObj instanceof Map) { + Object destObj = destField.get(dest); + if (destObj == null) { + destField.set(dest, sourceObj); + } else if (destObj instanceof Map) { + @SuppressWarnings("unchecked") + Map sourceMap = (Map) sourceObj; + @SuppressWarnings("unchecked") + Map destMap = (Map) destObj; + + destMap.clear(); + Iterator entries = sourceMap.entrySet().iterator(); + while (entries.hasNext()) { + Map.Entry entry = (Map.Entry)entries.next(); + Object key = entry.getKey(); + Object value = entry.getValue(); + destMap.put(key, value); + } + + } else { + logger.error("Incompatible field type! '{}'", name); + } + } else { + destField.set(dest, sourceObj); + } + } catch (Exception e) { + logger.error("Unable to copy field: {}", name, e); + } + } + } + + public static void shutdown() { + synchronized(storages) { + storages.clear(); + } + } +} diff --git a/Dorkbox-Util/src/dorkbox/util/Sys.java b/Dorkbox-Util/src/dorkbox/util/Sys.java new file mode 100644 index 0000000..02fcf30 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/Sys.java @@ -0,0 +1,657 @@ +package dorkbox.util; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.LinkedList; +import java.util.List; + +import org.bouncycastle.crypto.digests.SHA256Digest; + +public class Sys { + public static final int KILOBYTE = 1024; + public static final int MEGABYTE = 1024 * KILOBYTE; + public static final int GIGABYTE = 1024 * MEGABYTE; + public static final long TERABYTE = 1024L * GIGABYTE; + + public static char[] HEX_CHARS = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + + public static final char[] convertStringToChars(String string) { + char[] charArray = string.toCharArray(); + + eraseString(string); + + return charArray; + } + + + public static final void eraseString(String string) { +// You can change the value of the inner char[] using reflection. +// +// You must be careful to either change it with an array of the same length, +// or to also update the count field. +// +// If you want to be able to use it as an entry in a set or as a value in map, +// you will need to recalculate the hash code and set the value of the hashCode field. + + try { + Field valueField = String.class.getDeclaredField("value"); + valueField.setAccessible(true); + char[] chars = (char[]) valueField.get(string); + Arrays.fill(chars, '*'); // asterisk it out in case of GC not picking up the old char array. + + valueField.set(string, new char[0]); // replace it. + + // set count to 0 + Field countField = String.class.getDeclaredField("count"); + countField.setAccessible(true); + countField.set(string, 0); + + // set hash to 0 + Field hashField = String.class.getDeclaredField("hash"); + hashField.setAccessible(true); + hashField.set(string, 0); + } catch (SecurityException e) { + e.printStackTrace(); + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + + public static String getSizePretty(final long size) { + if (size > TERABYTE) { + return String.format("%2.2fTB", (float) size / TERABYTE); + } + if (size > GIGABYTE) { + return String.format("%2.2fGB", (float) size / GIGABYTE); + } + if (size > MEGABYTE) { + return String.format("%2.2fMB", (float) size / MEGABYTE); + } + if (size > KILOBYTE) { + return String.format("%2.2fKB", (float) size / KILOBYTE); + } + + return String.valueOf(size) + "B"; + } + + /** + * Convenient close for a stream. + */ + public static void close(InputStream inputStream) { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException ioe) { + System.err.println("Error closing the input stream:" + inputStream); + ioe.printStackTrace(); + } + } + } + + /** + * Convenient close for a stream. + */ + public static void close(OutputStream outputStream) { + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException ioe) { + System.err.println("Error closing the output stream:" + outputStream); + ioe.printStackTrace(); + } + } + } + + /** + * Convenient close for a Reader. + */ + public static void close(Reader inputReader) { + if (inputReader != null) { + try { + inputReader.close(); + } catch (IOException ioe) { + System.err.println("Error closing input reader: " + inputReader); + ioe.printStackTrace(); + } + } + } + + /** + * Convenient close for a Writer. + */ + public static void close(Writer outputWriter) { + if (outputWriter != null) { + try { + outputWriter.close(); + } catch (IOException ioe) { + System.err.println("Error closing output writer: " + outputWriter); + ioe.printStackTrace(); + } + } + } + + /** + * Copy the contents of the input stream to the output stream. + *

+ * DOES NOT CLOSE THE STEAMS! + */ + public static T copyStream(InputStream inputStream, T outputStream) throws IOException { + byte[] buffer = new byte[4096]; + int read = 0; + while ((read = inputStream.read(buffer)) > 0) { + outputStream.write(buffer, 0, read); + } + + return outputStream; + } + + /** + * Convert the contents of the input stream to a byte array. + */ + public static byte[] getBytesFromStream(InputStream inputStream) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(8192); + + byte[] buffer = new byte[4096]; + int read = 0; + while ((read = inputStream.read(buffer)) > 0) { + baos.write(buffer, 0, read); + } + baos.flush(); + inputStream.close(); + + return baos.toByteArray(); + } + + public static final byte[] arrayCloneBytes(byte[] src) { + return arrayCloneBytes(src, 0); + } + + public static final byte[] arrayCloneBytes(byte[] src, int position) { + int length = src.length - position; + + byte[] b = new byte[length]; + System.arraycopy(src, position, b, 0, length); + return b; + } + + public static final byte[] concatBytes(byte[]... arrayBytes) { + int length = 0; + for (byte[] bytes : arrayBytes) { + length += bytes.length; + } + + byte[] concatBytes = new byte[length]; + + length = 0; + for (byte[] bytes : arrayBytes) { + System.arraycopy(bytes, 0, concatBytes, length, bytes.length); + length += bytes.length; + } + + return concatBytes; + } + + /** gets the SHA256 hash + SALT of the specified username, as UTF-16 */ + public static final byte[] getSha256WithSalt(String username, byte[] saltBytes) { + if (username == null) { + return null; + } + + byte[] charToBytes = Sys.charToBytes(username.toCharArray()); + byte[] userNameWithSalt = Sys.concatBytes(charToBytes, saltBytes); + + + SHA256Digest sha256 = new SHA256Digest(); + byte[] usernameHashBytes = new byte[sha256.getDigestSize()]; + sha256.update(userNameWithSalt, 0, userNameWithSalt.length); + sha256.doFinal(usernameHashBytes, 0); + + return usernameHashBytes; + } + + /** gets the SHA256 hash of the specified string, as UTF-16 */ + public static final byte[] getSha256(String string) { + byte[] charToBytes = Sys.charToBytes(string.toCharArray()); + + SHA256Digest sha256 = new SHA256Digest(); + byte[] usernameHashBytes = new byte[sha256.getDigestSize()]; + sha256.update(charToBytes, 0, charToBytes.length); + sha256.doFinal(usernameHashBytes, 0); + + return usernameHashBytes; + } + + /** gets the SHA256 hash of the specified byte array */ + public static final byte[] getSha256(byte[] bytes) { + + SHA256Digest sha256 = new SHA256Digest(); + byte[] hashBytes = new byte[sha256.getDigestSize()]; + sha256.update(bytes, 0, bytes.length); + sha256.doFinal(hashBytes, 0); + + return hashBytes; + } + + /** this saves the char array in UTF-16 format of bytes */ + public static final byte[] charToBytes(char[] text) { + // NOTE: this saves the char array in UTF-16 format of bytes. + byte[] bytes = new byte[text.length*2]; + for(int i=0; i>8); + bytes[2*i+1] = (byte) (text[i] & 0x00FF); + } + + return bytes; + } + + + public static final byte[] intsToBytes(int[] ints) { + int length = ints.length; + byte[] bytes = new byte[length]; + + for (int i = 0; i < length; i++) { + int intValue = ints[i]; + if (intValue < 0 || intValue > 255) { + System.err.println("WARNING: int at index " + i + "(" + intValue + ") was not a valid byte value (0-255)"); + return new byte[length]; + } + + bytes[i] = (byte)intValue; + } + + return bytes; + } + + public static final int[] bytesToInts(byte[] bytes) { + int length = bytes.length; + int[] ints = new int[length]; + + for (int i = 0; i < length; i++) { + ints[i] = bytes[i] & 0xFF; + } + + return ints; + } + + public static final String bytesToHex(byte[] bytes) { + return bytesToHex(bytes, false); + } + + public static final String bytesToHex(byte[] bytes, boolean padding) { + if (padding) { + char[] hexString = new char[3 * bytes.length]; + int j = 0; + + for (int i = 0; i < bytes.length; i++) { + hexString[j++] = HEX_CHARS[(bytes[i] & 0xF0) >> 4]; + hexString[j++] = HEX_CHARS[bytes[i] & 0x0F]; + hexString[j++] = ' '; + } + + return new String(hexString); + } else { + char[] hexString = new char[2 * bytes.length]; + int j = 0; + + for (int i = 0; i < bytes.length; i++) { + hexString[j++] = HEX_CHARS[(bytes[i] & 0xF0) >> 4]; + hexString[j++] = HEX_CHARS[bytes[i] & 0x0F]; + } + + return new String(hexString); + } + } + + /** + * Converts an ASCII character representing a hexadecimal + * value into its integer equivalent. + */ + public static final int hexByteToInt(byte b) { + switch (b) { + case '0' : + return 0; + case '1' : + return 1; + case '2' : + return 2; + case '3' : + return 3; + case '4' : + return 4; + case '5' : + return 5; + case '6' : + return 6; + case '7' : + return 7; + case '8' : + return 8; + case '9' : + return 9; + case 'A' : + case 'a' : + return 10; + case 'B' : + case 'b' : + return 11; + case 'C' : + case 'c' : + return 12; + case 'D' : + case 'd' : + return 13; + case 'E' : + case 'e' : + return 14; + case 'F' : + case 'f' : + return 15; + default : + throw new IllegalArgumentException("Error decoding byte"); + } + } + + /** + * A 4-digit hex result. + */ + public static final void hex4(char c, StringBuilder sb) { + sb.append(HEX_CHARS[(c & 0xF000) >> 12]); + sb.append(HEX_CHARS[(c & 0x0F00) >> 8]); + sb.append(HEX_CHARS[(c & 0x00F0) >> 4]); + sb.append(HEX_CHARS[c & 0x000F]); + } + + /** + * Returns a string representation of the byte array as a series of + * hexadecimal characters. + * + * @param bytes + * byte array to convert + * @return a string representation of the byte array as a series of + * hexadecimal characters + */ + public static final String toHexString(byte[] bytes) { + char[] hexString = new char[2 * bytes.length]; + int j = 0; + + for (int i = 0; i < bytes.length; i++) { + hexString[j++] = HEX_CHARS[(bytes[i] & 0xF0) >> 4]; + hexString[j++] = HEX_CHARS[bytes[i] & 0x0F]; + } + + return new String(hexString); + } + + /** + * XOR two byte arrays together, and save result in originalArray + * + * @param originalArray this is the base of the XOR operation. + * @param keyArray this is XOR'd into the original array, repeats if necessary. + */ + public static void xorArrays(byte[] originalArray, byte[] keyArray) { + int keyIndex = 0; + int keyLength = keyArray.length; + + for (int i=0;i array) { + int length = 0; + for (String s : array) { + byte[] bytes = s.getBytes(); + if (bytes != null) { + length += bytes.length; + } + } + + if (length == 0) { + return new byte[0]; + } + + byte[] bytes = new byte[length+array.size()]; + + length = 0; + for (String s : array) { + byte[] sBytes = s.getBytes(); + System.arraycopy(sBytes, 0, bytes, length, sBytes.length); + length += sBytes.length; + bytes[length++] = (byte) 0x01; + } + + return bytes; + } + + public static final ArrayList decodeStringArray(byte[] bytes) { + int length = bytes.length; + int position = 0; + byte token = (byte) 0x01; + ArrayList list = new ArrayList(0); + + int last = 0; + while (last+position < length) { + byte b = bytes[last+position++]; + if (b == token ) { + byte[] xx = new byte[position-1]; + System.arraycopy(bytes, last, xx, 0, position-1); + list.add(new String(xx)); + last += position; + position = 0; + } + + } + + return list; + } + + public static String printArrayRaw(byte[] bytes) { + return printArrayRaw(bytes, 0); + } + + public static String printArrayRaw(byte[] bytes, int lineLength) { + if (lineLength > 0) { + int mod = lineLength; + int length = bytes.length; + int comma = length-1; + + StringBuilder builder = new StringBuilder(length + length/mod); + for (int i = 0; i < length; i++) { + builder.append(bytes[i]); + if (i < comma) { + builder.append(","); + } + if (i > 0 && i%mod == 0) { + builder.append(OS.LINE_SEPARATOR); + } + } + + return builder.toString(); + + } else { + int length = bytes.length; + int comma = length-1; + + StringBuilder builder = new StringBuilder(length + length); + for (int i = 0; i < length; i++) { + builder.append(bytes[i]); + if (i < comma) { + builder.append(","); + } + } + + return builder.toString(); + } + } + + public static void printArray(byte[] bytes) { + printArray(bytes, bytes.length, true); + } + + public static void printArray(byte[] bytes, int length, boolean includeByteCount) { + if (includeByteCount) { + System.err.println("Bytes: " + length); + } + + int mod = 40; + int comma = length-1; + + StringBuilder builder = new StringBuilder(length + length/mod); + for (int i = 0; i < length; i++) { + builder.append(bytes[i]); + if (i < comma) { + builder.append(","); + } + if (i > 0 && i%mod == 0) { + builder.append(OS.LINE_SEPARATOR); + } + } + + System.err.println(builder.toString()); + } + + /** + * Finds a list of classes that are annotated with the specified annotation. + */ + public static final List> findAnnotatedClasses(Class annotation) { + return findAnnotatedClasses("", annotation); + } + + /** + * Finds a list of classes in the specific package that are annotated with the specified annotation. + */ + public static final List> findAnnotatedClasses(String packageName, Class annotation) { + // find ALL ServerLoader classes and use reflection to load them. + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + + if (packageName != null && !packageName.isEmpty()) { + packageName = packageName.replace('.', '/'); + } + + // look for all annotated classes in the projects package. + try { + LinkedList> classesThatWereAnnotated = new LinkedList>(); + + URL url; + Enumeration resources = classLoader.getResources(packageName); + + // this means we want to search EVERYTHING + if (packageName == null || packageName.isEmpty()) { + // lengthy, but it will traverse how we want. + while (resources.hasMoreElements()) { + url = resources.nextElement(); + + // go through and look at the subdirs there. run a package search on THOSE. + File urlFile = new File(url.getPath()); + File[] listFiles = urlFile.listFiles(); + if (listFiles != null) { + for (File file : listFiles) { + if (file.isDirectory()) { + findSubClasses(classLoader, null, annotation, file, urlFile.getAbsolutePath(), classesThatWereAnnotated); + } + } + } + } + } else { + // lengthy, but it will traverse how we want. + while (resources.hasMoreElements()) { + url = resources.nextElement(); + String externalForm = url.toExternalForm(); + + if (externalForm.charAt(externalForm.length()-1) != '/') { + if (url.getProtocol().equals("file")) { + File directory = new File(url.getFile()).getAbsoluteFile(); + findSubClasses(classLoader, packageName, annotation, directory, directory.getParent(), classesThatWereAnnotated); + } + } + } + } + + return classesThatWereAnnotated; + } catch (Exception e) { + System.err.println("Problem registering build classes. ABORTING!"); + System.exit(-1); + } + + return null; + } + + private static final void findSubClasses(ClassLoader classLoader, String packageName, + Class annotation, File directory, + String rootPath, + List> classesThatWereAnnotated) throws ClassNotFoundException { + + File[] files = directory.listFiles(); + + if (files != null) { + for (File file : files) { + String absolutePath = file.getAbsolutePath(); + String fileName = file.getName(); + + if (file.isDirectory()) { + findSubClasses(classLoader, packageName , annotation, file, rootPath, classesThatWereAnnotated); + } + else if (isValid(fileName)) { + String classPath = absolutePath.substring(rootPath.length() + 1, absolutePath.length() - 6); + + if (packageName != null) { + if (!classPath.startsWith(packageName)) { + return; + } + } + + String toDots = classPath.replaceAll(File.separator, "."); + + Class clazz = Class.forName(toDots, false, classLoader); + if (clazz.getAnnotation(annotation) != null) { + classesThatWereAnnotated.add(clazz); + } + } + } + } + } + + + /** + * remove directories from the search. make sure it's a class file shortcut so we don't load ALL .class files! + * + **/ + private static boolean isValid(String name) { + + if (name == null) { + return false; + } + + int length = name.length(); + boolean isValid = length > 6 && + name.charAt(length-1) != '/' && // remove directories from the search. + name.charAt(length-6) == '.' && + name.charAt(length-5) == 'c' && + name.charAt(length-4) == 'l' && + name.charAt(length-3) == 'a' && + name.charAt(length-2) == 's' && + name.charAt(length-1) == 's'; // make sure it's a class file + + + + return isValid; + } +} diff --git a/Dorkbox-Util/src/dorkbox/util/bytes/BigEndian.java b/Dorkbox-Util/src/dorkbox/util/bytes/BigEndian.java new file mode 100644 index 0000000..463c1bf --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/bytes/BigEndian.java @@ -0,0 +1,120 @@ +package dorkbox.util.bytes; + +import java.nio.ByteBuffer; + +public class BigEndian { + // the following are ALL in Big-Endian (big is to the left, first byte is most significant, unsigned bytes) + + /** SHORT to and from bytes */ + public static class Short_ { + public static final short fromBytes(byte[] bytes) { + return fromBytes(bytes[0], bytes[1]); + } + + public static final short fromBytes(byte b0, byte b1) { + return (short) ((b0 & 0xFF) << 8 | + (b1 & 0xFF) << 0); + } + + + public static final byte[] toBytes(short x) { + return new byte[] {(byte) (x >> 8), + (byte) (x >> 0) + }; + } + + public static final int fromBytes(ByteBuffer buff) { + return fromBytes(buff.get(), buff.get()); + } + } + + /** CHAR to and from bytes */ + public static class Char_ { + public static final char fromBytes(byte[] bytes) { + return fromBytes(bytes[0], bytes[1]); + } + + public static final char fromBytes(byte b0, byte b1) { + return (char) ((b0 & 0xFF) << 8 | + (b1 & 0xFF) << 0); + } + + + public static final byte[] toBytes(char x) { + return new byte[] {(byte) (x >> 8), + (byte) (x >> 0) + }; + } + + public static final int fromBytes(ByteBuffer buff) { + return fromBytes(buff.get(), buff.get()); + } + } + + + /** INT to and from bytes */ + public static class Int_ { + public static final int fromBytes(byte[] bytes) { + return fromBytes(bytes[0], bytes[1], bytes[2], bytes[3]); + } + + public static final int fromBytes(byte b0, byte b1, byte b2, byte b3) { + return (b0 & 0xFF) << 24 | + (b1 & 0xFF) << 16 | + (b2 & 0xFF) << 8 | + (b3 & 0xFF) << 0; + } + + public static int fromBytes(byte b0, byte b1) { + return (b0 & 0xFF) << 24 | + (b1 & 0xFF) << 16; + } + + public static final byte[] toBytes(int x) { + return new byte[] {(byte) (x >> 24), + (byte) (x >> 16), + (byte) (x >> 8), + (byte) (x >> 0) + } ; + } + + public static final int fromBytes(ByteBuffer buff) { + return fromBytes(buff.get(), buff.get(), buff.get(), buff.get()); + } + } + + /** LONG to and from bytes */ + public static class Long_ { + public static final long fromBytes(byte[] bytes) { + return fromBytes(bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7]); + } + + public static final long fromBytes(byte b0, byte b1, byte b2, byte b3, byte b4, byte b5, byte b6, byte b7) { + return (long) (b0 & 0xFF) << 56 | + (long) (b1 & 0xFF) << 48 | + (long) (b2 & 0xFF) << 40 | + (long) (b3 & 0xFF) << 32 | + (long) (b4 & 0xFF) << 24 | + (long) (b5 & 0xFF) << 16 | + (long) (b6 & 0xFF) << 8 | + (long) (b7 & 0xFF) << 0; + } + + public static final byte[] toBytes (long x) { + return new byte[] {(byte) (x >> 56), + (byte) (x >> 48), + (byte) (x >> 40), + (byte) (x >> 32), + (byte) (x >> 24), + (byte) (x >> 16), + (byte) (x >> 8), + (byte) (x >> 0), + }; + } + + public static final long fromBytes(ByteBuffer buff) { + return fromBytes(buff.get(), buff.get(), buff.get(), buff.get(), buff.get(), buff.get(), buff.get(), buff.get()); + } + } +} + diff --git a/Dorkbox-Util/src/dorkbox/util/bytes/ByteBuffer2.java b/Dorkbox-Util/src/dorkbox/util/bytes/ByteBuffer2.java new file mode 100644 index 0000000..5f31b8c --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/bytes/ByteBuffer2.java @@ -0,0 +1,318 @@ +package dorkbox.util.bytes; + +import java.nio.BufferUnderflowException; + +/** + * Cleanroom implementation of a self-growing bytebuffer + */ +public class ByteBuffer2 { + + private byte[] bytes; + + private int position = 0; + private int mark = -1; + private int limit = 0; + + public static ByteBuffer2 wrap(byte[] buffer) { + return new ByteBuffer2(buffer); + } + + public static ByteBuffer2 allocate(int capacity) { + ByteBuffer2 byteBuffer2 = new ByteBuffer2(new byte[capacity]); + byteBuffer2.clear(); + return byteBuffer2; + } + + public ByteBuffer2() { + this(0); + } + + public ByteBuffer2(int size) { + this(new byte[size]); + } + + public ByteBuffer2(byte[] bytes) { + this.bytes = bytes; + clear(); + position = bytes.length; + } + + public byte getByte() { + if (position > limit) { + throw new BufferUnderflowException(); + } + + return bytes[position++]; + } + + public byte getByte(int i) { + if (i > limit) { + throw new BufferUnderflowException(); + } + + return bytes[i]; + } + + public void getBytes(byte[] buffer) { + getBytes(buffer, 0, buffer.length); + } + + public void getBytes(byte[] buffer, int length) { + getBytes(buffer, 0, length); + } + + public void getBytes(byte[] buffer, int offset, int length) { + if (position + length - offset > limit) { + throw new BufferUnderflowException(); + } + + System.arraycopy(bytes, position, buffer, 0, length-offset); + position += length-offset; + } + + /** + * MUST call checkBuffer before calling this! + * + * NOT PROTECTED + */ + private final void _put(byte b) { + bytes[position++] = b; + } + + /** NOT PROTECTED! */ + private final void checkBuffer(int threshold) { + if (bytes.length < threshold) { + byte[] t = new byte[threshold]; + // grow at back of array + System.arraycopy(bytes, 0, t, 0, bytes.length); + limit = t.length; + + bytes = t; + } + } + + public final synchronized void put(ByteBuffer2 buffer) { + putBytes(buffer.array(), buffer.position, buffer.limit); + buffer.position = buffer.limit; + } + + public final synchronized ByteBuffer2 putBytes(byte[] src) { + return putBytes(src, 0, src.length); + } + + public final synchronized ByteBuffer2 putBytes(byte[] src, int offset, int length) { + checkBuffer(position + length - offset); + + System.arraycopy(src, offset, bytes, position, length); + position += length; + + return this; + } + + public final synchronized ByteBuffer2 putByte(byte b) { + checkBuffer(position + 1); + + _put(b); + return this; + } + + public final synchronized void putByte(int position, byte b) { + this.position = position; + putByte(b); + } + + public final synchronized void putChar(char c) { + checkBuffer(position + 2); + + putBytes(BigEndian.Char_.toBytes(c)); + } + + public final synchronized char getChar() { + return BigEndian.Char_.fromBytes(getByte(), getByte()); + } + + public final synchronized ByteBuffer2 putShort(short x) { + checkBuffer(position + 2); + + putBytes(BigEndian.Short_.toBytes(x)); + + return this; + } + + public final synchronized short getShort() { + return BigEndian.Short_.fromBytes(getByte(), getByte()); + } + + public final synchronized void putInt(int x) { + checkBuffer(position + 4); + + putBytes(BigEndian.Int_.toBytes(x)); + } + + public final synchronized int getInt() { + byte b3 = getByte(); + byte b2 = getByte(); + byte b1 = getByte(); + return BigEndian.Int_.fromBytes(getByte(), b1, b2, b3); + } + + public final synchronized void putLong(long x) { + checkBuffer(position + 8); + + putBytes(BigEndian.Long_.toBytes(x)); + } + + public final synchronized long getLong() { + return BigEndian.Long_.fromBytes(getByte(), getByte(), getByte(), getByte(), getByte(), getByte(), getByte(), getByte()); + } + + /** + * Returns the backing array of this buffer + */ + public byte[] array() { + return bytes; + } + + /** + * Returns a copy of the backing array of this buffer + */ + public final synchronized byte[] arrayCopy() { + int length = bytes.length - position; + + byte[] b = new byte[length]; + System.arraycopy(bytes, position, b, 0, length); + return b; + } + + /** + * Returns this buffer's position. + */ + public int position() { + return position; + } + + /** + * Sets this buffer's position. + */ + public final synchronized ByteBuffer2 position(int position) { + if (position > bytes.length || position < 0) { + throw new IllegalArgumentException(); + } + + this.position = position; + if (mark > position) { + mark = -1; + } + + return this; + } + + /** + * Returns the number of elements between the current position and the + * limit. + */ + public final synchronized int remaining() { + return limit - position; + } + + /** + * Tells whether there are any elements between the current position and + * the limit. + */ + public final synchronized boolean hasRemaining() { + return position < limit; + } + + /** + * Sets this buffer's limit. + */ + public final synchronized void limit(int limit) { + this.limit = limit; + if (position > limit) { + position = limit; + } + if (mark > limit) { + mark = -1; + } + } + + /** + * Returns this buffer's limit. + */ + public int limit() { + return limit; + } + + /** + * Returns this buffer's capacity. + */ + public int capacity() { + return bytes.length; + } + + /** + * The bytes between the buffer's current position and its limit, if any, are copied to the beginning of the buffer. + * That is, the byte at index p = position() is copied to index zero, the byte at index p + 1 is copied to index one, + * and so forth until the byte at index limit() - 1 is copied to index n = limit() - 1 - p. The buffer's position is + * then set to n+1 and its limit is set to its capacity. The mark, if defined, is discarded. + * + * The buffer's position is set to the number of bytes copied, rather than to zero, so that an invocation of this method + * can be followed immediately by an invocation of another relative put method. + */ + public final synchronized void compact() { + mark = -1; + System.arraycopy(bytes, position, bytes, 0, remaining()); + + position(remaining()); + limit(capacity()); + } + + /** + * Readies the buffer for reading. + * + * Flips this buffer. The limit is set to the current position and then + * the position is set to zero. If the mark is defined then it is + * discarded. + */ + public final synchronized void flip() { + limit = position; + position = 0; + mark = -1; + } + + /** + * Clears this buffer. The position is set to zero, the limit is set to + * the capacity, and the mark is discarded. + */ + public final synchronized void clear() { + position = 0; + limit = capacity(); + mark = -1; + } + + /** + * Rewinds this buffer. The position is set to zero and the mark is + * discarded. + */ + public final synchronized void rewind() { + position = 0; + mark = -1; + } + + /** + * Sets this buffer's mark at its position. + */ + public final synchronized void mark() { + mark = position; + } + + /** + * Resets this buffer's position to the previously-marked position. + * + *

Invoking this method neither changes nor discards the mark's + * value.

+ */ + public void reset() { + position = mark; + } +} \ No newline at end of file diff --git a/Dorkbox-Util/src/dorkbox/util/bytes/LittleEndian.java b/Dorkbox-Util/src/dorkbox/util/bytes/LittleEndian.java new file mode 100644 index 0000000..8666462 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/bytes/LittleEndian.java @@ -0,0 +1,345 @@ +package dorkbox.util.bytes; + +import java.nio.ByteBuffer; + +public class LittleEndian { + // the following are ALL in Little-Endian (big is to the right, first byte is least significant, unsigned bytes) + + /** CHAR to and from bytes */ + public static class Char_ { + @SuppressWarnings("fallthrough") + public static final char fromBytes(byte[] bytes) { + char number = 0; + + switch (bytes.length) { + case 2: number += (bytes[1] & 0xFF) << 8; + case 1: number += (bytes[0] & 0xFF) << 0; + } + + return number; + } + + public static final char fromBytes(byte b0, byte b1) { + return (char) ((b1 & 0xFF) << 8 | + (b0 & 0xFF) << 0); + } + + + public static final byte[] toBytes(char x) { + return new byte[] {(byte) (x >> 8), + (byte) (x >> 0) + }; + } + + public static final int fromBytes(ByteBuffer buff) { + return fromBytes(buff.get(), buff.get()); + } + } + + /** CHAR to and from bytes */ + public static class UChar_ { + @SuppressWarnings("fallthrough") + public static final char fromBytes(byte[] bytes) { + char number = 0; + + switch (bytes.length) { + case 2: number += (bytes[1] & 0xFF) << 8; + case 1: number += (bytes[0] & 0xFF) << 0; + } + + return number; + } + + public static final char fromBytes(byte b0, byte b1) { + return (char) ((b1 & 0xFF) << 8 | + (b0 & 0xFF) << 0); + } + + + public static final byte[] toBytes(char x) { + return new byte[] {(byte) (x >> 8), + (byte) (x >> 0) + }; + } + + public static final int fromBytes(ByteBuffer buff) { + return fromBytes(buff.get(), buff.get()); + } + } + + /** SHORT to and from bytes */ + public static class Short_ { + @SuppressWarnings("fallthrough") + public static final short fromBytes(byte[] bytes) { + short number = 0; + + switch (bytes.length) { + case 2: number += (bytes[1] & 0xFF) << 8; + case 1: number += (bytes[0] & 0xFF) << 0; + } + + return number; + } + + public static final short fromBytes(byte b0, byte b1) { + return (short) ((b1 & 0xFF) << 8 | + (b0 & 0xFF) << 0); + } + + + public static final byte[] toBytes(short x) { + return new byte[] {(byte) (x >> 0), + (byte) (x >> 8) + }; + } + + public static final int fromBytes(ByteBuffer buff) { + return fromBytes(buff.get(), buff.get()); + } + } + + /** SHORT to and from bytes */ + public static class UShort_ { + @SuppressWarnings("fallthrough") + public static final short fromBytes(byte[] bytes) { + short number = 0; + + switch (bytes.length) { + case 2: number += (bytes[1] & 0xFF) << 8; + case 1: number += (bytes[0] & 0xFF) << 0; + } + + return number; + } + + @SuppressWarnings("fallthrough") + public static short fromBytes(byte[] bytes, int offset, int bytenum) { + short number = 0; + + switch (bytenum) { + case 2: number += (bytes[offset+1] & 0xFF) << 8; + case 1: number += (bytes[offset+0] & 0xFF) << 0; + } + + return number; + } + + public static final short fromBytes(byte b0, byte b1) { + return (short) ((b1 & 0xFF) << 8 | + (b0 & 0xFF) << 0); + } + + + public static final byte[] toBytes(short x) { + return new byte[] {(byte) (x >> 0), + (byte) (x >> 8) + }; + } + + public static final int fromBytes(ByteBuffer buff) { + return fromBytes(buff.get(), buff.get()); + } + } + + /** INT to and from bytes */ + public static class Int_ { + @SuppressWarnings("fallthrough") + public static final int fromBytes(byte[] bytes) { + int number = 0; + + switch (bytes.length) { + case 4: number += (bytes[3] & 0xFF) << 24; + case 3: number += (bytes[2] & 0xFF) << 16; + case 2: number += (bytes[1] & 0xFF) << 8; + case 1: number += (bytes[0] & 0xFF) << 0; + } + + return number; + } + + public static final int fromBytes(byte b0, byte b1, byte b2, byte b3) { + return (b3 & 0xFF) << 24 | + (b2 & 0xFF) << 16 | + (b1 & 0xFF) << 8 | + (b0 & 0xFF) << 0; + } + + public static final byte[] toBytes(int x) { + return new byte[] {(byte) (x >> 0), + (byte) (x >> 8), + (byte) (x >> 16), + (byte) (x >> 24) + } ; + } + + public static final int fromBytes(ByteBuffer buff) { + return fromBytes(buff.get(), buff.get(), buff.get(), buff.get()); + } + } + + /** INT to and from bytes */ + public static class UInt_ { + @SuppressWarnings("fallthrough") + public static final int fromBytes(byte[] bytes) { + int number = 0; + + switch (bytes.length) { + case 4: number += (bytes[3] & 0xFF) << 24; + case 3: number += (bytes[2] & 0xFF) << 16; + case 2: number += (bytes[1] & 0xFF) << 8; + case 1: number += (bytes[0] & 0xFF) << 0; + } + + return number; + } + + @SuppressWarnings("fallthrough") + public static int fromBytes(byte[] bytes, int offset, int bytenum) { + int number = 0; + + switch (bytenum) { + case 4: number += (bytes[offset+3] & 0xFF) << 24; + case 3: number += (bytes[offset+2] & 0xFF) << 16; + case 2: number += (bytes[offset+1] & 0xFF) << 8; + case 1: number += (bytes[offset+0] & 0xFF) << 0; + } + + return number; + } + + public static final int fromBytes(byte b0, byte b1, byte b2, byte b3) { + return (b3 & 0xFF) << 24 | + (b2 & 0xFF) << 16 | + (b1 & 0xFF) << 8 | + (b0 & 0xFF) << 0; + } + + public static final byte[] toBytes(int x) { + return new byte[] {(byte) (x >> 0), + (byte) (x >> 8), + (byte) (x >> 16), + (byte) (x >> 24) + } ; + } + + public static final int fromBytes(ByteBuffer buff) { + return fromBytes(buff.get(), buff.get(), buff.get(), buff.get()); + } + } + + /** LONG to and from bytes */ + public static class Long_ { + + @SuppressWarnings("fallthrough") + public static final long fromBytes(byte[] bytes) { + long number = 0L; + + switch (bytes.length) { + case 8: number += (long) (bytes[7] & 0xFF) << 56; + case 7: number += (long) (bytes[6] & 0xFF) << 48; + case 6: number += (long) (bytes[5] & 0xFF) << 40; + case 5: number += (long) (bytes[4] & 0xFF) << 32; + case 4: number += (long) (bytes[3] & 0xFF) << 24; + case 3: number += (long) (bytes[2] & 0xFF) << 16; + case 2: number += (long) (bytes[1] & 0xFF) << 8; + case 1: number += (long) (bytes[0] & 0xFF) << 0; + } + + return number; + } + + public static final long fromBytes(byte b0, byte b1, byte b2, byte b3, byte b4, byte b5, byte b6, byte b7) { + return (long) (b7 & 0xFF) << 56 | + (long) (b6 & 0xFF) << 48 | + (long) (b5 & 0xFF) << 40 | + (long) (b4 & 0xFF) << 32 | + (long) (b3 & 0xFF) << 24 | + (long) (b2 & 0xFF) << 16 | + (long) (b1 & 0xFF) << 8 | + (long) (b0 & 0xFF) << 0; + } + + public static final byte[] toBytes (long x) { + return new byte[] {(byte) (x >> 0), + (byte) (x >> 8), + (byte) (x >> 16), + (byte) (x >> 24), + (byte) (x >> 32), + (byte) (x >> 40), + (byte) (x >> 48), + (byte) (x >> 56), + }; + } + + public static final long fromBytes(ByteBuffer buff) { + return fromBytes(buff.get(), buff.get(), buff.get(), buff.get(), buff.get(), buff.get(), buff.get(), buff.get()); + } + } + + /** LONG to and from bytes */ + public static class ULong_ { + @SuppressWarnings("fallthrough") + public static int fromBytes(byte[] bytes, int offset, int bytenum) { + int number = 0; + + switch (bytenum) { + case 8: number += (long) (bytes[offset+7] & 0xFF) << 56; + case 7: number += (long) (bytes[offset+6] & 0xFF) << 48; + case 6: number += (long) (bytes[offset+5] & 0xFF) << 40; + case 5: number += (long) (bytes[offset+4] & 0xFF) << 32; + case 4: number += (long) (bytes[offset+3] & 0xFF) << 24; + case 3: number += (long) (bytes[offset+2] & 0xFF) << 16; + case 2: number += (long) (bytes[offset+1] & 0xFF) << 8; + case 1: number += (long) (bytes[offset+0] & 0xFF) << 0; + } + + return number; + } + + @SuppressWarnings("fallthrough") + public static final long fromBytes(byte[] bytes) { + long number = 0L; + + switch (bytes.length) { + case 8: number += (long) (bytes[7] & 0xFF) << 56; + case 7: number += (long) (bytes[6] & 0xFF) << 48; + case 6: number += (long) (bytes[5] & 0xFF) << 40; + case 5: number += (long) (bytes[4] & 0xFF) << 32; + case 4: number += (long) (bytes[3] & 0xFF) << 24; + case 3: number += (long) (bytes[2] & 0xFF) << 16; + case 2: number += (long) (bytes[1] & 0xFF) << 8; + case 1: number += (long) (bytes[0] & 0xFF) << 0; + } + + return number; + } + + public static final long fromBytes(byte b0, byte b1, byte b2, byte b3, byte b4, byte b5, byte b6, byte b7) { + return (long) (b7 & 0xFF) << 56 | + (long) (b6 & 0xFF) << 48 | + (long) (b5 & 0xFF) << 40 | + (long) (b4 & 0xFF) << 32 | + (long) (b3 & 0xFF) << 24 | + (long) (b2 & 0xFF) << 16 | + (long) (b1 & 0xFF) << 8 | + (long) (b0 & 0xFF) << 0; + } + + public static final byte[] toBytes (long x) { + return new byte[] {(byte) (x >> 0), + (byte) (x >> 8), + (byte) (x >> 16), + (byte) (x >> 24), + (byte) (x >> 32), + (byte) (x >> 40), + (byte) (x >> 48), + (byte) (x >> 56), + }; + } + + public static final long fromBytes(ByteBuffer buff) { + return fromBytes(buff.get(), buff.get(), buff.get(), buff.get(), buff.get(), buff.get(), buff.get(), buff.get()); + } + } +} + diff --git a/Dorkbox-Util/src/dorkbox/util/bytes/OptimizeUtils.java b/Dorkbox-Util/src/dorkbox/util/bytes/OptimizeUtils.java new file mode 100644 index 0000000..3b6a9ff --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/bytes/OptimizeUtils.java @@ -0,0 +1,541 @@ +package dorkbox.util.bytes; + +public class OptimizeUtils { + + private static final OptimizeUtils instance = new OptimizeUtils(); + + public static OptimizeUtils get() { + return instance; + } + + // int + + /** + * FROM KRYO + * + * Returns the number of bytes that would be written with {@link #writeInt(int, boolean)}. + * + * @param optimizePositive true if you want to optimize the number of bytes needed to write the length value + */ + public final int intLength (int value, boolean optimizePositive) { + if (!optimizePositive) { + value = value << 1 ^ value >> 31; + } + if (value >>> 7 == 0) { + return 1; + } + if (value >>> 14 == 0) { + return 2; + } + if (value >>> 21 == 0) { + return 3; + } + if (value >>> 28 == 0) { + return 4; + } + return 5; + } + + /** + * FROM KRYO + * + * look at buffer, and see if we can read the length of the int off of it. (from the reader index) + */ + public final boolean canReadInt (ByteBuffer2 buffer) { + int position = buffer.position(); + try { + int remaining = buffer.remaining(); + for (int offset = 0; offset < 32 && remaining > 0; offset += 7, remaining--) { + int b = buffer.getByte(); + if ((b & 0x80) == 0) { + return true; + } + } + return false; + } finally { + buffer.position(position); + } + } + + /** + * FROM KRYO + * + * look at buffer, and see if we can read the length of the int off of it. (from the reader index) + * + * @return 0 if we could not read anything, >0 for the number of bytes for the int on the buffer + */ + public boolean canReadInt (byte[] buffer) { + int length = buffer.length; + + if (length >= 5) { + return true; + } + int p = 0; + if ((buffer[p++] & 0x80) == 0) { + return true; + } + if (p == length) { + return false; + } + if ((buffer[p++] & 0x80) == 0) { + return true; + } + if (p == length) { + return false; + } + if ((buffer[p++] & 0x80) == 0) { + return true; + } + if (p == length) { + return false; + } + if ((buffer[p++] & 0x80) == 0) { + return true; + } + if (p == length) { + return false; + } + return true; + } + + /** + * FROM KRYO + * + * Reads an int from the buffer that was optimized. + */ + public final int readInt (ByteBuffer2 buffer, boolean optimizePositive) { + int b = buffer.getByte(); + int result = b & 0x7F; + if ((b & 0x80) != 0) { + b = buffer.getByte(); + result |= (b & 0x7F) << 7; + if ((b & 0x80) != 0) { + b = buffer.getByte(); + result |= (b & 0x7F) << 14; + if ((b & 0x80) != 0) { + b = buffer.getByte(); + result |= (b & 0x7F) << 21; + if ((b & 0x80) != 0) { + b = buffer.getByte(); + result |= (b & 0x7F) << 28; + } + } + } + } + return optimizePositive ? result : result >>> 1 ^ -(result & 1); + } + + /** + * FROM KRYO + * + * Reads an int from the buffer that was optimized. + */ + public int readInt (byte[] buffer, boolean optimizePositive) { + int position = 0; + int b = buffer[position++]; + int result = b & 0x7F; + if ((b & 0x80) != 0) { + b = buffer[position++]; + result |= (b & 0x7F) << 7; + if ((b & 0x80) != 0) { + b = buffer[position++]; + result |= (b & 0x7F) << 14; + if ((b & 0x80) != 0) { + b = buffer[position++]; + result |= (b & 0x7F) << 21; + if ((b & 0x80) != 0) { + b = buffer[position++]; + result |= (b & 0x7F) << 28; + } + } + } + } + return optimizePositive ? result : result >>> 1 ^ -(result & 1); + } + + + /** + * FROM KRYO + * + * Writes the specified int to the buffer using 1 to 5 bytes, depending on the size of the number. + * + * @param optimizePositive true if you want to optimize the number of bytes needed to write the length value + * @return the number of bytes written. + */ + public final int writeInt (ByteBuffer2 buffer, int value, boolean optimizePositive) { + if (!optimizePositive) { + value = value << 1 ^ value >> 31; + } + if (value >>> 7 == 0) { + buffer.putByte((byte)value); + return 1; + } + if (value >>> 14 == 0) { + buffer.putByte((byte)(value & 0x7F | 0x80)); + buffer.putByte((byte)(value >>> 7)); + return 2; + } + if (value >>> 21 == 0) { + buffer.putByte((byte)(value & 0x7F | 0x80)); + buffer.putByte((byte)(value >>> 7 | 0x80)); + buffer.putByte((byte)(value >>> 14)); + return 3; + } + if (value >>> 28 == 0) { + buffer.putByte((byte)(value & 0x7F | 0x80)); + buffer.putByte((byte)(value >>> 7 | 0x80)); + buffer.putByte((byte)(value >>> 14 | 0x80)); + buffer.putByte((byte)(value >>> 21)); + return 4; + } + buffer.putByte((byte)(value & 0x7F | 0x80)); + buffer.putByte((byte)(value >>> 7 | 0x80)); + buffer.putByte((byte)(value >>> 14 | 0x80)); + buffer.putByte((byte)(value >>> 21 | 0x80)); + buffer.putByte((byte)(value >>> 28)); + return 5; + } + + /** + * FROM KRYO + * + * Writes the specified int to the buffer using 1 to 5 bytes, depending on the size of the number. + * + * @param optimizePositive true if you want to optimize the number of bytes needed to write the length value + * @return the number of bytes written. + */ + public int writeInt (byte[] buffer, int value, boolean optimizePositive) { + int position = 0; + if (!optimizePositive) { + value = value << 1 ^ value >> 31; + } + if (value >>> 7 == 0) { + buffer[position++] = (byte)value; + return 1; + } + if (value >>> 14 == 0) { + buffer[position++] = (byte)(value & 0x7F | 0x80); + buffer[position++] = (byte)(value >>> 7); + return 2; + } + if (value >>> 21 == 0) { + buffer[position++] = (byte)(value & 0x7F | 0x80); + buffer[position++] = (byte)(value >>> 7 | 0x80); + buffer[position++] = (byte)(value >>> 14); + return 3; + } + if (value >>> 28 == 0) { + buffer[position++] = (byte)(value & 0x7F | 0x80); + buffer[position++] = (byte)(value >>> 7 | 0x80); + buffer[position++] = (byte)(value >>> 14 | 0x80); + buffer[position++] = (byte)(value >>> 21); + return 4; + } + buffer[position++] = (byte)(value & 0x7F | 0x80); + buffer[position++] = (byte)(value >>> 7 | 0x80); + buffer[position++] = (byte)(value >>> 14 | 0x80); + buffer[position++] = (byte)(value >>> 21 | 0x80); + buffer[position++] = (byte)(value >>> 28); + return 5; + } + + + // long + + /** + * Returns the number of bytes that would be written with {@link #writeLong(long, boolean)}. + * + * @param optimizePositive true if you want to optimize the number of bytes needed to write the length value + */ + public final int longLength (long value, boolean optimizePositive) { + if (!optimizePositive) { + value = value << 1 ^ value >> 63; + } + if (value >>> 7 == 0) { + return 1; + } + if (value >>> 14 == 0) { + return 2; + } + if (value >>> 21 == 0) { + return 3; + } + if (value >>> 28 == 0) { + return 4; + } + if (value >>> 35 == 0) { + return 5; + } + if (value >>> 42 == 0) { + return 6; + } + if (value >>> 49 == 0) { + return 7; + } + if (value >>> 56 == 0) { + return 8; + } + return 9; + } + + /** + * FROM KRYO + * + * Reads a 1-9 byte long. + * + * @param optimizePositive true if you want to optimize the number of bytes needed to write the length value + */ + public final long readLong (ByteBuffer2 buffer, boolean optimizePositive) { + int b = buffer.getByte(); + long result = b & 0x7F; + if ((b & 0x80) != 0) { + b = buffer.getByte(); + result |= (b & 0x7F) << 7; + if ((b & 0x80) != 0) { + b = buffer.getByte(); + result |= (b & 0x7F) << 14; + if ((b & 0x80) != 0) { + b = buffer.getByte(); + result |= (b & 0x7F) << 21; + if ((b & 0x80) != 0) { + b = buffer.getByte(); + result |= (long)(b & 0x7F) << 28; + if ((b & 0x80) != 0) { + b = buffer.getByte(); + result |= (long)(b & 0x7F) << 35; + if ((b & 0x80) != 0) { + b = buffer.getByte(); + result |= (long)(b & 0x7F) << 42; + if ((b & 0x80) != 0) { + b = buffer.getByte(); + result |= (long)(b & 0x7F) << 49; + if ((b & 0x80) != 0) { + b = buffer.getByte(); + result |= (long)b << 56; + } + } + } + } + } + } + } + } + if (!optimizePositive) { + result = result >>> 1 ^ -(result & 1); + } + return result; + } + + /** + * FROM KRYO + * + * Reads a 1-9 byte long. + * + * @param optimizePositive true if you want to optimize the number of bytes needed to write the length value + */ + public long readLong (byte[] buffer, boolean optimizePositive) { + int position = 0; + int b = buffer[position++]; + long result = b & 0x7F; + if ((b & 0x80) != 0) { + b = buffer[position++]; + result |= (b & 0x7F) << 7; + if ((b & 0x80) != 0) { + b = buffer[position++]; + result |= (b & 0x7F) << 14; + if ((b & 0x80) != 0) { + b = buffer[position++]; + result |= (b & 0x7F) << 21; + if ((b & 0x80) != 0) { + b = buffer[position++]; + result |= (long)(b & 0x7F) << 28; + if ((b & 0x80) != 0) { + b = buffer[position++]; + result |= (long)(b & 0x7F) << 35; + if ((b & 0x80) != 0) { + b = buffer[position++]; + result |= (long)(b & 0x7F) << 42; + if ((b & 0x80) != 0) { + b = buffer[position++]; + result |= (long)(b & 0x7F) << 49; + if ((b & 0x80) != 0) { + b = buffer[position++]; + result |= (long)b << 56; + } + } + } + } + } + } + } + } + if (!optimizePositive) { + result = result >>> 1 ^ -(result & 1); + } + return result; + } + + /** + * FROM KRYO + * + * Writes a 1-9 byte long. + * + * @param optimizePositive If true, small positive numbers will be more efficient (1 byte) and small negative numbers will be + * inefficient (9 bytes). + * @return the number of bytes written. + */ + public final int writeLong (ByteBuffer2 buffer, long value, boolean optimizePositive) { + if (!optimizePositive) { + value = value << 1 ^ value >> 63; + } + if (value >>> 7 == 0) { + buffer.putByte((byte)value); + return 1; + } + if (value >>> 14 == 0) { + buffer.putByte((byte)(value & 0x7F | 0x80)); + buffer.putByte((byte)(value >>> 7)); + return 2; + } + if (value >>> 21 == 0) { + buffer.putByte((byte)(value & 0x7F | 0x80)); + buffer.putByte((byte)(value >>> 7 | 0x80)); + buffer.putByte((byte)(value >>> 14)); + return 3; + } + if (value >>> 28 == 0) { + buffer.putByte((byte)(value & 0x7F | 0x80)); + buffer.putByte((byte)(value >>> 7 | 0x80)); + buffer.putByte((byte)(value >>> 14 | 0x80)); + buffer.putByte((byte)(value >>> 21)); + return 4; + } + if (value >>> 35 == 0) { + buffer.putByte((byte)(value & 0x7F | 0x80)); + buffer.putByte((byte)(value >>> 7 | 0x80)); + buffer.putByte((byte)(value >>> 14 | 0x80)); + buffer.putByte((byte)(value >>> 21 | 0x80)); + buffer.putByte((byte)(value >>> 28)); + return 5; + } + if (value >>> 42 == 0) { + buffer.putByte((byte)(value & 0x7F | 0x80)); + buffer.putByte((byte)(value >>> 7 | 0x80)); + buffer.putByte((byte)(value >>> 14 | 0x80)); + buffer.putByte((byte)(value >>> 21 | 0x80)); + buffer.putByte((byte)(value >>> 28 | 0x80)); + buffer.putByte((byte)(value >>> 35)); + return 6; + } + if (value >>> 49 == 0) { + buffer.putByte((byte)(value & 0x7F | 0x80)); + buffer.putByte((byte)(value >>> 7 | 0x80)); + buffer.putByte((byte)(value >>> 14 | 0x80)); + buffer.putByte((byte)(value >>> 21 | 0x80)); + buffer.putByte((byte)(value >>> 28 | 0x80)); + buffer.putByte((byte)(value >>> 35 | 0x80)); + buffer.putByte((byte)(value >>> 42)); + return 7; + } + if (value >>> 56 == 0) { + buffer.putByte((byte)(value & 0x7F | 0x80)); + buffer.putByte((byte)(value >>> 7 | 0x80)); + buffer.putByte((byte)(value >>> 14 | 0x80)); + buffer.putByte((byte)(value >>> 21 | 0x80)); + buffer.putByte((byte)(value >>> 28 | 0x80)); + buffer.putByte((byte)(value >>> 35 | 0x80)); + buffer.putByte((byte)(value >>> 42 | 0x80)); + buffer.putByte((byte)(value >>> 49)); + return 8; + } + buffer.putByte((byte)(value & 0x7F | 0x80)); + buffer.putByte((byte)(value >>> 7 | 0x80)); + buffer.putByte((byte)(value >>> 14 | 0x80)); + buffer.putByte((byte)(value >>> 21 | 0x80)); + buffer.putByte((byte)(value >>> 28 | 0x80)); + buffer.putByte((byte)(value >>> 35 | 0x80)); + buffer.putByte((byte)(value >>> 42 | 0x80)); + buffer.putByte((byte)(value >>> 49 | 0x80)); + buffer.putByte((byte)(value >>> 56)); + return 9; + } + + /** + * FROM KRYO + * + * look at buffer, and see if we can read the length of the long off of it (from the reader index). + */ + public final boolean canReadLong (ByteBuffer2 buffer) { + int position = buffer.position(); + try { + int remaining = buffer.remaining(); + for (int offset = 0; offset < 64 && remaining > 0; offset += 7, remaining--) { + int b = buffer.getByte(); + if ((b & 0x80) == 0) { + return true; + } + } + return false; + } finally { + buffer.position(position); + } + } + + public boolean canReadLong (byte[] buffer) { + int limit = buffer.length; + + if (limit >= 9) { + return true; + } + int p = 0; + if ((buffer[p++] & 0x80) == 0) { + return true; + } + if (p == limit) { + return false; + } + if ((buffer[p++] & 0x80) == 0) { + return true; + } + if (p == limit) { + return false; + } + if ((buffer[p++] & 0x80) == 0) { + return true; + } + if (p == limit) { + return false; + } + if ((buffer[p++] & 0x80) == 0) { + return true; + } + if (p == limit) { + return false; + } + if ((buffer[p++] & 0x80) == 0) { + return true; + } + if (p == limit) { + return false; + } + if ((buffer[p++] & 0x80) == 0) { + return true; + } + if (p == limit) { + return false; + } + if ((buffer[p++] & 0x80) == 0) { + return true; + } + if (p == limit) { + return false; + } + if ((buffer[p++] & 0x80) == 0) { + return true; + } + if (p == limit) { + return false; + } + return true; + } +} diff --git a/Dorkbox-Util/src/dorkbox/util/bytes/OptimizeUtilsByteBuf.java b/Dorkbox-Util/src/dorkbox/util/bytes/OptimizeUtilsByteBuf.java new file mode 100644 index 0000000..6fc6a56 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/bytes/OptimizeUtilsByteBuf.java @@ -0,0 +1,297 @@ +package dorkbox.util.bytes; + +import io.netty.buffer.ByteBuf; + +public class OptimizeUtilsByteBuf { + + private static final OptimizeUtilsByteBuf instance = new OptimizeUtilsByteBuf(); + + public static OptimizeUtilsByteBuf get() { + return instance; + } + + // int + + /** + * FROM KRYO + * + * Returns the number of bytes that would be written with {@link #writeInt(int, boolean)}. + * + * @param optimizePositive true if you want to optimize the number of bytes needed to write the length value + */ + public final int intLength (int value, boolean optimizePositive) { + if (!optimizePositive) value = (value << 1) ^ (value >> 31); + if (value >>> 7 == 0) return 1; + if (value >>> 14 == 0) return 2; + if (value >>> 21 == 0) return 3; + if (value >>> 28 == 0) return 4; + return 5; + } + + /** + * FROM KRYO + * + * look at buffer, and see if we can read the length of the int off of it. (from the reader index) + * + * @return 0 if we could not read anything, >0 for the number of bytes for the int on the buffer + */ + public final int canReadInt (ByteBuf buffer) { + int startIndex = buffer.readerIndex(); + try { + int remaining = buffer.readableBytes(); + for (int offset = 0, count = 1; offset < 32 && remaining > 0; offset += 7, remaining--, count++) { + int b = buffer.readByte(); + if ((b & 0x80) == 0) { + return count; + } + } + return 0; + } finally { + buffer.readerIndex(startIndex); + } + } + + /** + * FROM KRYO + * + * Reads an int from the buffer that was optimized. + */ + public final int readInt (ByteBuf buffer, boolean optimizePositive) { + int b = buffer.readByte(); + int result = b & 0x7F; + if ((b & 0x80) != 0) { + b = buffer.readByte(); + result |= (b & 0x7F) << 7; + if ((b & 0x80) != 0) { + b = buffer.readByte(); + result |= (b & 0x7F) << 14; + if ((b & 0x80) != 0) { + b = buffer.readByte(); + result |= (b & 0x7F) << 21; + if ((b & 0x80) != 0) { + b = buffer.readByte(); + result |= (b & 0x7F) << 28; + } + } + } + } + return optimizePositive ? result : ((result >>> 1) ^ -(result & 1)); + } + + + /** + * FROM KRYO + * + * Writes the specified int to the buffer using 1 to 5 bytes, depending on the size of the number. + * + * @param optimizePositive true if you want to optimize the number of bytes needed to write the length value + * @return the number of bytes written. + */ + public final int writeInt (ByteBuf buffer, int value, boolean optimizePositive) { + if (!optimizePositive) value = (value << 1) ^ (value >> 31); + if (value >>> 7 == 0) { + buffer.writeByte((byte)value); + return 1; + } + if (value >>> 14 == 0) { + buffer.writeByte((byte)((value & 0x7F) | 0x80)); + buffer.writeByte((byte)(value >>> 7)); + return 2; + } + if (value >>> 21 == 0) { + buffer.writeByte((byte)((value & 0x7F) | 0x80)); + buffer.writeByte((byte)(value >>> 7 | 0x80)); + buffer.writeByte((byte)(value >>> 14)); + return 3; + } + if (value >>> 28 == 0) { + buffer.writeByte((byte)((value & 0x7F) | 0x80)); + buffer.writeByte((byte)(value >>> 7 | 0x80)); + buffer.writeByte((byte)(value >>> 14 | 0x80)); + buffer.writeByte((byte)(value >>> 21)); + return 4; + } + buffer.writeByte((byte)((value & 0x7F) | 0x80)); + buffer.writeByte((byte)(value >>> 7 | 0x80)); + buffer.writeByte((byte)(value >>> 14 | 0x80)); + buffer.writeByte((byte)(value >>> 21 | 0x80)); + buffer.writeByte((byte)(value >>> 28)); + return 5; + } + + + + + // long + + /** + * Returns the number of bytes that would be written with {@link #writeLong(long, boolean)}. + * + * @param optimizePositive true if you want to optimize the number of bytes needed to write the length value + */ + public final int longLength (long value, boolean optimizePositive) { + if (!optimizePositive) value = (value << 1) ^ (value >> 63); + if (value >>> 7 == 0) return 1; + if (value >>> 14 == 0) return 2; + if (value >>> 21 == 0) return 3; + if (value >>> 28 == 0) return 4; + if (value >>> 35 == 0) return 5; + if (value >>> 42 == 0) return 6; + if (value >>> 49 == 0) return 7; + if (value >>> 56 == 0) return 8; + return 9; + } + + /** + * FROM KRYO + * + * Reads a 1-9 byte long. + * + * @param optimizePositive true if you want to optimize the number of bytes needed to write the length value + */ + public final long readLong (ByteBuf buffer, boolean optimizePositive) { + int b = buffer.readByte(); + long result = b & 0x7F; + if ((b & 0x80) != 0) { + b = buffer.readByte(); + result |= (b & 0x7F) << 7; + if ((b & 0x80) != 0) { + b = buffer.readByte(); + result |= (b & 0x7F) << 14; + if ((b & 0x80) != 0) { + b = buffer.readByte(); + result |= (b & 0x7F) << 21; + if ((b & 0x80) != 0) { + b = buffer.readByte(); + result |= (long)(b & 0x7F) << 28; + if ((b & 0x80) != 0) { + b = buffer.readByte(); + result |= (long)(b & 0x7F) << 35; + if ((b & 0x80) != 0) { + b = buffer.readByte(); + result |= (long)(b & 0x7F) << 42; + if ((b & 0x80) != 0) { + b = buffer.readByte(); + result |= (long)(b & 0x7F) << 49; + if ((b & 0x80) != 0) { + b = buffer.readByte(); + result |= (long)b << 56; + } + } + } + } + } + } + } + } + if (!optimizePositive) result = (result >>> 1) ^ -(result & 1); + return result; + } + + + /** + * FROM KRYO + * + * Writes a 1-9 byte long. + * + * @param optimizePositive If true, small positive numbers will be more efficient (1 byte) and small negative numbers will be + * inefficient (9 bytes). + * @return the number of bytes written. + */ + public final int writeLong (ByteBuf buffer, long value, boolean optimizePositive) { + if (!optimizePositive) value = (value << 1) ^ (value >> 63); + if (value >>> 7 == 0) { + buffer.writeByte((byte)value); + return 1; + } + if (value >>> 14 == 0) { + buffer.writeByte((byte)((value & 0x7F) | 0x80)); + buffer.writeByte((byte)(value >>> 7)); + return 2; + } + if (value >>> 21 == 0) { + buffer.writeByte((byte)((value & 0x7F) | 0x80)); + buffer.writeByte((byte)(value >>> 7 | 0x80)); + buffer.writeByte((byte)(value >>> 14)); + return 3; + } + if (value >>> 28 == 0) { + buffer.writeByte((byte)((value & 0x7F) | 0x80)); + buffer.writeByte((byte)(value >>> 7 | 0x80)); + buffer.writeByte((byte)(value >>> 14 | 0x80)); + buffer.writeByte((byte)(value >>> 21)); + return 4; + } + if (value >>> 35 == 0) { + buffer.writeByte((byte)((value & 0x7F) | 0x80)); + buffer.writeByte((byte)(value >>> 7 | 0x80)); + buffer.writeByte((byte)(value >>> 14 | 0x80)); + buffer.writeByte((byte)(value >>> 21 | 0x80)); + buffer.writeByte((byte)(value >>> 28)); + return 5; + } + if (value >>> 42 == 0) { + buffer.writeByte((byte)((value & 0x7F) | 0x80)); + buffer.writeByte((byte)(value >>> 7 | 0x80)); + buffer.writeByte((byte)(value >>> 14 | 0x80)); + buffer.writeByte((byte)(value >>> 21 | 0x80)); + buffer.writeByte((byte)(value >>> 28 | 0x80)); + buffer.writeByte((byte)(value >>> 35)); + return 6; + } + if (value >>> 49 == 0) { + buffer.writeByte((byte)((value & 0x7F) | 0x80)); + buffer.writeByte((byte)(value >>> 7 | 0x80)); + buffer.writeByte((byte)(value >>> 14 | 0x80)); + buffer.writeByte((byte)(value >>> 21 | 0x80)); + buffer.writeByte((byte)(value >>> 28 | 0x80)); + buffer.writeByte((byte)(value >>> 35 | 0x80)); + buffer.writeByte((byte)(value >>> 42)); + return 7; + } + if (value >>> 56 == 0) { + buffer.writeByte((byte)((value & 0x7F) | 0x80)); + buffer.writeByte((byte)(value >>> 7 | 0x80)); + buffer.writeByte((byte)(value >>> 14 | 0x80)); + buffer.writeByte((byte)(value >>> 21 | 0x80)); + buffer.writeByte((byte)(value >>> 28 | 0x80)); + buffer.writeByte((byte)(value >>> 35 | 0x80)); + buffer.writeByte((byte)(value >>> 42 | 0x80)); + buffer.writeByte((byte)(value >>> 49)); + return 8; + } + buffer.writeByte((byte)((value & 0x7F) | 0x80)); + buffer.writeByte((byte)(value >>> 7 | 0x80)); + buffer.writeByte((byte)(value >>> 14 | 0x80)); + buffer.writeByte((byte)(value >>> 21 | 0x80)); + buffer.writeByte((byte)(value >>> 28 | 0x80)); + buffer.writeByte((byte)(value >>> 35 | 0x80)); + buffer.writeByte((byte)(value >>> 42 | 0x80)); + buffer.writeByte((byte)(value >>> 49 | 0x80)); + buffer.writeByte((byte)(value >>> 56)); + return 9; + } + + /** + * FROM KRYO + * + * look at buffer, and see if we can read the length of the long off of it (from the reader index). + * + * @return 0 if we could not read anything, >0 for the number of bytes for the long on the buffer + */ + public final int canReadLong (ByteBuf buffer) { + int position = buffer.readerIndex(); + try { + int remaining = buffer.readableBytes(); + for (int offset = 0, count = 1; offset < 64 && remaining > 0; offset += 7, remaining--, count++) { + int b = buffer.readByte(); + if ((b & 0x80) == 0) { + return count; + } + } + return 0; + } finally { + buffer.readerIndex(position); + } + } +} diff --git a/Dorkbox-Util/src/dorkbox/util/crypto/BCrypt.java b/Dorkbox-Util/src/dorkbox/util/crypto/BCrypt.java new file mode 100644 index 0000000..12abf12 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/crypto/BCrypt.java @@ -0,0 +1,782 @@ +package dorkbox.util.crypto; + +// Copyright (c) 2006 Damien Miller +// +// GWT modified version. +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +import java.io.UnsupportedEncodingException; +import java.security.SecureRandom; +import java.util.Arrays; + +/** + * BCrypt implements OpenBSD-style Blowfish password hashing using + * the scheme described in "A Future-Adaptable Password Scheme" by + * Niels Provos and David Mazieres. + *

+ * This password hashing system tries to thwart off-line password + * cracking using a computationally-intensive hashing algorithm, + * based on Bruce Schneier's Blowfish cipher. The work factor of + * the algorithm is parameterised, so it can be increased as + * computers get faster. + *

+ * Usage is really simple. To hash a password for the first time, + * call the hashpw method with a random salt, like this: + *

+ * + * String pw_hash = BCrypt.hashpw(plain_password, BCrypt.gensalt());
+ *
+ *

+ * To check whether a plaintext password matches one that has been + * hashed previously, use the checkpw method: + *

+ * + * if (BCrypt.checkpw(candidate_password, stored_hash))
+ *     System.out.println("It matches");
+ * else
+ *     System.out.println("It does not match");
+ *
+ *

+ * The gensalt() method takes an optional parameter (log_rounds) + * that determines the computational complexity of the hashing: + *

+ * + * String strong_salt = BCrypt.gensalt(10)
+ * String stronger_salt = BCrypt.gensalt(12)
+ *
+ *

+ * The amount of work increases exponentially (2**log_rounds), so + * each increment is twice as much work. The default log_rounds is + * 10, and the valid range is 4 to 31. + * + * @author Damien Miller + * @version 0.2 + */ +public class BCrypt { + // BCrypt parameters + private static final int GENSALT_DEFAULT_LOG2_ROUNDS = 10; + private static final int BCRYPT_SALT_LEN = 16; + + // Blowfish parameters + private static final int BLOWFISH_NUM_ROUNDS = 16; + + // Initial contents of key schedule + private static final int P_orig[] = { + 0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, + 0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89, + 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, + 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917, + 0x9216d5d9, 0x8979fb1b + }; + private static final int S_orig[] = { + 0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, + 0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99, + 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, + 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e, + 0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee, + 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, + 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, + 0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e, + 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, + 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, + 0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce, + 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, + 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, + 0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677, + 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, + 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, + 0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88, + 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, + 0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, + 0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0, + 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, + 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, + 0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88, + 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, + 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, + 0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d, + 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, + 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, + 0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba, + 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463, + 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, + 0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09, + 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, + 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, + 0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279, + 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, + 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, + 0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82, + 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, + 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, + 0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0, + 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, + 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, + 0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8, + 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, + 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, + 0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7, + 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, + 0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, + 0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1, + 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, + 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, + 0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477, + 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, + 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, + 0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af, + 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, + 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, + 0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41, + 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915, + 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, + 0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915, + 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, + 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a, + 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, + 0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266, + 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, + 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, + 0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6, + 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, + 0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, + 0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1, + 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, + 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, + 0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff, + 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, + 0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, + 0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7, + 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41, + 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, + 0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf, + 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af, + 0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, + 0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87, + 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, + 0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2, + 0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16, + 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, + 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, + 0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509, + 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, + 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, + 0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f, + 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a, + 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, + 0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960, + 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, + 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, + 0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802, + 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, + 0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, + 0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf, + 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, + 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, + 0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50, + 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, + 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, + 0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281, + 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, + 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, + 0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128, + 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73, + 0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, + 0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0, + 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, + 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250, + 0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3, + 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, + 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, + 0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061, + 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, + 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, + 0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735, + 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc, + 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, + 0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340, + 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, + 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7, + 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, + 0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068, + 0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af, + 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, + 0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45, + 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, + 0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, + 0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb, + 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, + 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, + 0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42, + 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b, + 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, + 0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb, + 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, + 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, + 0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33, + 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c, + 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, + 0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc, + 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, + 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, + 0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b, + 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115, + 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, + 0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728, + 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, + 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, + 0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37, + 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d, + 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, + 0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b, + 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3, + 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, + 0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d, + 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c, + 0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, + 0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9, + 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, + 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, + 0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d, + 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, + 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, + 0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61, + 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, + 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, + 0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2, + 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c, + 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, + 0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633, + 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, + 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169, + 0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52, + 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, + 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, + 0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62, + 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, + 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, + 0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24, + 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc, + 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, + 0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c, + 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837, + 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0, + 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, + 0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe, + 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, + 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, + 0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8, + 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, + 0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, + 0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22, + 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, + 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, + 0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9, + 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, + 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, + 0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51, + 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, + 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, + 0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b, + 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28, + 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, + 0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd, + 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, + 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319, + 0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb, + 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, + 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, + 0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32, + 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, + 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, + 0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae, + 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, + 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, + 0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47, + 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, + 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, + 0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84, + 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, + 0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, + 0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd, + 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, + 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, + 0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38, + 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, + 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, + 0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525, + 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, + 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, + 0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964, + 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e, + 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, + 0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d, + 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, + 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, + 0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02, + 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, + 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, + 0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a, + 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, + 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, + 0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0, + 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, + 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, + 0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9, + 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, + 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6 + }; + + // bcrypt IV: "OrpheanBeholderScryDoubt" + static private final int bf_crypt_ciphertext[] = { + 0x4f727068, 0x65616e42, 0x65686f6c, + 0x64657253, 0x63727944, 0x6f756274 + }; + + // Table for Base64 encoding + static private final char base64_code[] = { + '.', '/', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', + 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', + 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', + '6', '7', '8', '9' + }; + + // Table for Base64 decoding + static private final byte index_64[] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, 0, 1, 54, 55, + 56, 57, 58, 59, 60, 61, 62, 63, -1, -1, + -1, -1, -1, -1, -1, 2, 3, 4, 5, 6, + 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, + -1, -1, -1, -1, -1, -1, 28, 29, 30, + 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, + 51, 52, 53, -1, -1, -1, -1, -1 + }; + + // Expanded Blowfish key + private int P[]; + private int S[]; + + /** + * Encode a byte array using bcrypt's slightly-modified base64 + * encoding scheme. Note that this is *not* compatible with + * the standard MIME-base64 encoding. + * + * @param d the byte array to encode + * @param len the number of bytes to encode + * @return base64-encoded string + * @exception IllegalArgumentException if the length is invalid + */ + private static String encode_base64(byte d[], int len) + throws IllegalArgumentException { + int off = 0; + StringBuilder rs = new StringBuilder(); + int c1, c2; + + if (len <= 0 || len > d.length) { + throw new IllegalArgumentException ("Invalid len"); + } + + while (off < len) { + c1 = d[off++] & 0xff; + rs.append(base64_code[c1 >> 2 & 0x3f]); + c1 = (c1 & 0x03) << 4; + if (off >= len) { + rs.append(base64_code[c1 & 0x3f]); + break; + } + c2 = d[off++] & 0xff; + c1 |= c2 >> 4 & 0x0f; + rs.append(base64_code[c1 & 0x3f]); + c1 = (c2 & 0x0f) << 2; + if (off >= len) { + rs.append(base64_code[c1 & 0x3f]); + break; + } + c2 = d[off++] & 0xff; + c1 |= c2 >> 6 & 0x03; + rs.append(base64_code[c1 & 0x3f]); + rs.append(base64_code[c2 & 0x3f]); + } + return rs.toString(); + } + + /** + * Look up the 3 bits base64-encoded by the specified character, range-checking againt conversion table + * + * @param x the base64-encoded value + * @return the decoded value of x + */ + private static byte char64(char x) { + if (x < 0 || x > index_64.length) { + return -1; + } + return index_64[x]; + } + + /** + * Decode a string encoded using bcrypt's base64 scheme to a byte array. Note that this is *not* compatible with + * the standard MIME-base64 encoding. + * + * @param s the string to decode + * @param maxolen the maximum number of bytes to decode + * @return an array containing the decoded bytes + * @throws IllegalArgumentException if maxolen is invalid + */ + private static byte[] decode_base64(String s, int maxolen) + throws IllegalArgumentException { + StringBuilder rs = new StringBuilder(); + int off = 0, slen = s.length(), olen = 0; + byte ret[]; + byte c1, c2, c3, c4, o; + + if (maxolen <= 0) { + throw new IllegalArgumentException ("Invalid maxolen"); + } + + while (off < slen - 1 && olen < maxolen) { + c1 = char64(s.charAt(off++)); + c2 = char64(s.charAt(off++)); + if (c1 == -1 || c2 == -1) { + break; + } + o = (byte)(c1 << 2); + o |= (c2 & 0x30) >> 4; + rs.append((char)o); + if (++olen >= maxolen || off >= slen) { + break; + } + c3 = char64(s.charAt(off++)); + if (c3 == -1) { + break; + } + o = (byte)((c2 & 0x0f) << 4); + o |= (c3 & 0x3c) >> 2; + rs.append((char)o); + if (++olen >= maxolen || off >= slen) { + break; + } + c4 = char64(s.charAt(off++)); + o = (byte)((c3 & 0x03) << 6); + o |= c4; + rs.append((char)o); + ++olen; + } + + ret = new byte[olen]; + for (off = 0; off < olen; off++) { + ret[off] = (byte)rs.charAt(off); + } + return ret; + } + + /** + * Blowfish encipher a single 64-bit block encoded as two 32-bit halves + * + * @param lr an array containing the two 32-bit half blocks + * @param off the position in the array of the blocks + */ + private final void encipher(int lr[], int off) { + int i, n, l = lr[off], r = lr[off + 1]; + + l ^= P[0]; + for (i = 0; i <= BLOWFISH_NUM_ROUNDS - 2;) { + // Feistel substitution on left word + n = S[l >> 24 & 0xff]; + n += S[0x100 | l >> 16 & 0xff]; + n ^= S[0x200 | l >> 8 & 0xff]; + n += S[0x300 | l & 0xff]; + r ^= n ^ P[++i]; + + // Feistel substitution on right word + n = S[r >> 24 & 0xff]; + n += S[0x100 | r >> 16 & 0xff]; + n ^= S[0x200 | r >> 8 & 0xff]; + n += S[0x300 | r & 0xff]; + l ^= n ^ P[++i]; + } + lr[off] = r ^ P[BLOWFISH_NUM_ROUNDS + 1]; + lr[off + 1] = l; + } + + /** + * Cycically extract a word of key material + * + * @param data the string to extract the data from + * @param offp a "pointer" (as a one-entry array) to the current offset into data + * @return the next word of material from data + */ + private static int streamtoword(byte data[], int offp[]) { + int i; + int word = 0; + int off = offp[0]; + + for (i = 0; i < 4; i++) { + word = word << 8 | data[off] & 0xff; + off = (off + 1) % data.length; + } + + offp[0] = off; + return word; + } + + /** + * Initialize the Blowfish key schedule + */ + private void init_key() { + P = Arrays.copyOf(P_orig, P_orig.length); + S = Arrays.copyOf(S_orig, S_orig.length); + } + + /** + * Key the Blowfish cipher + * @param key an array containing the key + */ + private void key(byte key[]) { + int i; + int koffp[] = { 0 }; + int lr[] = { 0, 0 }; + int plen = P.length, slen = S.length; + + for (i = 0; i < plen; i++) { + P[i] = P[i] ^ streamtoword(key, koffp); + } + + for (i = 0; i < plen; i += 2) { + encipher(lr, 0); + P[i] = lr[0]; + P[i + 1] = lr[1]; + } + + for (i = 0; i < slen; i += 2) { + encipher(lr, 0); + S[i] = lr[0]; + S[i + 1] = lr[1]; + } + } + + /** + * Perform the "enhanced key schedule" step described by Provos and Mazieres in "A Future-Adaptable Password Scheme" + * http://www.openbsd.org/papers/bcrypt-paper.ps + * + * @param data salt information + * @param key password information + */ + private void ekskey(byte data[], byte key[]) { + int i; + int koffp[] = { 0 }, doffp[] = { 0 }; + int lr[] = { 0, 0 }; + int plen = P.length, slen = S.length; + + for (i = 0; i < plen; i++) { + P[i] = P[i] ^ streamtoword(key, koffp); + } + + for (i = 0; i < plen; i += 2) { + lr[0] ^= streamtoword(data, doffp); + lr[1] ^= streamtoword(data, doffp); + encipher(lr, 0); + P[i] = lr[0]; + P[i + 1] = lr[1]; + } + + for (i = 0; i < slen; i += 2) { + lr[0] ^= streamtoword(data, doffp); + lr[1] ^= streamtoword(data, doffp); + encipher(lr, 0); + S[i] = lr[0]; + S[i + 1] = lr[1]; + } + } + + /** + * Perform the central password hashing step in the bcrypt scheme + * + * @param password the password to hash + * @param salt the binary salt to hash with the password + * @param log_rounds the binary logarithm of the number of rounds of hashing to apply + * @return an array containing the binary hashed password + */ + private byte[] crypt_raw(byte password[], byte salt[], int log_rounds) { + int rounds, i, j; + int cdata[] = Arrays.copyOf(bf_crypt_ciphertext, bf_crypt_ciphertext.length); + int clen = cdata.length; + byte ret[]; + + if (log_rounds < 4 || log_rounds > 31) { + throw new IllegalArgumentException ("Bad number of rounds"); + } + rounds = 1 << log_rounds; + if (salt.length != BCRYPT_SALT_LEN) { + throw new IllegalArgumentException ("Bad salt length"); + } + + init_key(); + ekskey(salt, password); + for (i = 0; i < rounds; i++) { + key(password); + key(salt); + } + + for (i = 0; i < 64; i++) { + for (j = 0; j < clen >> 1; j++) { + encipher(cdata, j << 1); + } + } + + ret = new byte[clen * 4]; + for (i = 0, j = 0; i < clen; i++) { + ret[j++] = (byte)(cdata[i] >> 24 & 0xff); + ret[j++] = (byte)(cdata[i] >> 16 & 0xff); + ret[j++] = (byte)(cdata[i] >> 8 & 0xff); + ret[j++] = (byte)(cdata[i] & 0xff); + } + return ret; + } + + /** + * Hash a password using the OpenBSD bcrypt scheme + * + * @param password the password to hash + * @return the hashed password + */ + public static String hashpw(String password) { + return hashpw(password, BCrypt.gensalt()); + } + + /** + * Hash a password using the OpenBSD bcrypt scheme + * + * @param password the password to hash + * @param salt the salt to hash with (perhaps generated using BCrypt.gensalt) + * @return the hashed password + */ + public static String hashpw(String password, String salt) { + BCrypt B; + String real_salt; + byte passwordb[], saltb[], hashed[]; + char minor = (char)0; + int rounds, off = 0; + StringBuilder rs = new StringBuilder(); + + if (salt.charAt(0) != '$' || salt.charAt(1) != '2') { + throw new IllegalArgumentException ("Invalid salt version"); + } + if (salt.charAt(2) == '$') { + off = 3; + } else { + minor = salt.charAt(2); + if (minor != 'a' || salt.charAt(3) != '$') { + throw new IllegalArgumentException ("Invalid salt revision"); + } + off = 4; + } + + // Extract number of rounds + if (salt.charAt(off + 2) > '$') { + throw new IllegalArgumentException ("Missing salt rounds"); + } + rounds = Integer.parseInt(salt.substring(off, off + 2)); + + real_salt = salt.substring(off + 3, off + 25); + try { + passwordb = (password + (minor >= 'a' ? "\000" : "")).getBytes("UTF-8"); + } catch (UnsupportedEncodingException uee) { + throw new AssertionError("UTF-8 is not supported"); + } + + saltb = decode_base64(real_salt, BCRYPT_SALT_LEN); + + B = new BCrypt(); + hashed = B.crypt_raw(passwordb, saltb, rounds); + + rs.append("$2"); + if (minor >= 'a') { + rs.append(minor); + } + rs.append("$"); + if (rounds < 10) { + rs.append("0"); + } + rs.append(String.valueOf(rounds)); + rs.append("$"); + rs.append(encode_base64(saltb, saltb.length)); + rs.append(encode_base64(hashed, bf_crypt_ciphertext.length * 4 - 1)); + return rs.toString(); + } + + /** + * Generate a salt for use with the BCrypt.hashpw() method + * @param log_rounds the log2 of the number of rounds of + * hashing to apply - the work factor therefore increases as + * 2**log_rounds. + * @param random an instance of SecureRandom to use + * @return an encoded salt value + */ + public static String gensalt(int log_rounds, SecureRandom random) { + StringBuilder rs = new StringBuilder(); + byte rnd[] = new byte[BCRYPT_SALT_LEN]; + + random.nextBytes(rnd); + + rs.append("$2a$"); + if (log_rounds < 10) { + rs.append("0"); + } + rs.append(String.valueOf(log_rounds)); + rs.append("$"); + rs.append(encode_base64(rnd, rnd.length)); + return rs.toString(); + } + + /** + * Generate a salt for use with the BCrypt.hashpw() method + * @param log_rounds the log2 of the number of rounds of + * hashing to apply - the work factor therefore increases as + * 2**log_rounds. + * @return an encoded salt value + */ + public static String gensalt(int log_rounds) { + return gensalt(log_rounds, new SecureRandom()); + } + + /** + * Generate a salt for use with the BCrypt.hashpw() method, + * selecting a reasonable default for the number of hashing + * rounds to apply + * @return an encoded salt value + */ + public static String gensalt() { + return gensalt(GENSALT_DEFAULT_LOG2_ROUNDS); + } + + /** + * Check that a plaintext password matches a previously hashed + * one + * @param plaintext the plaintext password to verify + * @param hashed the previously-hashed password + * @return true if the passwords match, false otherwise + */ + public static boolean checkpw(String plaintext, String hashed) { + return hashed.compareTo(hashpw(plaintext, hashed)) == 0; + } +} diff --git a/Dorkbox-Util/src/dorkbox/util/crypto/Crypto.java b/Dorkbox-Util/src/dorkbox/util/crypto/Crypto.java new file mode 100644 index 0000000..0497867 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/crypto/Crypto.java @@ -0,0 +1,1761 @@ +package dorkbox.util.crypto; + + +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.security.Provider; +import java.security.SecureRandom; +import java.security.Security; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; + +import org.bouncycastle.crypto.AsymmetricBlockCipher; +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.crypto.BufferedBlockCipher; +import org.bouncycastle.crypto.CipherParameters; +import org.bouncycastle.crypto.DataLengthException; +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.bouncycastle.crypto.PBEParametersGenerator; +import org.bouncycastle.crypto.agreement.ECDHCBasicAgreement; +import org.bouncycastle.crypto.digests.SHA1Digest; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.digests.SHA384Digest; +import org.bouncycastle.crypto.digests.SHA512Digest; +import org.bouncycastle.crypto.engines.IESEngine; +import org.bouncycastle.crypto.generators.DSAKeyPairGenerator; +import org.bouncycastle.crypto.generators.DSAParametersGenerator; +import org.bouncycastle.crypto.generators.ECKeyPairGenerator; +import org.bouncycastle.crypto.generators.KDF2BytesGenerator; +import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator; +import org.bouncycastle.crypto.generators.RSAKeyPairGenerator; +import org.bouncycastle.crypto.macs.HMac; +import org.bouncycastle.crypto.modes.GCMBlockCipher; +import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher; +import org.bouncycastle.crypto.params.DSAKeyGenerationParameters; +import org.bouncycastle.crypto.params.DSAParameters; +import org.bouncycastle.crypto.params.DSAPrivateKeyParameters; +import org.bouncycastle.crypto.params.DSAPublicKeyParameters; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECKeyGenerationParameters; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.crypto.params.IESParameters; +import org.bouncycastle.crypto.params.IESWithCipherParameters; +import org.bouncycastle.crypto.params.KeyParameter; +import org.bouncycastle.crypto.params.ParametersWithIV; +import org.bouncycastle.crypto.params.ParametersWithRandom; +import org.bouncycastle.crypto.params.RSAKeyGenerationParameters; +import org.bouncycastle.crypto.params.RSAKeyParameters; +import org.bouncycastle.crypto.params.RSAPrivateCrtKeyParameters; +import org.bouncycastle.crypto.signers.DSASigner; +import org.bouncycastle.crypto.signers.ECDSASigner; +import org.bouncycastle.crypto.signers.PSSSigner; +import org.bouncycastle.jcajce.provider.util.DigestFactory; +import org.bouncycastle.jce.ECNamedCurveTable; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jce.spec.ECParameterSpec; +import org.bouncycastle.math.ec.ECFieldElement; +import org.bouncycastle.math.ec.ECPoint; + +/** + * http://en.wikipedia.org/wiki/NSA_Suite_B + * http://www.nsa.gov/ia/programs/suiteb_cryptography/ + * + * NSA Suite B + * + * TOP-SECRET LEVEL + * AES256/GCM + * ECC with 384-bit prime curve (FIPS PUB 186-3), and SHA-384 + * + * SECRET LEVEL + * AES 128 + * ECDH and ECDSA using the 256-bit prime (FIPS PUB 186-3), and SHA-256. RSA with 2048 can be used for DH key negotiation + * + * WARNING! + * Note that this call is INCOMPATIBLE with GWT, so we have EXCLUDED IT from gwt, and created a CryptoGwt class in the web-client project + * which only has the necessary crypto utility methods that are + * 1) Necessary + * 2) Compatible with GWT + * + */ +public class Crypto { + private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(Crypto.class); + + public static final void addProvider() { + // make sure we only add it once (in case it's added elsewhere...) + Provider provider = Security.getProvider("BC"); + if (provider == null) { + Security.addProvider(new BouncyCastleProvider()); + } + } + + public static class Util { + + /** + * Return the hash of the file or NULL if file is invalid + */ + public static final byte[] hashFile(File file, Digest digest) throws IOException { + return hashFile(file, digest, 0L); + } + + /** + * Return the hash of the file or NULL if file is invalid + */ + public static final byte[] hashFile(File file, Digest digest, long lengthFromEnd) throws IOException { + if (file.isFile() && file.canRead()) { + InputStream inputStream = new FileInputStream(file); + long size = file.length(); + + if (lengthFromEnd > 0 && lengthFromEnd < size) { + size -= lengthFromEnd; + } + + try { + int bufferSize = 4096; + byte[] buffer = new byte[bufferSize]; + + int readBytes = 0; + digest.reset(); + + while (size > 0) { + int maxToRead = (int) Math.min(bufferSize, size); + readBytes = inputStream.read(buffer, 0, maxToRead); + size -= readBytes; + + if (readBytes == 0) { + //wtf. finally still gets called. + return null; + } + + digest.update(buffer, 0, readBytes); + } + } finally { + inputStream.close(); + } + + byte[] digestBytes = new byte[digest.getDigestSize()]; + + digest.doFinal(digestBytes, 0); + return digestBytes; + + } else { + return null; + } + } + + /** + * Specifically, to return the hash of the ALL files/directories inside the jar, minus the action specified (LGPL) files. + */ + public static final byte[] hashJarContentsExcludeAction(JarFile jarFile, Digest digest, int action) throws IOException { + // repack token: ':|', from BoxHandler. + // if this is CHANGED, make sure to update it there as well. + String token = ":|"; + + Enumeration jarElements = jarFile.entries(); + + boolean okToHash = false; + byte[] buffer = new byte[2048]; + int read = 0; + digest.reset(); + + while (jarElements.hasMoreElements()) { + JarEntry jarEntry = jarElements.nextElement(); + String name = jarEntry.getName(); + okToHash = !jarEntry.isDirectory(); + + if (!okToHash) { + continue; + } + + okToHash = false; + int startIndex = name.lastIndexOf(token); // lastIndexOf, in case there are multiple box files stacked in eachother + if (startIndex > -1) { + String type = name.substring(startIndex + 2); + int parseInt = Integer.parseInt(type); + + if ((parseInt & action) != action) { + okToHash = true; + } + } + + // skips hashing lgpl files. (technically, whatever our action bitmask is...) + if (okToHash) { + InputStream inputStream = jarFile.getInputStream(jarEntry); + + while ((read = inputStream.read(buffer)) > 0) { + digest.update(buffer, 0, read); + } + inputStream.close(); + } + } + + byte[] digestBytes = new byte[digest.getDigestSize()]; + + digest.doFinal(digestBytes, 0); + return digestBytes; + } + + /** + * return the hash of the specified files/directories inside the jar. + */ + public static final byte[] hashJarContents(JarFile jarFile, Digest digest, String... filesOrDirsToHash) throws IOException { + Enumeration jarElements = jarFile.entries(); + + boolean okToHash = false; + byte[] buffer = new byte[2048]; + int read = 0; + digest.reset(); + + while (jarElements.hasMoreElements()) { + JarEntry jarEntry = jarElements.nextElement(); + String name = jarEntry.getName(); + okToHash = !jarEntry.isDirectory(); + + if (!okToHash) { + continue; + } + + okToHash = false; + if (filesOrDirsToHash != null) { + for (String dir : filesOrDirsToHash) { + if (name.startsWith(dir)) { + okToHash = true; + break; + } + } + } else { + okToHash = true; + } + + if (okToHash) { + InputStream inputStream = jarFile.getInputStream(jarEntry); + + while ((read = inputStream.read(buffer)) > 0) { + digest.update(buffer, 0, read); + } + inputStream.close(); + } + } + + byte[] digestBytes = new byte[digest.getDigestSize()]; + + digest.doFinal(digestBytes, 0); + return digestBytes; + } + + /** + * Encrypts the contents (avoiding the specified files/dirs) of a jar. + * + * This will KEEP the order of the contents of the jar! + * @throws IOException + */ + public static void encryptJarContents(JarFile jarFile, GCMBlockCipher aesEngine, byte[] aesRaw, byte[] ivRaw, + String... filesOrdirectoriesToAvoid) throws IOException { + + // encrypt to a stream, then save that stream BACK to the file... "in place encryption" + int blockSize = aesEngine.getUnderlyingCipher().getBlockSize(); + byte[] aesKey = new byte[blockSize]; + byte[] ivKey = new byte[blockSize]; + + System.arraycopy(aesRaw, 0, aesKey, 0, blockSize); + System.arraycopy(ivKey, 0, ivRaw, 0, blockSize); + + // now encrypt the jar + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + JarOutputStream jarOutputStream = new JarOutputStream(new BufferedOutputStream(byteArrayOutputStream)); + jarOutputStream.setLevel(9); // COMPRESSION LEVEL + + boolean okToCrypt = false; + + Enumeration jarEntries = jarFile.entries(); + while (jarEntries.hasMoreElements()) { + JarEntry jarEntry = jarEntries.nextElement(); + String name = jarEntry.getName(); + + okToCrypt = !jarEntry.isDirectory(); + + if (!okToCrypt) { + continue; + } + + okToCrypt = true; + for (String dir : filesOrdirectoriesToAvoid) { + if (name.startsWith(dir)) { + okToCrypt = false; + break; + } + } + + if (okToCrypt) { + InputStream inputStream = jarFile.getInputStream(jarEntry); + Crypto.AES.encryptStream(aesEngine, aesKey, ivKey, inputStream, jarOutputStream); + inputStream.close(); + jarOutputStream.flush(); + jarOutputStream.closeEntry(); + } + } + + // finish the stream that we have been writing to + jarOutputStream.finish(); + jarOutputStream.close(); + + jarFile.close(); + } + + + /** + * Hash an input stream, based on the specified digest + */ + public static byte[] hashStream(Digest digest, InputStream inputStream) throws IOException { + + byte[] buffer = new byte[2048]; + int read = 0; + digest.reset(); + + + while ((read = inputStream.read(buffer)) > 0) { + digest.update(buffer, 0, read); + } + inputStream.close(); + + byte[] digestBytes = new byte[digest.getDigestSize()]; + + digest.doFinal(digestBytes, 0); + return digestBytes; + } + + /** + * Hash an input stream (auto-converts to an output stream first), based on the specified digest + */ + public static byte[] hashStream(Digest digest, ByteArrayOutputStream outputStream) throws IOException { + ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray()); + + return hashStream(digest, inputStream); + } + + + /** + * Secure way to generate an AES key based on a password. Will '*' out the passed-in password + * + * @param password will be filled with '*' + * @param salt should be a RANDOM number, at least 256bits (32 bytes) in size. + * @param iterationCount should be a lot, like 10,000 + * @return the secure key to use + */ + public static final byte[] PBKDF2(char[] password, byte[] salt, int iterationCount) { + // will also zero out the password. + byte[] charToBytes = Crypto.Util.charToBytesPassword(password); + + return PBKDF2(charToBytes, salt, iterationCount); + } + + /** + * Secure way to generate an AES key based on a password. + * + * @param password + * @param salt should be a RANDOM number, at least 256bits (32 bytes) in size. + * @param iterationCount should be a lot, like 10,000 + * @return the secure key to use + */ + public static final byte[] PBKDF2(byte[] password, byte[] salt, int iterationCount) { + SHA256Digest digest = new SHA256Digest(); + PBEParametersGenerator pGen = new PKCS5S2ParametersGenerator(digest); + pGen.init(password, salt, iterationCount); + + KeyParameter key = (KeyParameter) pGen.generateDerivedMacParameters(digest.getDigestSize() * 8); // *8 for bit length. + + // zero out the password. + Arrays.fill(password, (byte)0); + + return key.getKey(); + } + + + /** this saves the char array in UTF-16 format of bytes and BLANKS out the password char array. */ + public static final byte[] charToBytesPassword(char[] password) { + // note: this saves the char array in UTF-16 format of bytes. + byte[] passwordBytes = new byte[password.length*2]; + for(int i=0; i>8); + passwordBytes[2*i+1] = (byte) (password[i] & 0x00FF); + } + + // asterisk out the password + Arrays.fill(password, '*'); + + return passwordBytes; + } + } + + + public static class AES { + private static final int ivSize = 16; + + /** + * AES encrypts data with a specified key. + * + * @return empty byte[] if error + */ + public static final byte[] encryptWithIV(GCMBlockCipher aesEngine, byte[] aesKey, byte[] aesIV, byte[] data) { + byte[] encryptAES = encrypt(aesEngine, aesKey, aesIV, data); + + int length = encryptAES.length; + + byte[] out = new byte[length+ivSize]; + System.arraycopy(aesIV, 0, out, 0, ivSize); + System.arraycopy(encryptAES, 0, out, ivSize, length); + + return out; + } + + /** + * CONVENIENCE METHOD ONLY - DO NOT USE UNLESS YOU HAVE TO + *

+ * Use GCM instead, as it's an authenticated cipher (and "regular" AES is not). This prevents tampering with the blocks of encrypted data. + *

+ * AES encrypts data with a specified key. + * + * @return empty byte[] if error + */ + @Deprecated + public static final byte[] encryptWithIV(BufferedBlockCipher aesEngine, byte[] aesKey, byte[] aesIV, byte[] data) { + + byte[] encryptAES = encrypt(aesEngine, aesKey, aesIV, data); + + int length = encryptAES.length; + + byte[] out = new byte[length+ivSize]; + System.arraycopy(aesIV, 0, out, 0, ivSize); + System.arraycopy(encryptAES, 0, out, ivSize, length); + + return out; + } + + /** + * AES encrypts data with a specified key. + * + * @return true if successful + */ + public static final boolean encryptStreamWithIV(GCMBlockCipher aesEngine, byte[] aesKey, byte[] aesIV, + InputStream in, OutputStream out) { + + try { + out.write(aesIV); + } catch (IOException e) { + logger.error("Unable to perform AES cipher.", e); + return false; + } + + boolean success = encryptStream(aesEngine, aesKey, aesIV, in, out); + return success; + } + + /** + * CONVENIENCE METHOD ONLY - DO NOT USE UNLESS YOU HAVE TO + *

+ * Use GCM instead, as it's an authenticated cipher (and "regular" AES is not). This prevents tampering with the blocks of encrypted data. + *

+ * AES encrypts data with a specified key. + * + * @return true if successful + */ + @Deprecated + public static final boolean encryptStreamWithIV(BufferedBlockCipher aesEngine, byte[] aesKey, byte[] aesIV, + InputStream in, OutputStream out) { + + try { + out.write(aesIV); + } catch (IOException e) { + logger.error("Unable to perform AES cipher.", e); + return false; + } + + boolean success = encryptStream(aesEngine, aesKey, aesIV, in, out); + return success; + } + + + /** + * AES encrypts data with a specified key. + * + * @return empty byte[] if error + */ + public static final byte[] encrypt(GCMBlockCipher aesEngine, byte[] aesKey, byte[] aesIV, byte[] data) { + int length = data.length; + + CipherParameters aesIVAndKey = new ParametersWithIV(new KeyParameter(aesKey), aesIV); + aesEngine.init(true, aesIVAndKey); + + int minSize = aesEngine.getOutputSize(length); + byte[] outBuf = new byte[minSize]; + + int actualLength = aesEngine.processBytes(data, 0, length, outBuf, 0); + + try { + actualLength += aesEngine.doFinal(outBuf, actualLength); + } catch (DataLengthException e) { + logger.error("Unable to perform AES cipher.", e); + return new byte[0]; + } catch (IllegalStateException e) { + logger.error("Unable to perform AES cipher.", e); + return new byte[0]; + } catch (InvalidCipherTextException e) { + logger.error("Unable to perform AES cipher.", e); + return new byte[0]; + } + + if (outBuf.length == actualLength) { + return outBuf; + } else { + byte[] result = new byte[actualLength]; + System.arraycopy(outBuf, 0, result, 0, result.length); + return result; + } + } + + + /** + * AES encrypts data with a specified key. + * + * @return length of encrypted data, -1 if there was an error. + */ + public static final int encrypt(dorkbox.util.crypto.bouncycastle.GCMBlockCipher_ByteBuf aesEngine, CipherParameters aesIVAndKey, + io.netty.buffer.ByteBuf inBuffer, io.netty.buffer.ByteBuf outBuffer, int length) { + + aesEngine.reset(); + aesEngine.init(true, aesIVAndKey); + + length = aesEngine.processBytes(inBuffer, outBuffer, length); + + try { + length += aesEngine.doFinal(outBuffer); + } catch (DataLengthException e) { + logger.debug("Unable to perform AES cipher.", e); + return -1; + } catch (IllegalStateException e) { + logger.debug("Unable to perform AES cipher.", e); + return -1; + } catch (InvalidCipherTextException e) { + logger.debug("Unable to perform AES cipher.", e); + return -1; + } + + // specify where the encrypted data is at + outBuffer.readerIndex(0); + outBuffer.writerIndex(length); + + return length; + } + + + + /** + * CONVENIENCE METHOD ONLY - DO NOT USE UNLESS YOU HAVE TO + *

+ * Use GCM instead, as it's an authenticated cipher (and "regular" AES is not). This prevents tampering with the blocks of encrypted data. + *

+ * AES encrypts data with a specified key. + * + * @return empty byte[] if error + */ + @Deprecated + public static final byte[] encrypt(BufferedBlockCipher aesEngine, byte[] aesKey, byte[] aesIV, byte[] data) { + int length = data.length; + + CipherParameters aesIVAndKey = new ParametersWithIV(new KeyParameter(aesKey), aesIV); + aesEngine.init(true, aesIVAndKey); + + int minSize = aesEngine.getOutputSize(length); + byte[] outBuf = new byte[minSize]; + + int actualLength = aesEngine.processBytes(data, 0, length, outBuf, 0); + + try { + actualLength += aesEngine.doFinal(outBuf, actualLength); + } catch (DataLengthException e) { + logger.error("Unable to perform AES cipher.", e); + return new byte[0]; + } catch (IllegalStateException e) { + logger.error("Unable to perform AES cipher.", e); + return new byte[0]; + } catch (InvalidCipherTextException e) { + logger.error("Unable to perform AES cipher.", e); + return new byte[0]; + } + + if (outBuf.length == actualLength) { + return outBuf; + } else { + byte[] result = new byte[actualLength]; + System.arraycopy(outBuf, 0, result, 0, result.length); + return result; + } + } + + /** + * CONVENIENCE METHOD ONLY - DO NOT USE UNLESS YOU HAVE TO + *

+ * Use GCM instead, as it's an authenticated cipher (and "regular" AES is not). This prevents tampering with the blocks of encrypted data. + *

+ * AES encrypt from one stream to another. + * + * @return true if successful + */ + @Deprecated + public static final boolean encryptStream(BufferedBlockCipher aesEngine, byte[] aesKey, byte[] aesIV, + InputStream in, OutputStream out) { + byte[] buf = new byte[ivSize]; + byte[] outbuf = new byte[512]; + + CipherParameters aesIVAndKey = new ParametersWithIV(new KeyParameter(aesKey), aesIV); + aesEngine.init(true, aesIVAndKey); + + try { + int bytesRead = 0; + int bytesProcessed = 0; + + while ((bytesRead = in.read(buf)) >= 0) { + bytesProcessed = aesEngine.processBytes(buf, 0, bytesRead, outbuf, 0); + out.write(outbuf, 0, bytesProcessed); + } + + bytesProcessed = aesEngine.doFinal(outbuf, 0); + + out.write(outbuf, 0, bytesProcessed); + out.flush(); + } catch (IOException e) { + logger.error("Unable to perform AES cipher.", e); + return false; + } catch (DataLengthException e) { + logger.error("Unable to perform AES cipher.", e); + return false; + } catch (IllegalStateException e) { + logger.error("Unable to perform AES cipher.", e); + return false; + } catch (InvalidCipherTextException e) { + logger.error("Unable to perform AES cipher.", e); + return false; + } + + return true; + } + + /** + * AES encrypt from one stream to another. + * + * @return true if successful + */ + public static final boolean encryptStream(GCMBlockCipher aesEngine, byte[] aesKey, byte[] aesIV, + InputStream in, OutputStream out) { + + byte[] buf = new byte[ivSize]; + byte[] outbuf = new byte[512]; + + CipherParameters aesIVAndKey = new ParametersWithIV(new KeyParameter(aesKey), aesIV); + aesEngine.init(true, aesIVAndKey); + + try { + int bytesRead = 0; + int bytesProcessed = 0; + + while ((bytesRead = in.read(buf)) >= 0) { + bytesProcessed = aesEngine.processBytes(buf, 0, bytesRead, outbuf, 0); + out.write(outbuf, 0, bytesProcessed); + } + + bytesProcessed = aesEngine.doFinal(outbuf, 0); + + out.write(outbuf, 0, bytesProcessed); + out.flush(); + } catch (IOException e) { + logger.error("Unable to perform AES cipher.", e); + return false; + } catch (DataLengthException e) { + logger.error("Unable to perform AES cipher.", e); + return false; + } catch (IllegalStateException e) { + logger.error("Unable to perform AES cipher.", e); + return false; + } catch (InvalidCipherTextException e) { + logger.error("Unable to perform AES cipher.", e); + return false; + } + + return true; + } + + /** + * AES decrypt (if the aes IV is included in the data) + * + * @return empty byte[] if error + */ + public static final byte[] decryptWithIV(GCMBlockCipher aesEngine, byte[] aesKey, byte[] data) { + byte[] aesIV = new byte[ivSize]; + System.arraycopy(data, 0, aesIV, 0, ivSize); + + byte[] in = new byte[data.length-ivSize]; + System.arraycopy(data, ivSize, in, 0, in.length); + + return decrypt(aesEngine, aesKey, aesIV, in); + } + + /** + * CONVENIENCE METHOD ONLY - DO NOT USE UNLESS YOU HAVE TO + *

+ * Use GCM instead, as it's an authenticated cipher (and "regular" AES is not). This prevents tampering with the blocks of encrypted data. + *

+ * AES decrypt (if the aes IV is included in the data) + * + * @return empty byte[] if error + */ + @Deprecated + public static final byte[] decryptWithIV(BufferedBlockCipher aesEngine, byte[] aesKey, byte[] data) { + byte[] aesIV = new byte[ivSize]; + System.arraycopy(data, 0, aesIV, 0, ivSize); + + byte[] in = new byte[data.length-ivSize]; + System.arraycopy(data, ivSize, in, 0, in.length); + + return decrypt(aesEngine, aesKey, aesIV, in); + } + + /** + * AES decrypt (if the aes IV is included in the data) + * + * @return true if successful + */ + public static final boolean decryptStreamWithIV(GCMBlockCipher aesEngine, byte[] aesKey, + InputStream in, OutputStream out) { + byte[] aesIV = new byte[ivSize]; + try { + in.read(aesIV, 0, ivSize); + } catch (IOException e) { + logger.error("Unable to perform AES cipher.", e); + return false; + } + + boolean success = decryptStream(aesEngine, aesKey, aesIV, in, out); + return success; + } + + /** + * CONVENIENCE METHOD ONLY - DO NOT USE UNLESS YOU HAVE TO + *

+ * Use GCM instead, as it's an authenticated cipher (and "regular" AES is not). This prevents tampering with the blocks of encrypted data. + *

+ * AES decrypt (if the aes IV is included in the data) + * + * @return true if successful + */ + @Deprecated + public static final boolean decryptStreamWithIV(BufferedBlockCipher aesEngine, byte[] aesKey, + InputStream in, OutputStream out) { + byte[] aesIV = new byte[ivSize]; + try { + in.read(aesIV, 0, ivSize); + } catch (IOException e) { + logger.error("Unable to perform AES cipher.", e); + return false; + } + + boolean success = decryptStream(aesEngine, aesKey, aesIV, in, out); + return success; + } + + /** + * AES decrypt (if we already know the aes IV -- and it's NOT included in the data) + * + * @return empty byte[] if error + */ + public static final byte[] decrypt(GCMBlockCipher aesEngine, byte[] aesKey, byte[] aesIV, byte[] data) { + int length = data.length; + + CipherParameters aesIVAndKey = new ParametersWithIV(new KeyParameter(aesKey), aesIV); + aesEngine.init(false, aesIVAndKey); + + int minSize = aesEngine.getOutputSize(length); + byte[] outBuf = new byte[minSize]; + + int actualLength = aesEngine.processBytes(data, 0, length, outBuf, 0); + + try { + actualLength += aesEngine.doFinal(outBuf, actualLength); + } catch (DataLengthException e) { + logger.error("Unable to perform AES cipher.", e); + return new byte[0]; + } catch (IllegalStateException e) { + logger.error("Unable to perform AES cipher.", e); + return new byte[0]; + } catch (InvalidCipherTextException e) { + logger.error("Unable to perform AES cipher.", e); + return new byte[0]; + } + + if (outBuf.length == actualLength) { + return outBuf; + } else { + byte[] result = new byte[actualLength]; + System.arraycopy(outBuf, 0, result, 0, result.length); + return result; + } + } + + /** + * AES decrypt (if we already know the aes IV -- and it's NOT included in the data) + * + * @return length of decrypted data, -1 if there was an error. + */ + public static final int decrypt(dorkbox.util.crypto.bouncycastle.GCMBlockCipher_ByteBuf aesEngine, ParametersWithIV aesIVAndKey, + io.netty.buffer.ByteBuf bufferWithData, io.netty.buffer.ByteBuf bufferTempData, int length) { + + aesEngine.reset(); + aesEngine.init(false, aesIVAndKey); + + // ignore the start position + // we also do NOT want to have the same start position for the altBuffer, since it could then grow larger than the buffer capacity. + length = aesEngine.processBytes(bufferWithData, bufferTempData, length); + + try { + length += aesEngine.doFinal(bufferTempData); + } catch (DataLengthException e) { + logger.debug("Unable to perform AES cipher.", e); + return -1; + } catch (IllegalStateException e) { + logger.debug("Unable to perform AES cipher.", e); + return -1; + } catch (InvalidCipherTextException e) { + logger.debug("Unable to perform AES cipher.", e); + return -1; + } + + bufferTempData.readerIndex(0); + bufferTempData.writerIndex(length); + + return length; + } + + /** + * CONVENIENCE METHOD ONLY - DO NOT USE UNLESS YOU HAVE TO + *

+ * Use GCM instead, as it's an authenticated cipher (and "regular" AES is not). This prevents tampering with the blocks of encrypted data. + *

+ * AES decrypt (if we already know the aes IV -- and it's NOT included in the data) + * + * @return empty byte[] if error + */ + @Deprecated + public static final byte[] decrypt(BufferedBlockCipher aesEngine, byte[] aesKey, byte[] aesIV, byte[] data) { + + int length = data.length; + + CipherParameters aesIVAndKey = new ParametersWithIV(new KeyParameter(aesKey), aesIV); + aesEngine.init(false, aesIVAndKey); + + int minSize = aesEngine.getOutputSize(length); + byte[] outBuf = new byte[minSize]; + + int actualLength = aesEngine.processBytes(data, 0, length, outBuf, 0); + + try { + actualLength += aesEngine.doFinal(outBuf, actualLength); + } catch (DataLengthException e) { + logger.error("Unable to perform AES cipher.", e); + return new byte[0]; + } catch (IllegalStateException e) { + logger.error("Unable to perform AES cipher.", e); + return new byte[0]; + } catch (InvalidCipherTextException e) { + logger.error("Unable to perform AES cipher.", e); + return new byte[0]; + } + + if (outBuf.length == actualLength) { + return outBuf; + } else { + byte[] result = new byte[actualLength]; + System.arraycopy(outBuf, 0, result, 0, result.length); + return result; + } + } + + /** + * AES decrypt from one stream to another. + * + * @return true if successful + */ + public static final boolean decryptStream(GCMBlockCipher aesEngine, byte[] aesKey, byte[] aesIV, + InputStream in, OutputStream out) { + byte[] buf = new byte[ivSize]; + byte[] outbuf = new byte[512]; + + CipherParameters aesIVAndKey = new ParametersWithIV(new KeyParameter(aesKey), aesIV); + aesEngine.init(false, aesIVAndKey); + + try { + int bytesRead = 0; + int bytesProcessed = 0; + + while ((bytesRead = in.read(buf)) >= 0) { + bytesProcessed = aesEngine.processBytes(buf, 0, bytesRead, outbuf, 0); + out.write(outbuf, 0, bytesProcessed); + } + + bytesProcessed = aesEngine.doFinal(outbuf, 0); + + out.write(outbuf, 0, bytesProcessed); + out.flush(); + } catch (IOException e) { + logger.error("Unable to perform AES cipher.", e); + return false; + } catch (DataLengthException e) { + logger.error("Unable to perform AES cipher.", e); + return false; + } catch (IllegalStateException e) { + logger.error("Unable to perform AES cipher.", e); + return false; + } catch (InvalidCipherTextException e) { + logger.error("Unable to perform AES cipher.", e); + return false; + } + + return true; + } + + /** + * CONVENIENCE METHOD ONLY - DO NOT USE UNLESS YOU HAVE TO + *

+ * Use GCM instead, as it's an authenticated cipher (and "regular" AES is not). This prevents tampering with the blocks of encrypted data. + *

+ * AES decrypt from one stream to another. + * + * @return true if successful + */ + @Deprecated + public static final boolean decryptStream(BufferedBlockCipher aesEngine, byte[] aesKey, byte[] aesIV, + InputStream in, OutputStream out) { + byte[] buf = new byte[ivSize]; + byte[] outbuf = new byte[512]; + + CipherParameters aesIVAndKey = new ParametersWithIV(new KeyParameter(aesKey), aesIV); + aesEngine.init(false, aesIVAndKey); + + try { + int bytesRead = 0; + int bytesProcessed = 0; + + while ((bytesRead = in.read(buf)) >= 0) { + bytesProcessed = aesEngine.processBytes(buf, 0, bytesRead, outbuf, 0); + out.write(outbuf, 0, bytesProcessed); + } + + bytesProcessed = aesEngine.doFinal(outbuf, 0); + + out.write(outbuf, 0, bytesProcessed); + out.flush(); + } catch (IOException e) { + logger.error("Unable to perform AES cipher.", e); + return false; + } catch (DataLengthException e) { + logger.error("Unable to perform AES cipher.", e); + return false; + } catch (IllegalStateException e) { + logger.error("Unable to perform AES cipher.", e); + return false; + } catch (InvalidCipherTextException e) { + logger.error("Unable to perform AES cipher.", e); + return false; + } + + return true; + } + } + + + + + + // Note: this is here just for keeping track of how this is done. This should NOT be used, and instead use ECC crypto. + @Deprecated + public static class RSA { + + public static final AsymmetricCipherKeyPair generateKeyPair(SecureRandom secureRandom, int keyLength) { + RSAKeyPairGenerator keyGen = new RSAKeyPairGenerator(); + RSAKeyGenerationParameters params = new RSAKeyGenerationParameters(new BigInteger("65537"), // public exponent + secureRandom, //pnrg + keyLength, // key length + 8); //the number of iterations of the Miller-Rabin primality test. + keyGen.init(params); + return keyGen.generateKeyPair(); + } + + + /** + * RSA encrypt using public key A, and sign data with private key B. + * + * byte[0][] = encrypted data + * byte[1][] = signature + * + * @return empty byte[][] if error + */ + public static final byte[][] encryptAndSign(AsymmetricBlockCipher rsaEngine, Digest digest, + RSAKeyParameters rsaPublicKeyA, RSAPrivateCrtKeyParameters rsaPrivateKeyB, + byte[] bytes) { + if (bytes.length == 0) { + return new byte[0][0]; + } + + byte[] encryptBytes = encrypt(rsaEngine, rsaPublicKeyA, bytes); + + if (encryptBytes.length == 0) { + return new byte[0][0]; + } + + // now sign it. + PSSSigner signer = new PSSSigner(rsaEngine, digest, digest.getDigestSize()); + + byte[] signatureRSA = Crypto.RSA.sign(signer, rsaPrivateKeyB, encryptBytes); + + if (signatureRSA.length == 0) { + return new byte[0][0]; + } + + byte[][] total = new byte[2][]; + total[0] = encryptBytes; + total[1] = signatureRSA; + + + return total; + } + + /** + * RSA verify data with public key B, and decrypt using private key A. + * + * @return empty byte[] if error + */ + public static final byte[] decryptAndVerify(AsymmetricBlockCipher rsaEngine, Digest digest, + RSAKeyParameters rsaPublicKeyA, RSAPrivateCrtKeyParameters rsaPrivateKeyB, + byte[] encryptedData, byte[] signature) { + if (encryptedData.length == 0 || signature.length == 0) { + return new byte[0]; + } + + // verify encrypted data. + PSSSigner signer = new PSSSigner(rsaEngine, digest, digest.getDigestSize()); + + boolean verify = verify(signer, rsaPublicKeyA, signature, encryptedData); + if (!verify) { + return new byte[0]; + } + + byte[] decryptBytes = decrypt(rsaEngine, rsaPrivateKeyB, encryptedData); + + return decryptBytes; + + } + + /** + * RSA encrypts data with a specified key. + * + * @return empty byte[] if error + */ + public static final byte[] encrypt(AsymmetricBlockCipher rsaEngine, RSAKeyParameters rsaPublicKey, byte[] bytes) { + rsaEngine.init(true, rsaPublicKey); + + try { + int inputBlockSize = rsaEngine.getInputBlockSize(); + if (inputBlockSize < bytes.length) { + int outSize = rsaEngine.getOutputBlockSize(); + int realsize = (int) Math.round(bytes.length/(outSize*1.0D)+.5); + ByteBuffer buffer = ByteBuffer.allocateDirect(outSize * realsize); + + int position = 0; + + while (position < bytes.length) { + int size = Math.min(inputBlockSize, bytes.length - position); + + byte[] block = rsaEngine.processBlock(bytes, position, size); + buffer.put(block, 0, block.length); + + position += size; + } + + + return buffer.array(); + + } else { + return rsaEngine.processBlock(bytes, 0, bytes.length); + } + } catch (InvalidCipherTextException e) { + logger.error("Unable to perform RSA cipher.", e); + return new byte[0]; + } + } + + /** + * RSA decrypt data with a specified key. + * + * @return empty byte[] if error + */ + public static final byte[] decrypt(AsymmetricBlockCipher rsaEngine, RSAPrivateCrtKeyParameters rsaPrivateKey, byte[] bytes) { + rsaEngine.init(false, rsaPrivateKey); + + try { + int inputBlockSize = rsaEngine.getInputBlockSize(); + if (inputBlockSize < bytes.length) { + int outSize = rsaEngine.getOutputBlockSize(); + int realsize = (int) Math.round(bytes.length/(outSize*1.0D)+.5); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(outSize * realsize); + + int position = 0; + + while (position < bytes.length) { + int size = Math.min(inputBlockSize, bytes.length - position); + + byte[] block = rsaEngine.processBlock(bytes, position, size); + buffer.write(block, 0, block.length); + + position += size; + } + + + return buffer.toByteArray(); + } else { + return rsaEngine.processBlock(bytes, 0, bytes.length); + } + } catch (InvalidCipherTextException e) { + logger.error("Unable to perform RSA cipher.", e); + return new byte[0]; + } + } + + /** + * RSA sign data with a specified key. + * + * @return empty byte[] if error + */ + public static final byte[] sign(PSSSigner signer, RSAPrivateCrtKeyParameters rsaPrivateKey, byte[] mesg) { + signer.init(true, rsaPrivateKey); + signer.update(mesg, 0, mesg.length); + + try { + return signer.generateSignature(); + } catch (Exception e) { + logger.error("Unable to perform RSA cipher.", e); + return new byte[0]; + } + } + + /** + * RSA verify data with a specified key. + */ + public static final boolean verify(PSSSigner signer, RSAKeyParameters rsaPublicKey, byte[] sig, byte[] mesg) { + signer.init(false, rsaPublicKey); + signer.update(mesg, 0, mesg.length); + + return signer.verifySignature(sig); + } + + public static boolean compare(RSAKeyParameters publicA, RSAKeyParameters publicB) { + if (!publicA.getExponent().equals(publicB.getExponent())) { + return false; + } + if (!publicA.getModulus().equals(publicB.getModulus())) { + return false; + } + + return true; + } + + public static boolean compare(RSAPrivateCrtKeyParameters private1, RSAPrivateCrtKeyParameters private2) { + if (!private1.getModulus().equals(private2.getModulus())) { + return false; + } + if (!private1.getExponent().equals(private2.getExponent())) { + return false; + } + if (!private1.getDP().equals(private2.getDP())) { + return false; + } + if (!private1.getDQ().equals(private2.getDQ())) { + return false; + } + if (!private1.getP().equals(private2.getP())) { + return false; + } + if (!private1.getPublicExponent().equals(private2.getPublicExponent())) { + return false; + } + if (!private1.getQ().equals(private2.getQ())) { + return false; + } + if (!private1.getQInv().equals(private2.getQInv())) { + return false; + } + + return true; + } + } + + + + + // Note: this is here just for keeping track of how this is done. This should NOT be used, and instead use ECC crypto. + @Deprecated + public static class DSA { + /** + * Generates the DSA key (using RSA and SHA1) + *

+ * Note: this is here just for keeping track of how this is done. This should NOT be used, and instead use ECC crypto. + */ + public static final AsymmetricCipherKeyPair generateKeyPair(SecureRandom secureRandom, int keyLength) { + DSAKeyPairGenerator keyGen = new DSAKeyPairGenerator(); + + DSAParametersGenerator dsaParametersGenerator = new DSAParametersGenerator(); + dsaParametersGenerator.init(keyLength, 20, secureRandom); + DSAParameters generateParameters = dsaParametersGenerator.generateParameters(); + + DSAKeyGenerationParameters params = new DSAKeyGenerationParameters(secureRandom, + generateParameters); + keyGen.init(params); + return keyGen.generateKeyPair(); + } + + /** + * The message will have the SHA1 hash calculated and used for the signature. + *

+ * Note: this is here just for keeping track of how this is done. This should NOT be used, and instead use ECC crypto. + * + * The returned signature is the {r,s} signature array. + */ + public static final BigInteger[] generateSignature(DSAPrivateKeyParameters privateKey, SecureRandom secureRandom, byte[] message) { + ParametersWithRandom param = new ParametersWithRandom(privateKey, secureRandom); + + DSASigner dsa = new DSASigner(); + + dsa.init(true, param); + + + SHA1Digest sha1Digest = new SHA1Digest(); + byte[] checksum = new byte[sha1Digest.getDigestSize()]; + + sha1Digest.update(message, 0, message.length); + sha1Digest.doFinal(checksum, 0); + + + BigInteger[] signature = dsa.generateSignature(checksum); + return signature; + } + + /** + * The message will have the SHA1 hash calculated and used for the signature. + *

+ * Note: this is here just for keeping track of how this is done. This should NOT be used, and instead use ECC crypto. + * + * @param signature is the {r,s} signature array. + * @return true if the signature is valid + */ + public static final boolean verifySignature(DSAPublicKeyParameters publicKey, byte[] message, BigInteger[] signature) { + SHA1Digest sha1Digest = new SHA1Digest(); + byte[] checksum = new byte[sha1Digest.getDigestSize()]; + + sha1Digest.update(message, 0, message.length); + sha1Digest.doFinal(checksum, 0); + + + DSASigner dsa = new DSASigner(); + + dsa.init(false, publicKey); + + boolean verifySignature = dsa.verifySignature(checksum, signature[0], signature[1]); + return verifySignature; + } + } + + + + public static class ECC { + static final String ECC_NAME = "EC"; + public static final String p521_curve = "secp521r1"; + + // more info about ECC from: http://www.johannes-bauer.com/compsci/ecc/?menuid=4 + // http://stackoverflow.com/questions/7419183/problems-implementing-ecdh-on-android-using-bouncycastle + // http://tools.ietf.org/html/draft-jivsov-openpgp-ecc-06#page-4 + // http://www.nsa.gov/ia/programs/suiteb_cryptography/ + // https://github.com/nelenkov/ecdh-kx/blob/master/src/org/nick/ecdhkx/Crypto.java + // http://nelenkov.blogspot.com/2011/12/using-ecdh-on-android.html + // http://www.secg.org/collateral/sec1_final.pdf + + public static final int macSize = 512; + + /** + * Uses SHA512 + */ + public static final IESEngine createEngine() { + return new IESEngine(new ECDHCBasicAgreement(), + new KDF2BytesGenerator(new SHA384Digest()), + new HMac(new SHA512Digest())); + } + + /** + * Uses SHA512 + */ + public static final IESEngine createEngine(PaddedBufferedBlockCipher aesEngine) { + return new IESEngine(new ECDHCBasicAgreement(), + new KDF2BytesGenerator(new SHA384Digest()), + new HMac(new SHA512Digest()), + aesEngine); + } + + /** + * These parameters are shared between the two parties. These are a NONCE (use ONCE number!!) + */ + public static final IESParameters generateSharedParameters(SecureRandom secureRandom) { + + int macSize = Crypto.ECC.macSize; // must be the MAC size + + // MUST be random EACH TIME encrypt/sign happens! + byte[] derivation = new byte[macSize/8]; + byte[] encoding = new byte[macSize/8]; + + secureRandom.nextBytes(derivation); + secureRandom.nextBytes(encoding); + + return new IESParameters(derivation, encoding, macSize); + } + + /** + * AES-256 ONLY! + */ + public static IESWithCipherParameters generateSharedParametersWithCipher(SecureRandom secureRandom) { + int macSize = Crypto.ECC.macSize; // must be the MAC size + + byte[] derivation = new byte[macSize/8]; // MUST be random EACH TIME encrypt/sign happens! + byte[] encoding = new byte[macSize/8]; + + secureRandom.nextBytes(derivation); + secureRandom.nextBytes(encoding); + + return new IESWithCipherParameters(derivation, encoding, macSize, 256); + } + + + public static final AsymmetricCipherKeyPair generateKeyPair(String eccCurveName, SecureRandom secureRandom) { + ECParameterSpec eccSpec = ECNamedCurveTable.getParameterSpec(eccCurveName); + + return generateKeyPair(eccSpec, secureRandom); + } + + public static final AsymmetricCipherKeyPair generateKeyPair(ECParameterSpec eccSpec, SecureRandom secureRandom) { + ECKeyGenerationParameters ecParams = new ECKeyGenerationParameters(new ECDomainParameters(eccSpec.getCurve() , + eccSpec.getG(), + eccSpec.getN()), + secureRandom); + + ECKeyPairGenerator ecKeyGen = new ECKeyPairGenerator(); + ecKeyGen.init(ecParams); + + return ecKeyGen.generateKeyPair(); + } + + /** + * ECC encrypts data with a specified key. + * + * @return empty byte[] if error + */ + public static final byte[] encrypt(IESEngine eccEngine, CipherParameters private1, CipherParameters public2, + IESParameters cipherParams, byte[] message) { + + eccEngine.init(true, private1, public2, cipherParams); + + try { + return eccEngine.processBlock(message, 0, message.length); + } catch (InvalidCipherTextException e) { + logger.error("Unable to perform ECC cipher.", e); + return new byte[0]; + } + } + + /** + * ECC decrypt data with a specified key. + * + * @return empty byte[] if error + */ + public static final byte[] decrypt(IESEngine eccEngine, CipherParameters private2, CipherParameters public1, + IESParameters cipherParams, byte[] encrypted) { + + eccEngine.init(false, private2, public1, cipherParams); + + try { + return eccEngine.processBlock(encrypted, 0, encrypted.length); + } catch (InvalidCipherTextException e) { + logger.error("Unable to perform ECC cipher.", e); + return new byte[0]; + } + } + + public static final boolean compare(ECPrivateKeyParameters privateA, ECPrivateKeyParameters privateB) { + ECDomainParameters parametersA = privateA.getParameters(); + ECDomainParameters parametersB = privateB.getParameters(); + + // is it the same curve? + boolean equals = parametersA.getCurve().equals(parametersB.getCurve()); + if (!equals) { + return false; + } + + equals = parametersA.getG().equals(parametersB.getG()); + if (!equals) { + return false; + } + + + equals = parametersA.getH().equals(parametersB.getH()); + if (!equals) { + return false; + } + + equals = parametersA.getN().equals(parametersB.getN()); + if (!equals) { + return false; + } + + equals = privateA.getD().equals(privateB.getD()); + + return equals; + } + + /** + * @return true if publicA and publicB are NOT NULL, and are both equal to eachother + */ + public static final boolean compare(ECPublicKeyParameters publicA, ECPublicKeyParameters publicB) { + if (publicA == null || publicB == null) { + return false; + } + + + ECDomainParameters parametersA = publicA.getParameters(); + ECDomainParameters parametersB = publicB.getParameters(); + + // is it the same curve? + boolean equals = parametersA.getCurve().equals(parametersB.getCurve()); + if (!equals) { + return false; + } + + equals = parametersA.getG().equals(parametersB.getG()); + if (!equals) { + return false; + } + + + equals = parametersA.getH().equals(parametersB.getH()); + if (!equals) { + return false; + } + + equals = parametersA.getN().equals(parametersB.getN()); + if (!equals) { + return false; + } + + + ECPoint normalizeA = publicA.getQ().normalize(); + ECPoint normalizeB = publicB.getQ().normalize(); + + + ECFieldElement xCoordA = normalizeA.getXCoord(); + ECFieldElement xCoordB = normalizeB.getXCoord(); + + equals = xCoordA.equals(xCoordB); + if (!equals) { + return false; + } + + ECFieldElement yCoordA = normalizeA.getYCoord(); + ECFieldElement yCoordB = normalizeB.getYCoord(); + + equals = yCoordA.equals(yCoordB); + if (!equals) { + return false; + } + + return true; + } + + public static final boolean compare(IESParameters cipherAParams, IESParameters cipherBParams) { + if (!Arrays.equals(cipherAParams.getDerivationV(), cipherBParams.getDerivationV())) { + return false; + } + if (!Arrays.equals(cipherAParams.getEncodingV(), cipherBParams.getEncodingV())) { + return false; + } + + if (cipherAParams.getMacKeySize() != cipherBParams.getMacKeySize()) { + return false; + } + return true; + } + + public static final boolean compare(IESWithCipherParameters cipherAParams, IESWithCipherParameters cipherBParams) { + if (cipherAParams.getCipherKeySize() != cipherBParams.getCipherKeySize()) { + return false; + } + + // only need to cast one side. + return compare((IESParameters)cipherAParams, cipherBParams); + } + + + /** + * The message will have the (digestName) hash calculated and used for the signature. + * + * The returned signature is the {r,s} signature array. + */ + public static final BigInteger[] generateSignature(String digestName, ECPrivateKeyParameters privateKey, SecureRandom secureRandom, byte[] bytes) { + + Digest digest = DigestFactory.getDigest(digestName); + + byte[] checksum = new byte[digest.getDigestSize()]; + + digest.update(bytes, 0, bytes.length); + digest.doFinal(checksum, 0); + + return generateSignatureForHash(privateKey, secureRandom, checksum); + } + + /** + * The message will use the bytes AS THE HASHED VALUE to calculate the signature. + * + * The returned signature is the {r,s} signature array. + */ + public static final BigInteger[] generateSignatureForHash(ECPrivateKeyParameters privateKey, SecureRandom secureRandom, byte[] hashBytes) { + + ParametersWithRandom param = new ParametersWithRandom(privateKey, secureRandom); + + ECDSASigner ecdsa = new ECDSASigner(); + ecdsa.init(true, param); + + BigInteger[] signature = ecdsa.generateSignature(hashBytes); + return signature; + } + + /** + * The message will have the (digestName) hash calculated and used for the signature. + * + * @param signature is the {r,s} signature array. + * @return true if the signature is valid + */ + public static final boolean verifySignature(String digestName, ECPublicKeyParameters publicKey, byte[] message, BigInteger[] signature) { + + Digest digest = DigestFactory.getDigest(digestName); + + byte[] checksum = new byte[digest.getDigestSize()]; + + digest.update(message, 0, message.length); + digest.doFinal(checksum, 0); + + + return verifySignatureHash(publicKey, checksum, signature); + } + + /** + * The provided hash will be used in the signature verification. + * + * @param signature is the {r,s} signature array. + * @return true if the signature is valid + */ + public static final boolean verifySignatureHash(ECPublicKeyParameters publicKey, byte[] hash, BigInteger[] signature) { + + ECDSASigner ecdsa = new ECDSASigner(); + ecdsa.init(false, publicKey); + + + boolean verifySignature = ecdsa.verifySignature(hash, signature[0], signature[1]); + return verifySignature; + } + } + + + + /** + * An implementation of the scrypt + * key derivation function. + */ + public static class SCrypt { + + /** + * Hash the supplied plaintext password and generate output using default parameters + *

+ * The password chars are no longer valid after this call + * + * @param password Password. + * @param salt Salt parameter + */ + public static final String encrypt(char[] password) { + return encrypt(password, 16384, 32, 1); + } + + /** + * Hash the supplied plaintext password and generate output using default parameters + *

+ * The password chars are no longer valid after this call + * + * @param password Password. + * @param salt Salt parameter + */ + public static final String encrypt(char[] password, byte[] salt) { + return encrypt(password, salt, 16384, 32, 1, 64); + } + + /** + * Hash the supplied plaintext password and generate output. + *

+ * The password chars are no longer valid after this call + * + * @param password Password. + * @param N CPU cost parameter. + * @param r Memory cost parameter. + * @param p Parallelization parameter. + * + * @return The hashed password. + */ + public static final String encrypt(char[] password, int N, int r, int p) { + SecureRandom secureRandom = new SecureRandom(); + byte[] salt = new byte[32]; + secureRandom.nextBytes(salt); + + return encrypt(password, salt, N, r, p, 64); + } + + /** + * Hash the supplied plaintext password and generate output. + *

+ * The password chars are no longer valid after this call + * + * @param password Password. + * @param salt Salt parameter + * @param N CPU cost parameter. + * @param r Memory cost parameter. + * @param p Parallelization parameter. + * @param dkLen Intended length of the derived key. + * + * @return The hashed password. + */ + public static final String encrypt(char[] password, byte[] salt, int N, int r, int p, int dkLen) { + // Note: this saves the char array in UTF-16 format of bytes. + // can't use password after this as it's been changed to '*' + byte[] passwordBytes = Crypto.Util.charToBytesPassword(password); + + byte[] derived = encrypt(passwordBytes, salt, N, r, p, dkLen); + + String params = Integer.toString(log2(N) << 16 | r << 8 | p, 16); + + StringBuilder sb = new StringBuilder((salt.length + derived.length) * 2); + sb.append("$s0$").append(params).append('$'); + sb.append(dorkbox.util.Base64Fast.encodeToString(salt, false)).append('$'); + sb.append(dorkbox.util.Base64Fast.encodeToString(derived, false)); + + return sb.toString(); + } + + /** + * Compare the supplied plaintext password to a hashed password. + * + * @param password Plaintext password. + * @param hashed scrypt hashed password. + * + * @return true if password matches hashed value. + */ + public static final boolean verify(char[] password, String hashed) { + // Note: this saves the char array in UTF-16 format of bytes. + // can't use password after this as it's been changed to '*' + byte[] passwordBytes = Crypto.Util.charToBytesPassword(password); + + String[] parts = hashed.split("\\$"); + + if (parts.length != 5 || !parts[1].equals("s0")) { + throw new IllegalArgumentException("Invalid hashed value"); + } + + int params = Integer.parseInt(parts[2], 16); + byte[] salt = dorkbox.util.Base64Fast.decodeFast(parts[3]); + byte[] derived0 = dorkbox.util.Base64Fast.decodeFast(parts[4]); + + int N = (int) Math.pow(2, params >> 16 & 0xFF); + int r = params >> 8 & 0xFF; + int p = params & 0xFF; + + int length = derived0.length; + if (length == 0) { + return false; + } + + byte[] derived1 = encrypt(passwordBytes, salt, N, r, p, length); + + if (length != derived1.length) { + return false; + } + + int result = 0; + for (int i = 0; i < length; i++) { + result |= derived0[i] ^ derived1[i]; + } + + return result == 0; + } + + private static final int log2(int n) { + int log = 0; + if ((n & 0xFFFF0000 ) != 0) { n >>>= 16; log = 16; } + if (n >= 256) { n >>>= 8; log += 8; } + if (n >= 16 ) { n >>>= 4; log += 4; } + if (n >= 4 ) { n >>>= 2; log += 2; } + return log + (n >>> 1); + } + + + /** + * Pure Java implementation of the scrypt KDF. + * + * @param password Password. + * @param salt Salt. + * @param N CPU cost parameter. + * @param r Memory cost parameter. + * @param p Parallelization parameter. + * @param dkLen Intended length of the derived key. + * + * @return The derived key. + */ + public static byte[] encrypt(byte[] password, byte[] salt, int N, int r, int p, int dkLen) { + if (N == 0 || (N & N - 1) != 0) { + throw new IllegalArgumentException("N must be > 0 and a power of 2"); + } + + if (N > Integer.MAX_VALUE / 128 / r) { + throw new IllegalArgumentException("Parameter N is too large"); + } + if (r > Integer.MAX_VALUE / 128 / p) { + throw new IllegalArgumentException("Parameter r is too large"); + } + + try { + return org.bouncycastle.crypto.generators.SCrypt.generate(password, salt, N, r, p, dkLen); + } finally { + // now zero out the bytes in password. + Arrays.fill(password, (byte)0); + } + } + } +} diff --git a/Dorkbox-Util/src/dorkbox/util/crypto/CryptoX509.java b/Dorkbox-Util/src/dorkbox/util/crypto/CryptoX509.java new file mode 100644 index 0000000..71d308d --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/crypto/CryptoX509.java @@ -0,0 +1,1292 @@ +package dorkbox.util.crypto; + + + +import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Writer; +import java.lang.reflect.Method; +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.PublicKey; +import java.security.Security; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.spec.DSAParameterSpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPrivateCrtKeySpec; +import java.security.spec.RSAPublicKeySpec; +import java.util.Date; +import java.util.Enumeration; + +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1Encoding; +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.ASN1Set; +import org.bouncycastle.asn1.ASN1TaggedObject; +import org.bouncycastle.asn1.BERSet; +import org.bouncycastle.asn1.DERBMPString; +import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.DERSet; +import org.bouncycastle.asn1.cms.CMSObjectIdentifiers; +import org.bouncycastle.asn1.cms.ContentInfo; +import org.bouncycastle.asn1.cms.IssuerAndSerialNumber; +import org.bouncycastle.asn1.cms.SignedData; +import org.bouncycastle.asn1.cms.SignerIdentifier; +import org.bouncycastle.asn1.cms.SignerInfo; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.pkcs.RSAPublicKey; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.Certificate; +import org.bouncycastle.asn1.x509.DSAParameter; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.SubjectKeyIdentifier; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.asn1.x9.X9ObjectIdentifiers; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; +import org.bouncycastle.cms.CMSProcessableByteArray; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.CMSTypedData; +import org.bouncycastle.cms.DefaultCMSSignatureAlgorithmNameGenerator; +import org.bouncycastle.crypto.params.AsymmetricKeyParameter; +import org.bouncycastle.crypto.params.DSAParameters; +import org.bouncycastle.crypto.params.DSAPrivateKeyParameters; +import org.bouncycastle.crypto.params.DSAPublicKeyParameters; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.crypto.params.RSAKeyParameters; +import org.bouncycastle.crypto.params.RSAPrivateCrtKeyParameters; +import org.bouncycastle.jcajce.provider.asymmetric.dsa.BCDSAPublicKey; +import org.bouncycastle.jcajce.provider.asymmetric.dsa.BCDSAPublicKeyAccessor; +import org.bouncycastle.jcajce.provider.asymmetric.dsa.DSAUtil; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; +import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPublicKey; +import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPublicKeyAccessor; +import org.bouncycastle.jcajce.provider.asymmetric.rsa.RSAUtil; +import org.bouncycastle.jcajce.provider.asymmetric.x509.X509Accessor; +import org.bouncycastle.jce.PrincipalUtil; +import org.bouncycastle.jce.interfaces.PKCS12BagAttributeCarrier; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jce.provider.JCEECPublicKey; +import org.bouncycastle.jce.provider.X509CertificateObject; +import org.bouncycastle.jce.spec.ECParameterSpec; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.ContentVerifierProvider; +import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder; +import org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.bc.BcContentSignerBuilder; +import org.bouncycastle.operator.bc.BcDSAContentSignerBuilder; +import org.bouncycastle.operator.bc.BcDSAContentVerifierProviderBuilder; +import org.bouncycastle.operator.bc.BcRSAContentSignerBuilder; +import org.bouncycastle.operator.bc.BcRSAContentVerifierProviderBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import dorkbox.util.Base64Fast; +import dorkbox.util.crypto.signers.BcECDSAContentSignerBuilder; +import dorkbox.util.crypto.signers.BcECDSAContentVerifierProviderBuilder; + +public class CryptoX509 { + + private static final Logger logger = LoggerFactory.getLogger(CryptoX509.class); + + public static final void addProvider() { + // make sure we only add it once (in case it's added elsewhere...) + Provider provider = Security.getProvider("BC"); + if (provider == null) { + Security.addProvider(new BouncyCastleProvider()); + } + } + + public static class Util { + + /** + * @return true if saving the x509 certificate to a PEM format file was successful + */ + public static boolean convertToPemFile(X509Certificate x509cert, String fileName) { + boolean failed = false; + Writer output = null; + + try { + String lineSeparator = "\r\n"; + + String cert_begin = "-----BEGIN CERTIFICATE-----"; + String cert_end = "-----END CERTIFICATE-----"; + + byte[] derCert = x509cert.getEncoded(); + char[] encodeToChar = Base64Fast.encodeToChar(derCert, false); + + int newLineCount = encodeToChar.length/64; + + int length = encodeToChar.length; + + output = new BufferedWriter(new FileWriter("dorkbox.crt", false), + cert_begin.length() + cert_end.length() + length + newLineCount + 3); + + output.write(cert_begin); + output.write(lineSeparator); + + int copyCount = 64; + for (int i=0;i length) { + copyCount = length - i; + } + + output.write(encodeToChar, i, copyCount); + output.write(lineSeparator); + } + output.write(cert_end); + output.write(lineSeparator); + } catch (Exception e) { + logger.error("Error during conversion.", e); + failed = true; + } finally { + if (output != null) { + try { + output.close(); + } catch (IOException e) { + logger.error("Error closing resource.", e); + } + } + } + + return !failed; + } + + public static String convertToPem(X509Certificate x509cert) throws CertificateEncodingException { + String lineSeparator = "\r\n"; + + String cert_begin = "-----BEGIN CERTIFICATE-----"; + String cert_end = "-----END CERTIFICATE-----"; + + byte[] derCert = x509cert.getEncoded(); + char[] encodeToChar = Base64Fast.encodeToChar(derCert, false); + + int newLineCount = encodeToChar.length/64; + + int length = encodeToChar.length; + int lastIndex = 0; + StringBuilder sb = new StringBuilder(cert_begin.length() + cert_end.length() + length + newLineCount + 2); + + sb.append(cert_begin); + sb.append(lineSeparator); + for (int i=64;i length) { + i = length; + } + + sb.append(encodeToChar, lastIndex, i); + sb.append(lineSeparator); + lastIndex = i; + } + sb.append(cert_end); + + return sb.toString(); + } + + public static String getDigestNameFromCert(X509CertificateHolder x509CertificateHolder) { + String digestName = CryptoX509.Util.getDigestNameFromSigAlgId(x509CertificateHolder.getSignatureAlgorithm().getAlgorithm()); + return digestName; + } + + public static String getDigestNameFromSigAlgId(ASN1ObjectIdentifier algorithm) { + String digest = null; + try { + // have to use reflection in order to access the DIGEST method used by the key. + DefaultCMSSignatureAlgorithmNameGenerator defaultCMSSignatureAlgorithmNameGenerator = new DefaultCMSSignatureAlgorithmNameGenerator(); + Method declaredMethod = DefaultCMSSignatureAlgorithmNameGenerator.class.getDeclaredMethod("getDigestAlgName", new Class[] {ASN1ObjectIdentifier.class}); + declaredMethod.setAccessible(true); + digest = (String) declaredMethod.invoke(defaultCMSSignatureAlgorithmNameGenerator, algorithm); + } catch (Throwable t) { + throw new RuntimeException("Weird error using reflection to get the digest name: " + algorithm.getId() + t.getMessage()); + } + + if (algorithm.getId().equals(digest)) { + throw new RuntimeException("Unable to get digest name from algorithm ID: " + algorithm.getId()); + } + + return digest; + } + + +// @SuppressWarnings("rawtypes") +// public static void verify(JarFile jf, X509Certificate[] trustedCaCerts) throws IOException, CertificateException { +// Vector entriesVec = new Vector(); +// +// // Ensure there is a manifest file +// Manifest man = jf.getManifest(); +// if (man == null) { +// throw new SecurityException("The JAR is not signed"); +// } +// +// // Ensure all the entries' signatures verify correctly +// byte[] buffer = new byte[8192]; +// Enumeration entries = jf.entries(); +// +// while (entries.hasMoreElements()) { +// JarEntry je = (JarEntry) entries.nextElement(); +// entriesVec.addElement(je); +// InputStream is = jf.getInputStream(je); +// @SuppressWarnings("unused") +// int n; +// while ((n = is.read(buffer, 0, buffer.length)) != -1) { +// // we just read. this will throw a SecurityException +// // if a signature/digest check fails. +// } +// is.close(); +// } +// jf.close(); +// +// // Get the list of signer certificates +// Enumeration e = entriesVec.elements(); +// while (e.hasMoreElements()) { +// JarEntry je = (JarEntry) e.nextElement(); +// +// if (je.isDirectory()) { +// continue; +// } +// // Every file must be signed - except +// // files in META-INF +// Certificate[] certs = je.getCertificates(); +// if (certs == null || certs.length == 0) { +// if (!je.getName().startsWith("META-INF")) { +// throw new SecurityException("The JCE framework has unsigned class files."); +// } +// } else { +// // Check whether the file +// // is signed as expected. +// // The framework may be signed by +// // multiple signers. At least one of +// // the signers must be a trusted signer. +// +// // First, determine the roots of the certificate chains +// X509Certificate[] chainRoots = getChainRoots(certs); +// boolean signedAsExpected = false; +// +// for (int i = 0; i < chainRoots.length; i++) { +// if (isTrusted(chainRoots[i], trustedCaCerts)) { +// signedAsExpected = true; +// break; +// } +// } +// +// if (!signedAsExpected) { +// throw new SecurityException("The JAR is not signed by a trusted signer"); +// } +// } +// } +// } + + public static boolean isTrusted(X509Certificate cert, X509Certificate[] trustedCaCerts) { + // Return true iff either of the following is true: + // 1) the cert is in the trustedCaCerts. + // 2) the cert is issued by a trusted CA. + + // Check whether the cert is in the trustedCaCerts + for (int i = 0; i < trustedCaCerts.length; i++) { + // If the cert has the same SubjectDN + // as a trusted CA, check whether + // the two certs are the same. + if (cert.getSubjectDN().equals(trustedCaCerts[i].getSubjectDN())) { + if (cert.equals(trustedCaCerts[i])) { + return true; + } + } + } + + // Check whether the cert is issued by a trusted CA. + // Signature verification is expensive. So we check + // whether the cert is issued + // by one of the trusted CAs if the above loop failed. + for (int i = 0; i < trustedCaCerts.length; i++) { + // If the issuer of the cert has the same name as + // a trusted CA, check whether that trusted CA + // actually issued the cert. + if (cert.getIssuerDN().equals(trustedCaCerts[i].getSubjectDN())) { + try { + cert.verify(trustedCaCerts[i].getPublicKey()); + return true; + } catch (Exception e) { + // Do nothing. + } + } + } + + return false; + } + +// public static X509Certificate[] getChainRoots(Certificate[] certs) { +// Vector result = new Vector(3); +// // choose a Vector size that seems reasonable +// for (int i = 0; i < certs.length - 1; i++) { +// if (!((X509Certificate) certs[i + 1]).getSubjectDN().equals( +// ((X509Certificate) certs[i]).getIssuerDN())) { +// // We've reached the end of a chain +// result.addElement((X509Certificate) certs[i]); +// } +// } +// +// // The final entry in the certs array is always +// // a "root" certificate +// result.addElement((X509Certificate) certs[certs.length - 1]); +// X509Certificate[] ret = new X509Certificate[result.size()]; +// result.copyInto(ret); +// +// return ret; +// } + } + + + public static class DSA { + static { + addProvider(); + } + + /** + * Creates a X509 certificate holder object.

+ * + * Look at BCStyle for a list of all valid X500 Names. + */ + public static X509CertificateHolder createCertHolder(Date startDate, Date expiryDate, + X500Name issuerName, X500Name subjectName, BigInteger serialNumber, + DSAPrivateKeyParameters privateKey, DSAPublicKeyParameters publicKey) { + + String signatureAlgorithm = "SHA1withDSA"; + + + AlgorithmIdentifier sigAlgId = new DefaultSignatureAlgorithmIdentifierFinder().find(signatureAlgorithm); + AlgorithmIdentifier digAlgId = new DefaultDigestAlgorithmIdentifierFinder().find(sigAlgId); + + + SubjectPublicKeyInfo subjectPublicKeyInfo = null; + DSAParameters parameters = publicKey.getParameters(); + try { + byte[] encoded = new SubjectPublicKeyInfo(new AlgorithmIdentifier(X9ObjectIdentifiers.id_dsa, + new DSAParameter(parameters.getP(), + parameters.getQ(), + parameters.getG())), + new ASN1Integer(publicKey.getY())).getEncoded(ASN1Encoding.DER); + + ASN1Sequence seq = (ASN1Sequence)ASN1Primitive.fromByteArray(encoded); + subjectPublicKeyInfo = new SubjectPublicKeyInfo(seq); + } catch (IOException e) { + logger.error("Error during DSA.", e); + return null; + } + + X509v3CertificateBuilder v3CertBuilder = new X509v3CertificateBuilder(issuerName, + serialNumber, startDate, expiryDate, + subjectName, subjectPublicKeyInfo); + + BcDSAContentSignerBuilder contentSignerBuilder = new BcDSAContentSignerBuilder(sigAlgId, digAlgId); + + ContentSigner build = null; + try { + build = contentSignerBuilder.build(privateKey); + } catch (OperatorCreationException e) { + logger.error("Error creating certificate.", e); + return null; + } + + X509CertificateHolder certHolder = v3CertBuilder.build(build); + return certHolder; + } + + + /** + * Verifies that the certificate is legitimate. + *

+ * MUST have BouncyCastle provider loaded by the security manager! + *

+ * @return true if it was a valid cert. + */ + public static final boolean validate(X509CertificateHolder x509CertificateHolder) { + try { + + // this is unique in that it verifies that the certificate is a LEGIT certificate, but not necessarily + // valid during this time period. + ContentVerifierProvider contentVerifierProvider = new BcDSAContentVerifierProviderBuilder( + new DefaultDigestAlgorithmIdentifierFinder()).build(x509CertificateHolder); + + boolean signatureValid = x509CertificateHolder.isSignatureValid(contentVerifierProvider); + + if (!signatureValid) { + return false; + } + + + org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory certificateFactory = new org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory(); + java.security.cert.Certificate certificate = certificateFactory.engineGenerateCertificate(new ByteArrayInputStream(x509CertificateHolder.getEncoded())); + // Note: this requires the BC provider to be loaded! + if (certificate == null || certificate.getPublicKey() == null) { + return false; + } + + // Verify the TIME/DATE of the certificate + X509Accessor.verifyDate(certificate); + + // if we get here, it means that our cert is LEGIT and VALID. + return true; + + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + /** + * Verifies the given x509 based signature against the OPTIONAL original public key. If not specified, then + * the public key from the signature is used. + *

+ * MUST have BouncyCastle provider loaded by the security manager! + *

+ * @return true if the signature was valid. + */ + public static boolean verifySignature(byte[] signatureBytes, DSAPublicKeyParameters optionalOriginalPublicKey) { + ASN1InputStream asn1InputStream = null; + try { + asn1InputStream = new ASN1InputStream(new ByteArrayInputStream(signatureBytes)); + ASN1Primitive signatureASN = asn1InputStream.readObject(); + ASN1Sequence seq = ASN1Sequence.getInstance(signatureASN); + ASN1TaggedObject tagged = (ASN1TaggedObject)seq.getObjectAt(1); + + // Extract certificates + SignedData newSignedData = SignedData.getInstance(tagged.getObject()); + + @SuppressWarnings("rawtypes") + Enumeration newSigOjects = newSignedData.getCertificates().getObjects(); + Object newSigElement = newSigOjects.nextElement(); + + if (newSigElement instanceof DERSequence) { + DERSequence newSigDERElement = (DERSequence) newSigElement; + InputStream newSigIn = new ByteArrayInputStream(newSigDERElement.getEncoded()); + + org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory certificateFactory = new org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory(); + java.security.cert.Certificate engineGenerateCert = certificateFactory.engineGenerateCertificate(newSigIn); + + PublicKey publicKey2 = engineGenerateCert.getPublicKey(); + + if (optionalOriginalPublicKey != null) { + // absolutely RETARDED that we have package private constructors .. but fortunately, we can get around that + DSAParameters parameters = optionalOriginalPublicKey.getParameters(); + BCDSAPublicKey origPublicKey = BCDSAPublicKeyAccessor.newInstance(optionalOriginalPublicKey.getY(), + new DSAParameterSpec(parameters.getP(), + parameters.getQ(), + parameters.getG())); + boolean equals = origPublicKey.equals(publicKey2); + if (!equals) { + return false; + } + publicKey2 = origPublicKey; + } + + engineGenerateCert.verify(publicKey2); + } + } catch (Throwable t) { + return false; + } finally { + if (asn1InputStream != null) { + try { + asn1InputStream.close(); + } catch (IOException e) { + logger.error("Error closing stream during DSA.", e); + } + } + } + + return true; + } + } + + public static class RSA { + static { + addProvider(); + } + +// public static class CertificateAuthority { +// public static X509Certificate generateCert(KeyFactory factory, Date startDate, Date expiryDate, +// String issuer, String subject, String friendlyName, +// RSAKeyParameters publicKey, RSAPrivateCrtKeyParameters privateKey) throws InvalidKeySpecException, IOException, InvalidKeyException, OperatorCreationException { +// +// return CryptoX509.RSA.generateCert(factory, startDate, expiryDate, new X500Name(issuer), new X500Name(subject), friendlyName, publicKey, privateKey, null); +// } +// +// public static X509Certificate generateCert(KeyFactory factory, Date startDate, Date expiryDate, +// X509Principal issuer, String subject, String friendlyName, +// RSAKeyParameters publicKey, RSAPrivateCrtKeyParameters privateKey) throws InvalidKeySpecException, InvalidKeyException, IOException, OperatorCreationException { +// +// return CryptoX509.RSA.generateCert(factory, startDate, expiryDate, X500Name.getInstance(issuer), new X500Name(subject), friendlyName, publicKey, privateKey, null); +// } +// } +// +// +// public static class IntermediateAuthority { +// public static X509Certificate generateCert(KeyFactory factory, Date startDate, Date expiryDate, +// String issuer, String subject, String friendlyName, +// RSAKeyParameters publicKey, RSAPrivateCrtKeyParameters privateKey, +// X509Certificate caCertificate) throws InvalidKeySpecException, IOException, InvalidKeyException, OperatorCreationException { +// +// return CryptoX509.RSA.generateCert(factory, startDate, expiryDate, new X500Name(issuer), new X500Name(subject), friendlyName, publicKey, privateKey, caCertificate); +// } +// +// public static X509Certificate generateCert(KeyFactory factory, Date startDate, Date expiryDate, +// X509Principal issuer, String subject, String friendlyName, +// RSAKeyParameters publicKey, RSAPrivateCrtKeyParameters privateKey, +// X509Certificate caCertificate) throws InvalidKeySpecException, InvalidKeyException, IOException, OperatorCreationException { +// +// return CryptoX509.RSA.generateCert(factory, startDate, expiryDate, X500Name.getInstance(issuer), new X500Name(subject), friendlyName, publicKey, privateKey, caCertificate); +// } +// } +// + public static class CertificateAuthrority { + public static X509Certificate generateCert(KeyFactory factory, Date startDate, Date endDate, + String subject, String friendlyName, + RSAKeyParameters publicKey, RSAPrivateCrtKeyParameters privateKey) { + + String signatureAlgorithm = "SHA1withRSA"; + + AlgorithmIdentifier sigAlgId = new DefaultSignatureAlgorithmIdentifierFinder().find(signatureAlgorithm); // specify it's RSA + AlgorithmIdentifier digAlgId = new DefaultDigestAlgorithmIdentifierFinder().find(sigAlgId); // specify SHA + + try { + // JCE format needed for the certificate - because getEncoded() is necessary... + PublicKey jcePublicKey = convertToJCE(factory, publicKey); +// PrivateKey jcePrivateKey = convertToJCE(factory, publicKey, privateKey); + + SubjectPublicKeyInfo subjectPublicKeyInfo = createSubjectPublicKey(jcePublicKey); + X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(new X500Name(subject), BigInteger.valueOf(System.currentTimeMillis()), + startDate, endDate, new X500Name(subject), + subjectPublicKeyInfo); + + // + // extensions + // + JcaX509ExtensionUtils jcaX509ExtensionUtils = new JcaX509ExtensionUtils(); // SHA1 + SubjectKeyIdentifier createSubjectKeyIdentifier = jcaX509ExtensionUtils.createSubjectKeyIdentifier(subjectPublicKeyInfo); + + certBuilder.addExtension(Extension.subjectKeyIdentifier, + false, + createSubjectKeyIdentifier); + + certBuilder.addExtension(Extension.basicConstraints, + true, + new BasicConstraints(1)); + + + ContentSigner hashSigner = new BcRSAContentSignerBuilder(sigAlgId, digAlgId).build(privateKey); + X509CertificateHolder certHolder = certBuilder.build(hashSigner); + X509CertificateObject certificate = new X509CertificateObject(Certificate.getInstance(certHolder.getEncoded())); + + certificate.verify(jcePublicKey); + + + PKCS12BagAttributeCarrier bagAttr = certificate; + + // + // this is actually optional - but if you want to have control + // over setting the friendly name this is the way to do it... + // + bagAttr.setBagAttribute(PKCSObjectIdentifiers.pkcs_9_at_friendlyName, + new DERBMPString(friendlyName)); + + return certificate; + } catch (Exception e) { + logger.error("Error generating certificate.", e); + return null; + } + } + } + + public static class SelfSigned { + public static X509Certificate generateCert(KeyFactory factory, Date startDate, Date endDate, + String subject, String friendlyName, + RSAKeyParameters publicKey, RSAPrivateCrtKeyParameters privateKey) { + + String signatureAlgorithm = "SHA1withRSA"; + + AlgorithmIdentifier sigAlgId = new DefaultSignatureAlgorithmIdentifierFinder().find(signatureAlgorithm); // specify it's RSA + AlgorithmIdentifier digAlgId = new DefaultDigestAlgorithmIdentifierFinder().find(sigAlgId); // specify SHA + + try { + // JCE format needed for the certificate - because getEncoded() is necessary... + PublicKey jcePublicKey = convertToJCE(factory, publicKey); +// PrivateKey jcePrivateKey = convertToJCE(factory, publicKey, privateKey); + + SubjectPublicKeyInfo subjectPublicKeyInfo = createSubjectPublicKey(jcePublicKey); + X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(new X500Name(subject), BigInteger.valueOf(System.currentTimeMillis()), + startDate, endDate, new X500Name(subject), + subjectPublicKeyInfo); + + // + // extensions + // + JcaX509ExtensionUtils jcaX509ExtensionUtils = new JcaX509ExtensionUtils(); // SHA1 + SubjectKeyIdentifier createSubjectKeyIdentifier = jcaX509ExtensionUtils.createSubjectKeyIdentifier(subjectPublicKeyInfo); + + certBuilder.addExtension(Extension.subjectKeyIdentifier, + false, + createSubjectKeyIdentifier); + + certBuilder.addExtension(Extension.basicConstraints, + true, + new BasicConstraints(false)); + + + ContentSigner hashSigner = new BcRSAContentSignerBuilder(sigAlgId, digAlgId).build(privateKey); + X509CertificateHolder certHolder = certBuilder.build(hashSigner); + X509CertificateObject certificate = new X509CertificateObject(Certificate.getInstance(certHolder.getEncoded())); + + certificate.verify(jcePublicKey); + + + PKCS12BagAttributeCarrier bagAttr = certificate; + + // + // this is actually optional - but if you want to have control + // over setting the friendly name this is the way to do it... + // + bagAttr.setBagAttribute(PKCSObjectIdentifiers.pkcs_9_at_friendlyName, + new DERBMPString(friendlyName)); + + return certificate; + } catch (Exception e) { + logger.error("Error generating certificate.", e); + return null; + } + } + } + + + /** + * Generate a cert that is signed by a CA cert. + */ + public static X509Certificate generateCert(KeyFactory factory, Date startDate, Date expiryDate, + X509Certificate issuerCert, String subject, String friendlyName, + RSAKeyParameters publicKey, RSAPrivateCrtKeyParameters signingCaKey) throws InvalidKeySpecException, InvalidKeyException, IOException, OperatorCreationException, CertificateException, NoSuchAlgorithmException, NoSuchProviderException, SignatureException { + + return CryptoX509.RSA.generateCert(factory, startDate, expiryDate, + X500Name.getInstance(PrincipalUtil.getSubjectX509Principal(issuerCert)), new X500Name(subject), friendlyName, + publicKey, + issuerCert, signingCaKey); + } + + + /** + * Generate a cert that is self signed. + */ + public static X509Certificate generateCert(KeyFactory factory, Date startDate, Date expiryDate, + String subject, String friendlyName, + RSAKeyParameters publicKey, RSAPrivateCrtKeyParameters privateKey) throws InvalidKeySpecException, InvalidKeyException, IOException, OperatorCreationException, CertificateException, NoSuchAlgorithmException, NoSuchProviderException, SignatureException { + + return CryptoX509.RSA.generateCert(factory, startDate, expiryDate, + new X500Name(subject), new X500Name(subject), friendlyName, + publicKey, null, privateKey); + } + + + + private static X509Certificate generateCert(KeyFactory factory, Date startDate, Date expiryDate, + X500Name issuer, X500Name subject, String friendlyName, + RSAKeyParameters certPublicKey, + X509Certificate signingCertificate, RSAPrivateCrtKeyParameters signingPrivateKey) throws InvalidKeySpecException, IOException, InvalidKeyException, OperatorCreationException, CertificateException, NoSuchAlgorithmException, NoSuchProviderException, SignatureException { + + + String signatureAlgorithm = "SHA1withRSA"; + + AlgorithmIdentifier sigAlgId = new DefaultSignatureAlgorithmIdentifierFinder().find(signatureAlgorithm); // specify it's RSA + AlgorithmIdentifier digAlgId = new DefaultDigestAlgorithmIdentifierFinder().find(sigAlgId); // specify SHA + + // JCE format needed for the certificate - because getEncoded() is necessary... + PublicKey jcePublicKey = convertToJCE(factory, certPublicKey); +// PrivateKey jcePrivateKey = convertToJCE(factory, publicKey, privateKey); + + + + + SubjectPublicKeyInfo subjectPublicKeyInfo = createSubjectPublicKey(jcePublicKey); + X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(issuer, BigInteger.valueOf(System.currentTimeMillis()), + startDate, expiryDate, subject, + subjectPublicKeyInfo); + + + + // + // extensions + // + JcaX509ExtensionUtils jcaX509ExtensionUtils = new JcaX509ExtensionUtils(); // SHA1 + SubjectKeyIdentifier createSubjectKeyIdentifier = jcaX509ExtensionUtils.createSubjectKeyIdentifier(subjectPublicKeyInfo); + + certBuilder.addExtension(Extension.subjectKeyIdentifier, + false, + createSubjectKeyIdentifier); + + if (signingCertificate != null) { + AuthorityKeyIdentifier createAuthorityKeyIdentifier = jcaX509ExtensionUtils.createAuthorityKeyIdentifier(signingCertificate.getPublicKey()); + certBuilder.addExtension(Extension.authorityKeyIdentifier, + false, + createAuthorityKeyIdentifier); +// new AuthorityKeyIdentifierStructure(signingCertificate)); + } + + certBuilder.addExtension(Extension.basicConstraints, + true, + new BasicConstraints(false)); + + + + ContentSigner signer = new BcRSAContentSignerBuilder(sigAlgId, digAlgId).build(signingPrivateKey); + X509CertificateHolder certHolder = certBuilder.build(signer); + X509CertificateObject certificate = new X509CertificateObject(Certificate.getInstance(certHolder.getEncoded())); + + if (signingCertificate != null) { + certificate.verify(signingCertificate.getPublicKey()); + } else { + certificate.verify(jcePublicKey); + } + + PKCS12BagAttributeCarrier bagAttr = certificate; + + // + // this is actually optional - but if you want to have control + // over setting the friendly name this is the way to do it... + // + bagAttr.setBagAttribute(PKCSObjectIdentifiers.pkcs_9_at_friendlyName, + new DERBMPString(friendlyName)); + + if (signingCertificate != null) { + bagAttr.setBagAttribute(PKCSObjectIdentifiers.pkcs_9_at_localKeyId, + subjectPublicKeyInfo); + } + + + return certificate; + + +// //// subject name table. +// //Hashtable attrs = new Hashtable(); +// //Vector order = new Vector(); +// // +// //attrs.put(BCStyle.C, "US"); +// //attrs.put(BCStyle.O, "Dorkbox"); +// //attrs.put(BCStyle.OU, "Dorkbox Certificate Authority"); +// //attrs.put(BCStyle.EmailAddress, "admin@dorkbox.com"); +// // +// //order.addElement(BCStyle.C); +// //order.addElement(BCStyle.O); +// //order.addElement(BCStyle.OU); +// //order.addElement(BCStyle.EmailAddress); +// // +// //X509Principal issuer = new X509Principal(order, attrs); +// // MASTER CERT +// +// //// signers name +// //String issuer = "C=US, O=dorkbox llc, OU=Dorkbox Certificate Authority"; +// // +// //// subjects name - the same as we are self signed. +// //String subject = "C=US, O=dorkbox llc, OU=Dorkbox Certificate Authority"; + } + + private static SubjectPublicKeyInfo createSubjectPublicKey(PublicKey jcePublicKey) throws IOException { + ASN1InputStream asn1InputStream = null; + try { + asn1InputStream = new ASN1InputStream(new ByteArrayInputStream(jcePublicKey.getEncoded())); + SubjectPublicKeyInfo subjectPublicKeyInfo1 = new SubjectPublicKeyInfo((ASN1Sequence) asn1InputStream.readObject()); + return subjectPublicKeyInfo1; + } finally { + if (asn1InputStream != null) { + asn1InputStream.close(); + } + } + } + + + public static PublicKey convertToJCE(RSAKeyParameters publicKey) throws NoSuchAlgorithmException, InvalidKeySpecException { + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + return convertToJCE(keyFactory, publicKey); + } + + public static PublicKey convertToJCE(KeyFactory keyFactory, RSAKeyParameters publicKey) throws InvalidKeySpecException { + return keyFactory.generatePublic(new RSAPublicKeySpec(publicKey.getModulus(), publicKey.getExponent())); + } + + public static RSAKeyParameters convertToBC(PublicKey publicKey) { + RSAPublicKey pubKey = RSAPublicKey.getInstance(publicKey); + return new RSAKeyParameters(false, pubKey.getModulus(), pubKey.getPublicExponent()); + } + + public static PrivateKey convertToJCE(RSAKeyParameters publicKey, RSAPrivateCrtKeyParameters privateKey) throws InvalidKeySpecException, NoSuchAlgorithmException { + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + return convertToJCE(keyFactory, publicKey, privateKey); + } + + public static PrivateKey convertToJCE(KeyFactory keyFactory, RSAKeyParameters publicKey, RSAPrivateCrtKeyParameters privateKey) throws InvalidKeySpecException { + return keyFactory.generatePrivate(new RSAPrivateCrtKeySpec(publicKey.getModulus(), publicKey.getExponent(), + privateKey.getExponent(), privateKey.getP(), privateKey.getQ(), + privateKey.getDP(), privateKey.getDQ(), privateKey.getQInv())); + } + + /** + * Creates a X509 certificate holder object.

+ * + * Look at BCStyle for a list of all valid X500 Names. + */ + public static X509CertificateHolder createCertHolder(Date startDate, Date expiryDate, + X500Name issuerName, X500Name subjectName, BigInteger serialNumber, + RSAPrivateCrtKeyParameters privateKey, RSAKeyParameters publicKey) { + + String signatureAlgorithm = "SHA256withRSA"; + + + AlgorithmIdentifier sigAlgId = new DefaultSignatureAlgorithmIdentifierFinder().find(signatureAlgorithm); + AlgorithmIdentifier digAlgId = new DefaultDigestAlgorithmIdentifierFinder().find(sigAlgId); + + + SubjectPublicKeyInfo subjectPublicKeyInfo = null; + + try { + // JCE format needed for the certificate - because getEncoded() is necessary... + PublicKey jcePublicKey = convertToJCE(publicKey); +// PrivateKey jcePrivateKey = convertToJCE(factory, publicKey, privateKey); + + subjectPublicKeyInfo = createSubjectPublicKey(jcePublicKey); + } catch (Exception e) { + logger.error("Unable to create RSA keyA.", e); + return null; + } + + + try { + X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(issuerName, serialNumber, + startDate, expiryDate, subjectName, + subjectPublicKeyInfo); + // + // extensions + // + JcaX509ExtensionUtils jcaX509ExtensionUtils = new JcaX509ExtensionUtils(); // SHA1 + SubjectKeyIdentifier createSubjectKeyIdentifier = jcaX509ExtensionUtils.createSubjectKeyIdentifier(subjectPublicKeyInfo); + + certBuilder.addExtension(Extension.subjectKeyIdentifier, + false, + createSubjectKeyIdentifier); + + certBuilder.addExtension(Extension.basicConstraints, + true, + new BasicConstraints(false)); + + + ContentSigner hashSigner = new BcRSAContentSignerBuilder(sigAlgId, digAlgId).build(privateKey); + X509CertificateHolder certHolder = certBuilder.build(hashSigner); + + return certHolder; + } catch (Exception e) { + logger.error("Error generating certificate.", e); + return null; + } + } + + + /** + * Verifies that the certificate is legitimate. + *

+ * MUST have BouncyCastle provider loaded by the security manager! + *

+ * @return true if it was a valid cert. + */ + public static final boolean validate(X509CertificateHolder x509CertificateHolder) { + try { + + // this is unique in that it verifies that the certificate is a LEGIT certificate, but not necessarily + // valid during this time period. + ContentVerifierProvider contentVerifierProvider = new BcRSAContentVerifierProviderBuilder(new DefaultDigestAlgorithmIdentifierFinder()).build(x509CertificateHolder); + + boolean signatureValid = x509CertificateHolder.isSignatureValid(contentVerifierProvider); + + if (!signatureValid) { + return false; + } + + org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory certificateFactory = new org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory(); + java.security.cert.Certificate certificate = certificateFactory.engineGenerateCertificate(new ByteArrayInputStream(x509CertificateHolder.getEncoded())); + // Note: this requires the BC provider to be loaded! + if (certificate == null || certificate.getPublicKey() == null) { + return false; + } + + // Verify the TIME/DATE of the certificate + X509Accessor.verifyDate(certificate); + + // if we get here, it means that our cert is LEGIT and VALID. + return true; + + } catch (Throwable t) { + logger.error("Error validating certificate.", t); + return false; + } + } + + /** + * Verifies the given x509 based signature against the OPTIONAL original public key. If not specified, then + * the public key from the signature is used. + *

+ * MUST have BouncyCastle provider loaded by the security manager! + *

+ * @return true if the signature was valid. + */ + public static boolean verifySignature(byte[] signatureBytes, RSAKeyParameters publicKey) { + + ASN1InputStream asn1InputStream = null; + try { + asn1InputStream = new ASN1InputStream(new ByteArrayInputStream(signatureBytes)); + ASN1Primitive signatureASN = asn1InputStream.readObject(); + ASN1Sequence seq = ASN1Sequence.getInstance(signatureASN); + ASN1TaggedObject tagged = (ASN1TaggedObject)seq.getObjectAt(1); + + // Extract certificates + SignedData newSignedData = SignedData.getInstance(tagged.getObject()); + + @SuppressWarnings("rawtypes") + Enumeration newSigOjects = newSignedData.getCertificates().getObjects(); + Object newSigElement = newSigOjects.nextElement(); + + if (newSigElement instanceof DERSequence) { + DERSequence newSigDERElement = (DERSequence) newSigElement; + InputStream newSigIn = new ByteArrayInputStream(newSigDERElement.getEncoded()); + + org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory certificateFactory = new org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory(); + java.security.cert.Certificate certificate = certificateFactory.engineGenerateCertificate(newSigIn); + + PublicKey publicKey2 = certificate.getPublicKey(); + + if (publicKey != null) { + BCRSAPublicKey origPublicKey = BCRSAPublicKeyAccessor.newInstance(publicKey); + boolean equals = origPublicKey.equals(publicKey2); + if (!equals) { + return false; + } + publicKey2 = origPublicKey; + } + + certificate.verify(publicKey2); + } + return true; + } catch (Throwable t) { + logger.error("Error validating certificate.", t); + return false; + } finally { + if (asn1InputStream != null) { + try { + asn1InputStream.close(); + } catch (IOException e) { + logger.error("Error closing stream during RSA.", e); + } + } + } + } + } + + public static class ECDSA { + static { + // make sure we only add it once (in case it's added elsewhere...) + Provider provider = Security.getProvider("BC"); + if (provider == null) { + Security.addProvider(new BouncyCastleProvider()); + } + } + + /** + * Creates a X509 certificate holder object. + */ + public static X509CertificateHolder createCertHolder(String digestName, + Date startDate, Date expiryDate, + X500Name issuerName, X500Name subjectName, + BigInteger serialNumber, + ECPrivateKeyParameters privateKey, + ECPublicKeyParameters publicKey) { + + String signatureAlgorithm = digestName + "withECDSA"; + + + // we WANT the ECparameterSpec to be null, so it's created from the public key + JCEECPublicKey pubKey = new JCEECPublicKey("EC", publicKey, (ECParameterSpec) null); + + AlgorithmIdentifier sigAlgId = new DefaultSignatureAlgorithmIdentifierFinder().find(signatureAlgorithm); + AlgorithmIdentifier digAlgId = new DefaultDigestAlgorithmIdentifierFinder().find(sigAlgId); + + SubjectPublicKeyInfo subjectPublicKeyInfo = null; + + try { + byte[] encoded = pubKey.getEncoded(); + ASN1Sequence seq = (ASN1Sequence)ASN1Primitive.fromByteArray(encoded); + subjectPublicKeyInfo = new SubjectPublicKeyInfo(seq); + } catch (IOException e) { + logger.error("Unable to perform DSA.", e); + return null; + } + + X509v3CertificateBuilder v3CertBuilder = new X509v3CertificateBuilder(issuerName, + serialNumber, startDate, expiryDate, + subjectName, subjectPublicKeyInfo); + + BcECDSAContentSignerBuilder contentSignerBuilder = new BcECDSAContentSignerBuilder(sigAlgId, digAlgId); + + ContentSigner build = null; + try { + build = contentSignerBuilder.build(privateKey); + } catch (OperatorCreationException e) { + logger.error("Error creating certificate.", e); + return null; + } + + X509CertificateHolder certHolder = v3CertBuilder.build(build); + return certHolder; + } + + /** + * Verifies that the certificate is legitimate. + *

+ * MUST have BouncyCastle provider loaded by the security manager! + *

+ * @return true if it was a valid cert. + */ + public static final boolean validate(X509CertificateHolder x509CertificateHolder) { + try { + + // this is unique in that it verifies that the certificate is a LEGIT certificate, but not necessarily + // valid during this time period. + ContentVerifierProvider contentVerifierProvider = new BcECDSAContentVerifierProviderBuilder( + new DefaultDigestAlgorithmIdentifierFinder()).build(x509CertificateHolder); + + boolean signatureValid = x509CertificateHolder.isSignatureValid(contentVerifierProvider); + + if (!signatureValid) { + return false; + } + + org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory certificateFactory = new org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory(); + java.security.cert.Certificate certificate = certificateFactory.engineGenerateCertificate(new ByteArrayInputStream(x509CertificateHolder.getEncoded())); + // Note: this requires the BC provider to be loaded! + if (certificate == null || certificate.getPublicKey() == null) { + return false; + } + + // Verify the TIME/DATE of the certificate + X509Accessor.verifyDate(certificate); + + // if we get here, it means that our cert is LEGIT and VALID. + return true; + } catch (Throwable t) { + logger.error("Error validating certificate.", t); + return false; + } + + } + + /** + * Verifies the given x509 based signature against the OPTIONAL original public key. If not specified, then + * the public key from the signature is used. + *

+ * MUST have BouncyCastle provider loaded by the security manager! + *

+ * @return true if the signature was valid. + */ + public static boolean verifySignature(byte[] signatureBytes, ECPublicKeyParameters optionalOriginalPublicKey) { + ASN1InputStream asn1InputStream = null; + try { + asn1InputStream = new ASN1InputStream(new ByteArrayInputStream(signatureBytes)); + ASN1Primitive signatureASN = asn1InputStream.readObject(); + ASN1Sequence seq = ASN1Sequence.getInstance(signatureASN); + ASN1TaggedObject tagged = (ASN1TaggedObject)seq.getObjectAt(1); + + // Extract certificates + SignedData newSignedData = SignedData.getInstance(tagged.getObject()); + + @SuppressWarnings("rawtypes") + Enumeration newSigOjects = newSignedData.getCertificates().getObjects(); + Object newSigElement = newSigOjects.nextElement(); + + if (newSigElement instanceof DERSequence) { + DERSequence newSigDERElement = (DERSequence) newSigElement; + InputStream newSigIn = new ByteArrayInputStream(newSigDERElement.getEncoded()); + + org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory certificateFactory = new org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory(); + java.security.cert.Certificate certificate = certificateFactory.engineGenerateCertificate(newSigIn); + + PublicKey publicKey2 = certificate.getPublicKey(); + + if (optionalOriginalPublicKey != null) { + ECDomainParameters parameters = optionalOriginalPublicKey.getParameters(); + ECParameterSpec ecParameterSpec = new ECParameterSpec(parameters.getCurve(), parameters.getG(), parameters.getN(), parameters.getH()); + BCECPublicKey origPublicKey = new BCECPublicKey("EC", optionalOriginalPublicKey, ecParameterSpec, null); + + boolean equals = origPublicKey.equals(publicKey2); + if (!equals) { + return false; + } + + publicKey2 = origPublicKey; + } + + certificate.verify(publicKey2); + } + } catch (Throwable t) { + logger.error("Error validating certificate.", t); + return false; + } finally { + if (asn1InputStream != null) { + try { + asn1InputStream.close(); + } catch (IOException e) { + logger.error("Error during ECDSA.", e); + } + } + } + + return true; + } + } + + + /** + * Creates a NEW signature block that contains the pkcs7 (minus content, which is the .SF file) + * signature of the .SF file. + * + * It contains the hash of the data, and the verification signature. + */ + public static byte[] createSignature(byte[] signatureSourceData, + X509CertificateHolder x509CertificateHolder, AsymmetricKeyParameter privateKey) { + + try { + CMSTypedData content = new CMSProcessableByteArray(signatureSourceData); + + ASN1ObjectIdentifier contentTypeOID = new ASN1ObjectIdentifier(content.getContentType().getId()); + ASN1EncodableVector digestAlgs = new ASN1EncodableVector(); + ASN1EncodableVector signerInfos = new ASN1EncodableVector(); + + AlgorithmIdentifier sigAlgId = x509CertificateHolder.getSignatureAlgorithm(); + AlgorithmIdentifier digAlgId = new DefaultDigestAlgorithmIdentifierFinder().find(sigAlgId); + + // use the bouncy-castle lightweight API to generate a hash of the signature source data (usually the signature file bytes) + BcContentSignerBuilder contentSignerBuilder; + AlgorithmIdentifier digEncryptionAlgorithm; + + + if (privateKey instanceof ECPrivateKeyParameters) { + contentSignerBuilder = new BcECDSAContentSignerBuilder(sigAlgId, digAlgId); + digEncryptionAlgorithm = new AlgorithmIdentifier(DSAUtil.dsaOids[0], null); // 1.2.840.10040.4.1 // DSA hashID + } else if (privateKey instanceof DSAPrivateKeyParameters) { + contentSignerBuilder = new BcDSAContentSignerBuilder(sigAlgId, digAlgId); + digEncryptionAlgorithm = new AlgorithmIdentifier(DSAUtil.dsaOids[0], null); // 1.2.840.10040.4.1 // DSA hashID + } else if (privateKey instanceof RSAPrivateCrtKeyParameters) { + contentSignerBuilder = new BcRSAContentSignerBuilder(sigAlgId, digAlgId); + digEncryptionAlgorithm = new AlgorithmIdentifier(RSAUtil.rsaOids[0], null); // 1.2.840.113549.1.1.1 // RSA hashID + } else { + throw new RuntimeException("Invalid signature type. Only ECDSA, DSA, RSA supported."); + } + + ContentSigner hashSigner = contentSignerBuilder.build(privateKey); + OutputStream outputStream = hashSigner.getOutputStream(); + outputStream.write(signatureSourceData, 0, signatureSourceData.length); + outputStream.flush(); + byte[] sigBytes = hashSigner.getSignature(); + + + SignerIdentifier sigId = new SignerIdentifier(new IssuerAndSerialNumber(x509CertificateHolder.toASN1Structure())); + + SignerInfo inf = new SignerInfo(sigId, + digAlgId, + (ASN1Set) null, + digEncryptionAlgorithm, + new DEROctetString(sigBytes), + (ASN1Set) null); + + digestAlgs.add(inf.getDigestAlgorithm()); + signerInfos.add(inf); + + + ASN1EncodableVector certs = new ASN1EncodableVector(); + certs.add(x509CertificateHolder.toASN1Structure()); + + + ContentInfo encInfo = new ContentInfo(contentTypeOID, null); + SignedData sd = new SignedData( + new DERSet(digestAlgs), + encInfo, + new BERSet(certs), + null, + new DERSet(signerInfos) + ); + + + ContentInfo contentInfo = new ContentInfo(CMSObjectIdentifiers.signedData, sd); + CMSSignedData cmsSignedData2 = new CMSSignedData(content, contentInfo); + byte[] signatureBlock = cmsSignedData2.getEncoded(); + + return signatureBlock; + } catch (Throwable t) { + logger.error("Error signing data.", t); + throw new RuntimeException("Error trying to sign data. " + t.getMessage()); + } + } + + /** + * Load a key and certificate from a Java KeyStore, and convert the key to a bouncy-castle key. + * + * Code is present but commented out, as it was a PITA to figure it out, as documentation is lacking.... + */ + public static void loadKeystore(String keystoreLocation, String alias, char[] passwd, char[] keypasswd) { +// FileInputStream fileIn = new FileInputStream(keystoreLocation); +// KeyStore keyStore = KeyStore.getInstance("JKS"); +// keyStore.load(fileIn, passwd); +// java.security.cert.Certificate[] chain = keyStore.getCertificateChain(alias); +// X509Certificate certChain[] = new X509Certificate[chain.length]; +// +// CertificateFactory cf = CertificateFactory.getInstance("X.509"); +// for (int count = 0; count < chain.length; count++) { +// ByteArrayInputStream certIn = new ByteArrayInputStream(chain[0].getEncoded()); +// X509Certificate cert = (X509Certificate) cf.generateCertificate(certIn); +// certChain[count] = cert; +// } +// +// Key key = keyStore.getKey(alias, keypasswd); +// KeyFactory keyFactory = KeyFactory.getInstance(key.getAlgorithm()); +// KeySpec keySpec; +// if (key instanceof DSAPrivateKey) { +// keySpec = keyFactory.getKeySpec(key, DSAPrivateKeySpec.class); +// } else { +// //keySpec = keyFactory.getKeySpec(key, RSAPrivateKeySpec.class); +// throw new RuntimeException("Only able to support DSA algorithm!"); +// } +// +// DSAPrivateKey privateKey = (DSAPrivateKey) keyFactory.generatePrivate(keySpec); + + // convert private key to bouncycastle specific +// DSAParams params = privateKey.getParams(); +// DSAPrivateKeyParameters wimpyPrivKey = new DSAPrivateKeyParameters(privateKey.getX(), new DSAParameters(params.getP(), params.getQ(), params.getG())); +// X509CertificateHolder x509CertificateHolder = new X509CertificateHolder(certChain[0].getEncoded()); +// + +// fileIn.close(); // close JKS + } +} diff --git a/Dorkbox-Util/src/dorkbox/util/crypto/bouncycastle/GCMBlockCipher_ByteBuf.java b/Dorkbox-Util/src/dorkbox/util/crypto/bouncycastle/GCMBlockCipher_ByteBuf.java new file mode 100644 index 0000000..8c1e454 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/crypto/bouncycastle/GCMBlockCipher_ByteBuf.java @@ -0,0 +1,420 @@ +package dorkbox.util.crypto.bouncycastle; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +import org.bouncycastle.crypto.BlockCipher; +import org.bouncycastle.crypto.CipherParameters; +import org.bouncycastle.crypto.DataLengthException; +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.bouncycastle.crypto.modes.AEADBlockCipher; +import org.bouncycastle.crypto.params.AEADParameters; +import org.bouncycastle.crypto.params.KeyParameter; +import org.bouncycastle.crypto.params.ParametersWithIV; +import org.bouncycastle.util.Arrays; +import org.bouncycastle.util.Pack; + +/** + * Implements the Galois/Counter mode (GCM) detailed in + * NIST Special Publication 800-38D. + */ +public class GCMBlockCipher_ByteBuf + implements AEADBlockCipher +{ + private static final int BLOCK_SIZE = 16; + private static final byte[] ZEROES = new byte[BLOCK_SIZE]; + + // not final due to a compiler bug + private BlockCipher cipher; + private Tables8kGCMMultiplier_ByteBuf multiplier; + + // These fields are set by init and not modified by processing + private boolean forEncryption; + private int macSize; + private byte[] nonce; + private byte[] A; + private byte[] H; + private ByteBuf initS; + private byte[] J0; + + // These fields are modified during processing + private byte[] bufBlock; + private byte[] macBlock; + private ByteBuf S; + private byte[] counter; + private int bufOff; + private long totalLength; + + public GCMBlockCipher_ByteBuf(BlockCipher c) + { + if (c.getBlockSize() != BLOCK_SIZE) + { + throw new IllegalArgumentException( + "cipher required with a block size of " + BLOCK_SIZE + "."); + } + + this.cipher = c; + this.multiplier = new Tables8kGCMMultiplier_ByteBuf(); + } + + @Override + public BlockCipher getUnderlyingCipher() + { + return this.cipher; + } + + @Override + public String getAlgorithmName() + { + return this.cipher.getAlgorithmName() + "/GCM"; + } + + @Override + public void init(boolean forEncryption, CipherParameters params) + throws IllegalArgumentException + { + this.forEncryption = forEncryption; + this.macBlock = null; + + KeyParameter keyParam; + + if (params instanceof AEADParameters) + { + AEADParameters param = (AEADParameters)params; + + this.nonce = param.getNonce(); + this.A = param.getAssociatedText(); + + int macSizeBits = param.getMacSize(); + if (macSizeBits < 96 || macSizeBits > 128 || macSizeBits % 8 != 0) + { + throw new IllegalArgumentException("Invalid value for MAC size: " + macSizeBits); + } + + this.macSize = macSizeBits / 8; + keyParam = param.getKey(); + } + else if (params instanceof ParametersWithIV) + { + ParametersWithIV param = (ParametersWithIV)params; + + this.nonce = param.getIV(); + this.A = null; + this.macSize = 16; + keyParam = (KeyParameter)param.getParameters(); + } + else + { + throw new IllegalArgumentException("invalid parameters passed to GCM"); + } + + int bufLength = forEncryption ? BLOCK_SIZE : BLOCK_SIZE + this.macSize; + this.bufBlock = new byte[bufLength]; + + if (this.nonce == null || this.nonce.length < 1) + { + throw new IllegalArgumentException("IV must be at least 1 byte"); + } + + if (this.A == null) + { + // Avoid lots of null checks + this.A = new byte[0]; + } + + // Cipher always used in forward mode + // if keyParam is null we're reusing the last key. + if (keyParam != null) + { + this.cipher.init(true, keyParam); + } + + // TODO This should be configurable by init parameters + // (but must be 16 if nonce length not 12) (BLOCK_SIZE?) +// this.tagLength = 16; + + this.H = new byte[BLOCK_SIZE]; + this.cipher.processBlock(ZEROES, 0, this.H, 0); + this.multiplier.init(this.H); + + this.initS = Unpooled.wrappedBuffer(gHASH(this.A)); + + if (this.nonce.length == 12) + { + this.J0 = new byte[16]; + System.arraycopy(this.nonce, 0, this.J0, 0, this.nonce.length); + this.J0[15] = 0x01; + } + else + { + this.J0 = gHASH(this.nonce); + byte[] X = new byte[16]; + packLength((long)this.nonce.length * 8, X, 8); + xor(this.J0, X); + this.multiplier.multiplyH(this.J0); + } + + this.S = this.initS.copy(); + this.counter = Arrays.clone(this.J0); + this.bufOff = 0; + this.totalLength = 0; + } + + @Override + public byte[] getMac() { + return Arrays.clone(this.macBlock); + } + + @Override + public int getOutputSize(int len) { + if (this.forEncryption) { + return len + this.bufOff + this.macSize; + } + + return len + this.bufOff - this.macSize; + } + + @Override + public int getUpdateOutputSize(int len) { + return (len + this.bufOff) / BLOCK_SIZE * BLOCK_SIZE; + } + + // MODIFIED + @Override + public int processByte(byte in, byte[] out, int outOff) + throws DataLengthException + { + // DO NOTHING + return 0; + } + + // MODIFIED + public int processBytes(ByteBuf in, ByteBuf out, int len) + throws DataLengthException + { + out.clear(); + + int didRead = 0; + int resultLen = 0; + + while (didRead < len) { + int buffOffRead = this.bufOff + len - didRead; + + if (buffOffRead >= this.bufBlock.length) { + int amtToRead = this.bufBlock.length - this.bufOff; + didRead += amtToRead; + + in.readBytes(this.bufBlock, this.bufOff, amtToRead); + + gCTRBlock(this.bufBlock, BLOCK_SIZE, out); + if (!this.forEncryption) { + System.arraycopy(this.bufBlock, BLOCK_SIZE, this.bufBlock, 0, this.macSize); + } + resultLen += BLOCK_SIZE; + this.bufOff = this.bufBlock.length - BLOCK_SIZE; + } else { + int read = len - didRead; + in.readBytes(this.bufBlock, this.bufOff, read); + this.bufOff += read; + break; + } + } + + return resultLen; + } + + public int doFinal(ByteBuf out) throws IllegalStateException, InvalidCipherTextException { + int extra = this.bufOff; + if (!this.forEncryption) { + if (extra < this.macSize) { + throw new InvalidCipherTextException("data too short"); + } + extra -= this.macSize; + } + + if (extra > 0) { + byte[] tmp = new byte[BLOCK_SIZE]; + System.arraycopy(this.bufBlock, 0, tmp, 0, extra); + gCTRBlock(tmp, extra, out); + } + + // Final gHASH + byte[] X = new byte[16]; + packLength((long) this.A.length * 8, X, 0); + packLength(this.totalLength * 8, X, 8); + + xor(this.S, X); + this.multiplier.multiplyH(this.S); + + // TODO Fix this if tagLength becomes configurable + // T = MSBt(GCTRk(J0,S)) + byte[] tag = new byte[BLOCK_SIZE]; + this.cipher.processBlock(this.J0, 0, tag, 0); + xor(tag, this.S); + + int resultLen = extra; + + // We place into macBlock our calculated value for T + this.macBlock = new byte[this.macSize]; + System.arraycopy(tag, 0, this.macBlock, 0, this.macSize); + + if (this.forEncryption) { + // Append T to the message + out.writeBytes(this.macBlock, 0, this.macSize); + resultLen += this.macSize; + } else { + // Retrieve the T value from the message and compare to calculated + // one + byte[] msgMac = new byte[this.macSize]; + System.arraycopy(this.bufBlock, extra, msgMac, 0, this.macSize); + if (!Arrays.constantTimeAreEqual(this.macBlock, msgMac)) { + throw new InvalidCipherTextException("mac check in GCM failed"); + } + } + + reset(false); + + return resultLen; + } + + @Override + public void reset() { + reset(true); + } + + private void reset(boolean clearMac) { + this.S = this.initS != null ? this.initS.copy() : Unpooled.buffer(); + this.counter = Arrays.clone(this.J0); + this.bufOff = 0; + this.totalLength = 0; + + if (this.bufBlock != null) { + Arrays.fill(this.bufBlock, (byte) 0); + } + + if (clearMac) { + this.macBlock = null; + } + + this.cipher.reset(); + } + + // MODIFIED + private void gCTRBlock(byte[] buf, int bufCount, ByteBuf out) { + // inc(counter); + for (int i = 15; i >= 12; --i) { + byte b = (byte) (this.counter[i] + 1 & 0xff); + this.counter[i] = b; + + if (b != 0) { + break; + } + } + + byte[] tmp = new byte[BLOCK_SIZE]; + this.cipher.processBlock(this.counter, 0, tmp, 0); + + byte[] hashBytes; + if (this.forEncryption) { + System.arraycopy(ZEROES, bufCount, tmp, bufCount, BLOCK_SIZE - bufCount); + hashBytes = tmp; + } else { + hashBytes = buf; + } + + for (int i = bufCount - 1; i >= 0; --i) { + tmp[i] ^= buf[i]; + } + out.writeBytes(tmp, 0, bufCount); + + // gHASHBlock(hashBytes); + xor(this.S, hashBytes); + this.multiplier.multiplyH(this.S); + + this.totalLength += bufCount; + } + + private byte[] gHASH(byte[] b) + { + byte[] Y = new byte[16]; + + for (int pos = 0; pos < b.length; pos += 16) + { + byte[] X = new byte[16]; + int num = Math.min(b.length - pos, 16); + System.arraycopy(b, pos, X, 0, num); + xor(Y, X); + this.multiplier.multiplyH(Y); + } + + return Y; + } + +// private void gHASHBlock(byte[] block) +// { +// xor(S, block); +// multiplier.multiplyH(S); +// } + +// private static void inc(byte[] block) +// { +// for (int i = 15; i >= 12; --i) +// { +// byte b = (byte)((block[i] + 1) & 0xff); +// block[i] = b; +// +// if (b != 0) +// { +// break; +// } +// } +// } + + private static void xor(ByteBuf block, byte[] val) { + for (int i = 15; i >= 0; --i) { + byte b = (byte) (block.getByte(i) ^ val[i]); + block.setByte(i, b); + } + } + + private static void xor(byte[] block, ByteBuf val) { + for (int i = 15; i >= 0; --i) { + block[i] ^= val.getByte(i); + } + } + + private static void xor(byte[] block, byte[] val) { + for (int i = 15; i >= 0; --i) { + block[i] ^= val[i]; + } + } + + private static void packLength(long count, byte[] bs, int off) { + Pack.intToBigEndian((int)(count >>> 32), bs, off); + Pack.intToBigEndian((int)count, bs, off + 4); + } + + // MODIFIED + @Override + public int processBytes(byte[] in, int inOff, int len, byte[] out, int outOff) throws DataLengthException { + // DO NOTHING + return 0; + } + + // MODIFIED + @Override + public int doFinal(byte[] out, int outOff) throws IllegalStateException, InvalidCipherTextException { + // DO NOTHING + return 0; + } + + @Override + public void processAADByte(byte arg0) { + + } + + @Override + public void processAADBytes(byte[] arg0, int arg1, int arg2) { + + } +} diff --git a/Dorkbox-Util/src/dorkbox/util/crypto/bouncycastle/GCMUtil_ByteBuf.java b/Dorkbox-Util/src/dorkbox/util/crypto/bouncycastle/GCMUtil_ByteBuf.java new file mode 100644 index 0000000..3ffb25e --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/crypto/bouncycastle/GCMUtil_ByteBuf.java @@ -0,0 +1,155 @@ +package dorkbox.util.crypto.bouncycastle; + +import org.bouncycastle.util.Arrays; +import org.bouncycastle.util.Pack; + +public abstract class GCMUtil_ByteBuf +{ + static byte[] oneAsBytes() + { + byte[] tmp = new byte[16]; + tmp[0] = (byte)0x80; + return tmp; + } + + static int[] oneAsInts() + { + int[] tmp = new int[4]; + tmp[0] = 0x80000000; + return tmp; + } + + static int[] asInts(byte[] bs) + { + int[] us = new int[4]; + us[0] = Pack.bigEndianToInt(bs, 0); + us[1] = Pack.bigEndianToInt(bs, 4); + us[2] = Pack.bigEndianToInt(bs, 8); + us[3] = Pack.bigEndianToInt(bs, 12); + return us; + } + + static void multiply(byte[] block, byte[] val) + { + byte[] tmp = Arrays.clone(block); + byte[] c = new byte[16]; + + for (int i = 0; i < 16; ++i) + { + byte bits = val[i]; + for (int j = 7; j >= 0; --j) + { + if ((bits & 1 << j) != 0) + { + xor(c, tmp); + } + + boolean lsb = (tmp[15] & 1) != 0; + shiftRight(tmp); + if (lsb) + { + // R = new byte[]{ 0xe1, ... }; +// GCMUtil.xor(v, R); + tmp[0] ^= (byte)0xe1; + } + } + } + + System.arraycopy(c, 0, block, 0, 16); + } + + // P is the value with only bit i=1 set + static void multiplyP(int[] x) + { + boolean lsb = (x[3] & 1) != 0; + shiftRight(x); + if (lsb) + { + // R = new int[]{ 0xe1000000, 0, 0, 0 }; +// xor(v, R); + x[0] ^= 0xe1000000; + } + } + + static void multiplyP8(int[] x) + { +// for (int i = 8; i != 0; --i) +// { +// multiplyP(x); +// } + + int lsw = x[3]; + shiftRightN(x, 8); + for (int i = 7; i >= 0; --i) + { + if ((lsw & 1 << i) != 0) + { + x[0] ^= 0xe1000000 >>> 7 - i; + } + } + } + + static void shiftRight(byte[] block) + { + int i = 0; + int bit = 0; + for (;;) + { + int b = block[i] & 0xff; + block[i] = (byte) (b >>> 1 | bit); + if (++i == 16) + { + break; + } + bit = (b & 1) << 7; + } + } + + static void shiftRight(int[] block) + { + int i = 0; + int bit = 0; + for (;;) + { + int b = block[i]; + block[i] = b >>> 1 | bit; + if (++i == 4) + { + break; + } + bit = b << 31; + } + } + + static void shiftRightN(int[] block, int n) + { + int i = 0; + int bits = 0; + for (;;) + { + int b = block[i]; + block[i] = b >>> n | bits; + if (++i == 4) + { + break; + } + bits = b << 32 - n; + } + } + + static void xor(byte[] block, byte[] val) + { + for (int i = 15; i >= 0; --i) + { + block[i] ^= val[i]; + } + } + + static void xor(int[] block, int[] val) + { + for (int i = 3; i >= 0; --i) + { + block[i] ^= val[i]; + } + } +} diff --git a/Dorkbox-Util/src/dorkbox/util/crypto/bouncycastle/Tables8kGCMMultiplier_ByteBuf.java b/Dorkbox-Util/src/dorkbox/util/crypto/bouncycastle/Tables8kGCMMultiplier_ByteBuf.java new file mode 100644 index 0000000..d04f395 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/crypto/bouncycastle/Tables8kGCMMultiplier_ByteBuf.java @@ -0,0 +1,139 @@ +package dorkbox.util.crypto.bouncycastle; + +import io.netty.buffer.ByteBuf; + +import org.bouncycastle.util.Pack; + +public class Tables8kGCMMultiplier_ByteBuf +{ + private final int[][][] M = new int[32][16][]; + + public void init(byte[] H) + { + this.M[0][0] = new int[4]; + this.M[1][0] = new int[4]; + this.M[1][8] = GCMUtil_ByteBuf.asInts(H); + + for (int j = 4; j >= 1; j >>= 1) + { + int[] tmp = new int[4]; + System.arraycopy(this.M[1][j + j], 0, tmp, 0, 4); + + GCMUtil_ByteBuf.multiplyP(tmp); + this.M[1][j] = tmp; + } + + { + int[] tmp = new int[4]; + System.arraycopy(this.M[1][1], 0, tmp, 0, 4); + + GCMUtil_ByteBuf.multiplyP(tmp); + this.M[0][8] = tmp; + } + + for (int j = 4; j >= 1; j >>= 1) + { + int[] tmp = new int[4]; + System.arraycopy(this.M[0][j + j], 0, tmp, 0, 4); + + GCMUtil_ByteBuf.multiplyP(tmp); + this.M[0][j] = tmp; + } + + int i = 0; + for (;;) + { + for (int j = 2; j < 16; j += j) + { + for (int k = 1; k < j; ++k) + { + int[] tmp = new int[4]; + System.arraycopy(this.M[i][j], 0, tmp, 0, 4); + + GCMUtil_ByteBuf.xor(tmp, this.M[i][k]); + this.M[i][j + k] = tmp; + } + } + + if (++i == 32) + { + return; + } + + if (i > 1) + { + this.M[i][0] = new int[4]; + for(int j = 8; j > 0; j >>= 1) + { + int[] tmp = new int[4]; + System.arraycopy(this.M[i - 2][j], 0, tmp, 0, 4); + + GCMUtil_ByteBuf.multiplyP8(tmp); + this.M[i][j] = tmp; + } + } + } + } + + public void multiplyH(byte[] x) + { +// assert x.Length == 16; + + int[] z = new int[4]; + for (int i = 15; i >= 0; --i) + { +// GCMUtil.xor(z, M[i + i][x[i] & 0x0f]); + int[] m = this.M[i + i][x[i] & 0x0f]; + z[0] ^= m[0]; + z[1] ^= m[1]; + z[2] ^= m[2]; + z[3] ^= m[3]; +// GCMUtil.xor(z, M[i + i + 1][(x[i] & 0xf0) >>> 4]); + m = this.M[i + i + 1][(x[i] & 0xf0) >>> 4]; + z[0] ^= m[0]; + z[1] ^= m[1]; + z[2] ^= m[2]; + z[3] ^= m[3]; + } + + Pack.intToBigEndian(z, x, 0); + } + + public void multiplyH(ByteBuf x) + { +// assert x.Length == 16; + + int[] z = new int[4]; + for (int i = 15; i >= 0; --i) + { +// GCMUtil.xor(z, M[i + i][x[i] & 0x0f]); + int[] m = this.M[i + i][x.getByte(i) & 0x0f]; + z[0] ^= m[0]; + z[1] ^= m[1]; + z[2] ^= m[2]; + z[3] ^= m[3]; +// GCMUtil.xor(z, M[i + i + 1][(x[i] & 0xf0) >>> 4]); + m = this.M[i + i + 1][(x.getByte(i) & 0xf0) >>> 4]; + z[0] ^= m[0]; + z[1] ^= m[1]; + z[2] ^= m[2]; + z[3] ^= m[3]; + } + + intToBigEndian(z, x, 0); + } + + public static void intToBigEndian(int[] ns, ByteBuf bs, int off) { + for (int i = 0; i < ns.length; ++i) { + intToBigEndian(ns[i], bs, off); + off += 4; + } + } + + public static void intToBigEndian(int n, ByteBuf bs, int off) { + bs.setByte(off, (byte) (n >>> 24)); + bs.setByte(++off, (byte) (n >>> 16)); + bs.setByte(++off, (byte) (n >>> 8)); + bs.setByte(++off, (byte) n); + } +} diff --git a/Dorkbox-Util/src/dorkbox/util/crypto/serialization/EccPrivateKeySerializer.java b/Dorkbox-Util/src/dorkbox/util/crypto/serialization/EccPrivateKeySerializer.java new file mode 100644 index 0000000..0a037ab --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/crypto/serialization/EccPrivateKeySerializer.java @@ -0,0 +1,241 @@ +package dorkbox.util.crypto.serialization; + + +import java.math.BigInteger; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.crypto.ec.CustomNamedCurves; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.math.ec.ECAccessor; +import org.bouncycastle.math.ec.ECCurve; +import org.bouncycastle.math.ec.ECPoint; + +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.Serializer; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; + +/** + * Only public keys are ever sent across the wire. + */ +public class EccPrivateKeySerializer extends Serializer { + + @Override + public void write(Kryo kryo, Output output, ECPrivateKeyParameters key) { + write(output, key); + } + + public static void write(Output output, ECPrivateKeyParameters key) { + byte[] bytes; + int length; + + ECDomainParameters parameters = key.getParameters(); + ECCurve curve = parameters.getCurve(); + + EccPrivateKeySerializer.serializeCurve(output, curve); + + ///////////// + BigInteger n = parameters.getN(); + ECPoint g = parameters.getG(); + + + ///////////// + bytes = n.toByteArray(); + length = bytes.length; + output.writeInt(length, true); + output.writeBytes(bytes, 0, length); + + + serializeECPoint(g, output); + + ///////////// + bytes = key.getD().toByteArray(); + length = bytes.length; + output.writeInt(length, true); + output.writeBytes(bytes, 0, length); + } + + @SuppressWarnings("rawtypes") + @Override + public ECPrivateKeyParameters read(Kryo kryo, Input input, Class type) { + return read(input); + } + + public static ECPrivateKeyParameters read(Input input) { + byte[] bytes; + int length; + + ECCurve curve = EccPrivateKeySerializer.deserializeCurve(input); + + // N + ///////////// + length = input.readInt(true); + bytes = new byte[length]; + input.readBytes(bytes, 0, length); + BigInteger n = new BigInteger(bytes); + + + // G + ///////////// + length = input.readInt(true); + bytes = new byte[length]; + input.readBytes(bytes, 0, length); + ECPoint g = curve.decodePoint(bytes); + + + // D + ///////////// + length = input.readInt(true); + bytes = new byte[length]; + input.readBytes(bytes, 0, length); + BigInteger D = new BigInteger(bytes); + + + ECDomainParameters ecDomainParameters = new ECDomainParameters(curve, g, n); + + return new ECPrivateKeyParameters(D, ecDomainParameters); + } + + static void serializeCurve(Output output, ECCurve curve) { + byte[] bytes; + int length; + // save out if it's a NAMED curve, or a UN-NAMED curve. If it is named, we can do less work. + String curveName = curve.getClass().getSimpleName(); + if (curveName.endsWith("Curve")) { + String cleanedName = curveName.substring(0, curveName.indexOf("Curve")); + + curveName = null; + if (!cleanedName.isEmpty()) { + ASN1ObjectIdentifier oid = CustomNamedCurves.getOID(cleanedName); + if (oid != null) { + // we use the OID (instead of serializing the entire curve) + output.writeBoolean(true); + curveName = oid.getId(); + output.writeString(curveName); + } + } + } + + // we have to serialize the ENTIRE curve. + if (curveName == null) { + // save out the curve info + BigInteger a = curve.getA().toBigInteger(); + BigInteger b = curve.getB().toBigInteger(); + BigInteger order = curve.getOrder(); + BigInteger cofactor = curve.getCofactor(); + BigInteger q = curve.getField().getCharacteristic(); + + ///////////// + bytes = a.toByteArray(); + length = bytes.length; + output.writeInt(length, true); + output.writeBytes(bytes, 0, length); + + ///////////// + bytes = b.toByteArray(); + length = bytes.length; + output.writeInt(length, true); + output.writeBytes(bytes, 0, length); + + ///////////// + bytes = order.toByteArray(); + length = bytes.length; + output.writeInt(length, true); + output.writeBytes(bytes, 0, length); + + + ///////////// + bytes = cofactor.toByteArray(); + length = bytes.length; + output.writeInt(length, true); + output.writeBytes(bytes, 0, length); + + + ///////////// + bytes = q.toByteArray(); + length = bytes.length; + output.writeInt(length, true); + output.writeBytes(bytes, 0, length); + + + // coordinate system + int coordinateSystem = curve.getCoordinateSystem(); + output.writeInt(coordinateSystem, true); + } + } + + static ECCurve deserializeCurve(Input input) { + byte[] bytes; + int length; + + ECCurve curve; + boolean usesOid = input.readBoolean(); + + // this means we just lookup the curve via the OID + if (usesOid) { + String oid = input.readString(); + X9ECParameters x9Curve = CustomNamedCurves.getByOID(new ASN1ObjectIdentifier(oid)); + curve = x9Curve.getCurve(); + } + // we have to read in the entire curve information. + else { + ///////////// + length = input.readInt(true); + bytes = new byte[length]; + input.readBytes(bytes, 0, length); + BigInteger a = new BigInteger(bytes); + + ///////////// + length = input.readInt(true); + bytes = new byte[length]; + input.readBytes(bytes, 0, length); + BigInteger b = new BigInteger(bytes); + + ///////////// + length = input.readInt(true); + bytes = new byte[length]; + input.readBytes(bytes, 0, length); + BigInteger order = new BigInteger(bytes); + + ///////////// + length = input.readInt(true); + bytes = new byte[length]; + input.readBytes(bytes, 0, length); + BigInteger cofactor = new BigInteger(bytes); + + ///////////// + length = input.readInt(true); + bytes = new byte[length]; + input.readBytes(bytes, 0, length); + BigInteger q = new BigInteger(bytes); + + + // coord system + int coordinateSystem = input.readInt(true); + + curve = new ECCurve.Fp(q, a, b, order, cofactor); + ECAccessor.setCoordSystem(curve, coordinateSystem); + } + return curve; + } + + static void serializeECPoint(ECPoint point, Output output) { + if (point.isInfinity()) { + return; + } + + ECPoint normed = point.normalize(); + + byte[] X = normed.getXCoord().getEncoded(); + byte[] Y = normed.getYCoord().getEncoded(); + + int length = 1 + X.length + Y.length; + output.writeInt(length, true); + + output.write(0x04); + output.write(X); + output.write(Y); + } +} diff --git a/Dorkbox-Util/src/dorkbox/util/crypto/serialization/EccPublicKeySerializer.java b/Dorkbox-Util/src/dorkbox/util/crypto/serialization/EccPublicKeySerializer.java new file mode 100644 index 0000000..0825610 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/crypto/serialization/EccPublicKeySerializer.java @@ -0,0 +1,94 @@ +package dorkbox.util.crypto.serialization; + + +import java.math.BigInteger; + +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.math.ec.ECCurve; +import org.bouncycastle.math.ec.ECPoint; + +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.Serializer; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; + +/** + * Only public keys are ever sent across the wire. + */ +public class EccPublicKeySerializer extends Serializer { + + @Override + public void write(Kryo kryo, Output output, ECPublicKeyParameters key) { + write(output, key); + } + + public static void write(Output output, ECPublicKeyParameters key) { + byte[] bytes; + int length; + + ECDomainParameters parameters = key.getParameters(); + ECCurve curve = parameters.getCurve(); + + EccPrivateKeySerializer.serializeCurve(output, curve); + + ///////////// + BigInteger n = parameters.getN(); + ECPoint g = parameters.getG(); + + + ///////////// + bytes = n.toByteArray(); + length = bytes.length; + output.writeInt(length, true); + output.writeBytes(bytes, 0, length); + + + EccPrivateKeySerializer.serializeECPoint(g, output); + EccPrivateKeySerializer.serializeECPoint(key.getQ(), output); + } + + @SuppressWarnings("rawtypes") + @Override + public ECPublicKeyParameters read(Kryo kryo, Input input, Class type) { + ECPublicKeyParameters ecPublicKeyParameters = read(input); + return ecPublicKeyParameters; + } + + public static ECPublicKeyParameters read(Input input) { + byte[] bytes; + int length; + + ECCurve curve = EccPrivateKeySerializer.deserializeCurve(input); + + + // N + ///////////// + length = input.readInt(true); + bytes = new byte[length]; + input.readBytes(bytes, 0, length); + BigInteger n = new BigInteger(bytes); + + + // G + ///////////// + length = input.readInt(true); + bytes = new byte[length]; + input.readBytes(bytes, 0, length); + ECPoint g = curve.decodePoint(bytes); + + + ECDomainParameters ecDomainParameters = new ECDomainParameters(curve, g, n); + + + // Q + ///////////// + length = input.readInt(true); + bytes = new byte[length]; + input.readBytes(bytes, 0, length); + ECPoint Q = curve.decodePoint(bytes); + + ECPublicKeyParameters ecPublicKeyParameters = new ECPublicKeyParameters(Q, ecDomainParameters); + return ecPublicKeyParameters; + } +} diff --git a/Dorkbox-Util/src/dorkbox/util/crypto/serialization/IesParametersSerializer.java b/Dorkbox-Util/src/dorkbox/util/crypto/serialization/IesParametersSerializer.java new file mode 100644 index 0000000..33e2546 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/crypto/serialization/IesParametersSerializer.java @@ -0,0 +1,59 @@ +package dorkbox.util.crypto.serialization; + +import org.bouncycastle.crypto.params.IESParameters; + +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.Serializer; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; + +/** + * Only public keys are ever sent across the wire. + */ +public class IesParametersSerializer extends Serializer { + + @Override + public void write(Kryo kryo, Output output, IESParameters key) { + byte[] bytes; + int length; + + /////////// + bytes = key.getDerivationV(); + length = bytes.length; + + output.writeInt(length, true); + output.writeBytes(bytes, 0, length); + + /////////// + bytes = key.getEncodingV(); + length = bytes.length; + + output.writeInt(length, true); + output.writeBytes(bytes, 0, length); + + /////////// + output.writeInt(key.getMacKeySize(), true); + } + + @SuppressWarnings("rawtypes") + @Override + public IESParameters read (Kryo kryo, Input input, Class type) { + + int length; + + ///////////// + length = input.readInt(true); + byte[] derivation = new byte[length]; + input.readBytes(derivation, 0, length); + + ///////////// + length = input.readInt(true); + byte[] encoding = new byte[length]; + input.readBytes(encoding, 0, length); + + ///////////// + int macKeySize = input.readInt(true); + + return new IESParameters(derivation, encoding, macKeySize); + } +} diff --git a/Dorkbox-Util/src/dorkbox/util/crypto/serialization/IesWithCipherParametersSerializer.java b/Dorkbox-Util/src/dorkbox/util/crypto/serialization/IesWithCipherParametersSerializer.java new file mode 100644 index 0000000..1a5a379 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/crypto/serialization/IesWithCipherParametersSerializer.java @@ -0,0 +1,66 @@ +package dorkbox.util.crypto.serialization; + +import org.bouncycastle.crypto.params.IESWithCipherParameters; + +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.Serializer; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; + +/** + * Only public keys are ever sent across the wire. + */ +public class IesWithCipherParametersSerializer extends Serializer { + + @Override + public void write(Kryo kryo, Output output, IESWithCipherParameters key) { + byte[] bytes; + int length; + + /////////// + bytes = key.getDerivationV(); + length = bytes.length; + + output.writeInt(length, true); + output.writeBytes(bytes, 0, length); + + /////////// + bytes = key.getEncodingV(); + length = bytes.length; + + output.writeInt(length, true); + output.writeBytes(bytes, 0, length); + + /////////// + output.writeInt(key.getMacKeySize(), true); + + + /////////// + output.writeInt(key.getCipherKeySize(), true); + } + + @SuppressWarnings("rawtypes") + @Override + public IESWithCipherParameters read (Kryo kryo, Input input, Class type) { + + int length; + + ///////////// + length = input.readInt(true); + byte[] derivation = new byte[length]; + input.readBytes(derivation, 0, length); + + ///////////// + length = input.readInt(true); + byte[] encoding = new byte[length]; + input.readBytes(encoding, 0, length); + + ///////////// + int macKeySize = input.readInt(true); + + ///////////// + int cipherKeySize = input.readInt(true); + + return new IESWithCipherParameters(derivation, encoding, macKeySize, cipherKeySize); + } +} diff --git a/Dorkbox-Util/src/dorkbox/util/crypto/serialization/RsaPrivateKeySerializer.java b/Dorkbox-Util/src/dorkbox/util/crypto/serialization/RsaPrivateKeySerializer.java new file mode 100644 index 0000000..556d32e --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/crypto/serialization/RsaPrivateKeySerializer.java @@ -0,0 +1,142 @@ +package dorkbox.util.crypto.serialization; + +import java.math.BigInteger; + +import org.bouncycastle.crypto.params.RSAPrivateCrtKeyParameters; + +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.Serializer; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; + +/** + * Only public keys are ever sent across the wire. + */ +public class RsaPrivateKeySerializer extends Serializer { + + @Override + public void write(Kryo kryo, Output output, RSAPrivateCrtKeyParameters key) { + byte[] bytes; + int length; + + ///////////// + bytes = key.getDP().toByteArray(); + length = bytes.length; + + output.writeInt(length, true); + output.writeBytes(bytes, 0, length); + + ///////////// + bytes = key.getDQ().toByteArray(); + length = bytes.length; + + output.writeInt(length, true); + output.writeBytes(bytes, 0, length); + + + ///////////// + bytes = key.getExponent().toByteArray(); + length = bytes.length; + + output.writeInt(length, true); + output.writeBytes(bytes, 0, length); + + + ///////////// + bytes = key.getModulus().toByteArray(); + length = bytes.length; + + output.writeInt(length, true); + output.writeBytes(bytes, 0, length); + + + ///////////// + bytes = key.getP().toByteArray(); + length = bytes.length; + + output.writeInt(length, true); + output.writeBytes(bytes, 0, length); + + + ///////////// + bytes = key.getPublicExponent().toByteArray(); + length = bytes.length; + + output.writeInt(length, true); + output.writeBytes(bytes, 0, length); + + + ///////////// + bytes = key.getQ().toByteArray(); + length = bytes.length; + + output.writeInt(length, true); + output.writeBytes(bytes, 0, length); + + + ///////////// + bytes = key.getQInv().toByteArray(); + length = bytes.length; + + output.writeInt(length, true); + output.writeBytes(bytes, 0, length); + } + + @SuppressWarnings("rawtypes") + @Override + public RSAPrivateCrtKeyParameters read (Kryo kryo, Input input, Class type) { + + byte[] bytes; + int length; + + ///////////// + length = input.readInt(true); + bytes = new byte[length]; + input.readBytes(bytes, 0, length); + BigInteger DP = new BigInteger(bytes); + + ///////////// + length = input.readInt(true); + bytes = new byte[length]; + input.readBytes(bytes, 0, length); + BigInteger DQ = new BigInteger(bytes); + + ///////////// + length = input.readInt(true); + bytes = new byte[length]; + input.readBytes(bytes, 0, length); + BigInteger exponent = new BigInteger(bytes); + + ///////////// + length = input.readInt(true); + bytes = new byte[length]; + input.readBytes(bytes, 0, length); + BigInteger modulus = new BigInteger(bytes); + + ///////////// + length = input.readInt(true); + bytes = new byte[length]; + input.readBytes(bytes, 0, length); + BigInteger P = new BigInteger(bytes); + + ///////////// + length = input.readInt(true); + bytes = new byte[length]; + input.readBytes(bytes, 0, length); + BigInteger publicExponent = new BigInteger(bytes); + + ///////////// + length = input.readInt(true); + bytes = new byte[length]; + input.readBytes(bytes, 0, length); + BigInteger q = new BigInteger(bytes); + + ///////////// + length = input.readInt(true); + bytes = new byte[length]; + input.readBytes(bytes, 0, length); + BigInteger qInv = new BigInteger(bytes); + + return new RSAPrivateCrtKeyParameters(modulus, publicExponent, exponent, P, q, DP, DQ, qInv); + } +} diff --git a/Dorkbox-Util/src/dorkbox/util/crypto/serialization/RsaPublicKeySerializer.java b/Dorkbox-Util/src/dorkbox/util/crypto/serialization/RsaPublicKeySerializer.java new file mode 100644 index 0000000..c042816 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/crypto/serialization/RsaPublicKeySerializer.java @@ -0,0 +1,58 @@ +package dorkbox.util.crypto.serialization; + +import java.math.BigInteger; + +import org.bouncycastle.crypto.params.RSAKeyParameters; + +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.Serializer; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; + +/** + * Only public keys are ever sent across the wire. + */ +public class RsaPublicKeySerializer extends Serializer { + + @Override + public void write(Kryo kryo, Output output, RSAKeyParameters key) { + byte[] bytes; + int length; + + /////////// + bytes = key.getModulus().toByteArray(); + length = bytes.length; + + output.writeInt(length, true); + output.writeBytes(bytes, 0, length); + + ///////////// + bytes = key.getExponent().toByteArray(); + length = bytes.length; + + output.writeInt(length, true); + output.writeBytes(bytes, 0, length); + } + + @SuppressWarnings("rawtypes") + @Override + public RSAKeyParameters read (Kryo kryo, Input input, Class type) { + + byte[] bytes; + int length; + + ///////////// + length = input.readInt(true); + bytes = new byte[length]; + input.readBytes(bytes, 0, length); + BigInteger modulus = new BigInteger(bytes); + + ///////////// + length = input.readInt(true); + bytes = new byte[length]; + input.readBytes(bytes, 0, length); + BigInteger exponent = new BigInteger(bytes); + + return new RSAKeyParameters(false, modulus, exponent); + } +} diff --git a/Dorkbox-Util/src/dorkbox/util/crypto/signers/BcECDSAContentSignerBuilder.java b/Dorkbox-Util/src/dorkbox/util/crypto/signers/BcECDSAContentSignerBuilder.java new file mode 100644 index 0000000..bb51756 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/crypto/signers/BcECDSAContentSignerBuilder.java @@ -0,0 +1,24 @@ +package dorkbox.util.crypto.signers; + +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.Signer; +import org.bouncycastle.crypto.signers.DSADigestSigner; +import org.bouncycastle.crypto.signers.ECDSASigner; +import org.bouncycastle.jcajce.provider.util.DigestFactory; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.bc.BcContentSignerBuilder; + +public class BcECDSAContentSignerBuilder extends BcContentSignerBuilder { + + public BcECDSAContentSignerBuilder(AlgorithmIdentifier sigAlgId, AlgorithmIdentifier digAlgId) { + super(sigAlgId, digAlgId); + } + + @Override + protected Signer createSigner(AlgorithmIdentifier sigAlgId, AlgorithmIdentifier digAlgId) throws OperatorCreationException { + Digest digest = DigestFactory.getDigest(digAlgId.getAlgorithm().getId()); // SHA1, SHA512, etc + + return new DSADigestSigner(new ECDSASigner(), digest); + } +} diff --git a/Dorkbox-Util/src/dorkbox/util/crypto/signers/BcECDSAContentVerifierProviderBuilder.java b/Dorkbox-Util/src/dorkbox/util/crypto/signers/BcECDSAContentVerifierProviderBuilder.java new file mode 100644 index 0000000..03cb51c --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/crypto/signers/BcECDSAContentVerifierProviderBuilder.java @@ -0,0 +1,36 @@ +package dorkbox.util.crypto.signers; + +import java.io.IOException; + +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.Signer; +import org.bouncycastle.crypto.params.AsymmetricKeyParameter; +import org.bouncycastle.crypto.signers.DSADigestSigner; +import org.bouncycastle.crypto.signers.ECDSASigner; +import org.bouncycastle.crypto.util.PublicKeyFactory; +import org.bouncycastle.jcajce.provider.util.DigestFactory; +import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder; +import org.bouncycastle.operator.DigestAlgorithmIdentifierFinder; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.bc.BcContentVerifierProviderBuilder; + +public class BcECDSAContentVerifierProviderBuilder extends BcContentVerifierProviderBuilder { + + public BcECDSAContentVerifierProviderBuilder(DigestAlgorithmIdentifierFinder digestAlgorithmFinder) { + } + + @Override + protected Signer createSigner(AlgorithmIdentifier sigAlgId) throws OperatorCreationException { + AlgorithmIdentifier digAlgId = new DefaultDigestAlgorithmIdentifierFinder().find(sigAlgId); + + Digest digest = DigestFactory.getDigest(digAlgId.getAlgorithm().getId()); // 1.2.23.4.5.6, etc + return new DSADigestSigner(new ECDSASigner(), digest); + } + + @Override + protected AsymmetricKeyParameter extractKeyParameters(SubjectPublicKeyInfo publicKeyInfo) throws IOException { + return PublicKeyFactory.createKey(publicKeyInfo); + } +} \ No newline at end of file diff --git a/Dorkbox-Util/src/dorkbox/util/gwt/GwtSymbolMapParser.java b/Dorkbox-Util/src/dorkbox/util/gwt/GwtSymbolMapParser.java new file mode 100644 index 0000000..68e9c69 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/gwt/GwtSymbolMapParser.java @@ -0,0 +1,118 @@ +package dorkbox.util.gwt; + +import io.netty.util.CharsetUtil; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.HashMap; +import java.util.Map; + +public class GwtSymbolMapParser { + + private final Map symbolMap; + + public GwtSymbolMapParser() { + symbolMap = new HashMap(); + } + + /** + * Efficiently parses the inputstream for symbolmap information. + *

+ * Automatically closes the input stream when finished. + */ + public void parse(InputStream inputStream) { + if (inputStream == null) { + return; + } + + InputStreamReader in = new InputStreamReader(inputStream, CharsetUtil.UTF_8); + + // 1024 is the longest the line will get. We start there, but StringBuilder will let us grow. + StringBuilder builder = new StringBuilder(1024); + + int charRead = '\r'; + char CHAR = (char) charRead; + try { + while ((charRead = in.read()) != -1) { + CHAR = (char) charRead; + + if (CHAR != '\r' && CHAR != '\n') { + builder.append(CHAR); + } else { + processLine(builder.toString()); + // new line! + builder.delete(0, builder.capacity()); + } + } + } catch (IOException e) { + + } finally { + try { + in.close(); + } catch (IOException e) { + } + } + } + + public Map getSymbolMap() { + return symbolMap; + } + + public void processLine(String line) { + if (line.charAt(0) == '#') { + return; + } + + String[] symbolInfo = line.split(","); + + // There are TWO versions of this file! + // 1) the ORIGINAL version (as created by the GWT compiler) + // 2) the SHRUNK version (as created by the build scripts) + + // version 1: + // # jsName, jsniIdent, className, memberName, sourceUri, sourceLine, fragmentNumber + + // version 2: + // jsName, className + + if (symbolInfo.length > 2) { + // version 1 + String jsName = symbolInfo[0]; +// String jsniIdent = symbolInfo[1]; + String className = symbolInfo[2]; + String memberName = symbolInfo[3]; +// String sourceUri = symbolInfo[4]; +// String sourceLine = symbolInfo[5]; +// String fragmentNumber = symbolInfo[6]; +// +// // The path relative to the source server. We assume it is just the +// // class path base. +// String sourcePath = className.replace('.', '/'); +// int lastSlashIndex = sourcePath.lastIndexOf("/") + 1; +// String sourcePathBase = sourcePath.substring(0, lastSlashIndex); +// +// // The sourceUri contains the actual file name. +// String sourceFileName = fileName.substring(fileName.lastIndexOf('/') + 1, fileName.length()); +// +// String sourceSymbolName = className + "::" + memberName; +// +// // simple symbol "holder" class +// GwtSymbol sourceSymbol = new GwtSymbol(sourcePathBase + sourceFileName, +// Integer.parseInt(sourceLine), +// sourceSymbolName, +// fileName); + + // only register class definitions. + // also, ignore if the source/dest name are the same, since that doesn't do any good for obfuscated names anyways. + if (memberName.isEmpty() && !jsName.equals(className)) { +// System.err.println(jsName + " : " + memberName + " : " + className); + symbolMap.put(jsName, className); + } + } else { + // version 2 + // The list has already been pruned, so always put everything into the symbol map + symbolMap.put(symbolInfo[0], symbolInfo[1]); + } + } +} diff --git a/Dorkbox-Util/src/dorkbox/util/process/JavaProcessBuilder.java b/Dorkbox-Util/src/dorkbox/util/process/JavaProcessBuilder.java new file mode 100644 index 0000000..8a1cd9f --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/process/JavaProcessBuilder.java @@ -0,0 +1,244 @@ +package dorkbox.util.process; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.List; + +import dorkbox.util.OS; + +/** + * This will FORK the java process initially used to start the currently running JVM. Changing the java executable will change this behaviors + */ +public class JavaProcessBuilder extends ShellProcessBuilder { + + // this is NOT related to JAVA_HOME, but is instead the location of the JRE that was used to launch java initially. + private String javaLocation = System.getProperty("java.home") + File.separator + "bin" + File.separator + "java"; + private String mainClass; + private int startingHeapSizeInMegabytes = 40; + private int maximumHeapSizeInMegabytes = 128; + + private List jvmOptions = new ArrayList(); + private List classpathEntries = new ArrayList(); + private List mainClassArguments = new ArrayList(); + + private String jarFile; + + public JavaProcessBuilder() { + super(null, null, null); + } + + // what version of java?? + // so, this starts a NEW java, from an ALREADY existing java. + + public JavaProcessBuilder(InputStream in, PrintStream out, PrintStream err) { + super(in, out, err); + } + + public final void setMainClass(String mainClass) { + this.mainClass = mainClass; + } + + public final void setStartingHeapSizeInMegabytes(int startingHeapSizeInMegabytes) { + this.startingHeapSizeInMegabytes = startingHeapSizeInMegabytes; + } + + public final void setMaximumHeapSizeInMegabytes(int maximumHeapSizeInMegabytes) { + this.maximumHeapSizeInMegabytes = maximumHeapSizeInMegabytes; + } + + public final void addJvmClasspath(String classpathEntry) { + classpathEntries.add(classpathEntry); + } + + public final void addJvmClasspaths(List paths) { + classpathEntries.addAll(paths); + } + + public final void addJvmOption(String argument) { + jvmOptions.add(argument); + } + + public final void addJvmOptions(List paths) { + jvmOptions.addAll(paths); + } + + public final void setJarFile(String jarFile) { + this.jarFile = jarFile; + } + + private String getClasspath() { + StringBuilder builder = new StringBuilder(); + int count = 0; + final int totalSize = classpathEntries.size(); + final String pathseparator = File.pathSeparator; + + // DO NOT QUOTE the elements in the classpath! + for (String classpathEntry : classpathEntries) { + try { + // make sure the classpath is ABSOLUTE pathname + classpathEntry = new File(classpathEntry).getCanonicalFile().getAbsolutePath(); + + // fix a nasty problem when spaces aren't properly escaped! + classpathEntry = classpathEntry.replaceAll(" ", "\\ "); + + builder.append(classpathEntry); + count++; + } catch (Exception e) { + e.printStackTrace(); + } + + if (count < totalSize) { + builder.append(pathseparator); // ; on windows, : on linux + } + } + return builder.toString(); + } + + /** + * Specify the JAVA exectuable to launch this process. By default, this will use the same java exectuable + * as was used to start the current JVM. + */ + public void setJava(String javaLocation) { + this.javaLocation = javaLocation; + } + + @Override + public void start() { + setExecutable(javaLocation); + + // save off the original arguments + List origArguments = new ArrayList(arguments.size()); + origArguments.addAll(arguments); + arguments = new ArrayList(0); + + + // two versions, java vs not-java + arguments.add("-Xms" + startingHeapSizeInMegabytes + "M"); + arguments.add("-Xmx" + maximumHeapSizeInMegabytes + "M"); + arguments.add("-server"); + + for (String option : jvmOptions) { + arguments.add(option); + } + + //same as -cp + String classpath = getClasspath(); + + // two more versions. jar vs classs + if (jarFile != null) { + arguments.add("-jar"); + arguments.add(jarFile); + + // interesting note. You CANNOT have a classpath specified on the commandline + // when using JARs!! It must be set in the jar's MANIFEST. + if (!classpath.isEmpty()) { + System.err.println("WHOOPS. You CANNOT have a classpath specified on the commandline when using JARs."); + System.err.println(" It must be set in the JARs MANIFEST instead."); + System.exit(1); + } + + } + // if we are running classes! + else if (mainClass != null) { + if (!classpath.isEmpty()) { + arguments.add("-classpath"); + arguments.add(classpath); + } + + // main class must happen AFTER the classpath! + arguments.add(mainClass); + } else { + System.err.println("WHOOPS. You must specify a jar or main class when running java!"); + System.exit(1); + } + + + for (String arg : mainClassArguments) { + if (arg.contains(" ")) { + // individual arguments MUST be in their own element in order to + // be processed properly (this is how it works on the command line!) + String[] split = arg.split(" "); + for (String s : split) { + arguments.add(s); + } + } else { + arguments.add(arg); + } + } + + arguments.addAll(origArguments); + + super.start(); + } + + + /** The directory into which a local VM installation should be unpacked. */ + public static final String LOCAL_JAVA_DIR = "java_vm"; + + /** + * Reconstructs the path to the JVM used to launch this process. + * + * @param windebug if true we will use java.exe instead of javaw.exe on Windows. + */ + public static String getJVMPath (File appdir, boolean windebug) + { + // first look in our application directory for an installed VM + String vmpath = checkJvmPath(new File(appdir, LOCAL_JAVA_DIR).getPath(), windebug); + + // then fall back to the VM in which we're already running + if (vmpath == null) { + vmpath = checkJvmPath(System.getProperty("java.home"), windebug); + } + + // then throw up our hands and hope for the best + if (vmpath == null) { + System.err.println("Unable to find java [appdir=" + appdir + ", java.home=" + System.getProperty("java.home") + "]!"); + vmpath = "java"; + } + + // Oddly, the Mac OS X specific java flag -Xdock:name will only work if java is launched + // from /usr/bin/java, and not if launched by directly referring to /bin/java, + // even though the former is a symlink to the latter! To work around this, see if the + // desired jvm is in fact pointed to by /usr/bin/java and, if so, use that instead. + if (OS.isMacOsX()) { + try { + File localVM = new File("/usr/bin/java").getCanonicalFile(); + if (localVM.equals(new File(vmpath).getCanonicalFile())) { + vmpath = "/usr/bin/java"; + } + } catch (IOException ioe) { + System.err.println("Failed to check Mac OS canonical VM path." + ioe); + } + } + + return vmpath; + } + + /** + * Checks whether a Java Virtual Machine can be located in the supplied path. + */ + private static String checkJvmPath(String vmhome, boolean windebug) { + // linux does this... + String vmbase = vmhome + File.separator + "bin" + File.separator; + String vmpath = vmbase + "java"; + if (new File(vmpath).exists()) { + return vmpath; + } + + // windows does this + if (!windebug) { + vmpath = vmbase + "javaw.exe"; + } else { + vmpath = vmbase + "java.exe"; // open a console on windows + } + + if (new File(vmpath).exists()) { + return vmpath; + } + + return null; + } +} \ No newline at end of file diff --git a/Dorkbox-Util/src/dorkbox/util/process/LauncherProcessBuilder.java b/Dorkbox-Util/src/dorkbox/util/process/LauncherProcessBuilder.java new file mode 100644 index 0000000..3fbd129 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/process/LauncherProcessBuilder.java @@ -0,0 +1,136 @@ +package dorkbox.util.process; + + + + + + +import java.io.File; +import java.io.InputStream; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.List; + +import dorkbox.util.OS; + +public class LauncherProcessBuilder extends ShellProcessBuilder { + + private String mainClass; + + private List classpathEntries = new ArrayList(); + private List mainClassArguments = new ArrayList(); + + private String jarFile; + + public LauncherProcessBuilder() { + super(null, null, null); + } + + public LauncherProcessBuilder(InputStream in, PrintStream out, PrintStream err) { + super(in, out, err); + } + + public final void setMainClass(String mainClass) { + this.mainClass = mainClass; + } + + public final void addJvmClasspath(String classpathEntry) { + classpathEntries.add(classpathEntry); + } + + public final void addJvmClasspaths(List paths) { + classpathEntries.addAll(paths); + } + + public final void setJarFile(String jarFile) { + this.jarFile = jarFile; + } + + private String getClasspath() { + StringBuilder builder = new StringBuilder(); + int count = 0; + final int totalSize = classpathEntries.size(); + final String pathseparator = File.pathSeparator; + + for (String classpathEntry : classpathEntries) { + // fix a nasty problem when spaces aren't properly escaped! + classpathEntry = classpathEntry.replaceAll(" ", "\\ "); + builder.append(classpathEntry); + count++; + if (count < totalSize) { + builder.append(pathseparator); // ; on windows, : on linux + } + } + return builder.toString(); + } + + @Override + public void start() { + if (OS.isWindows()) { + setExecutable("dorkboxc.exe"); + } else { + setExecutable("dorkbox"); + } + + + // save off the original arguments + List origArguments = new ArrayList(arguments.size()); + origArguments.addAll(arguments); + arguments = new ArrayList(0); + + arguments.add("-Xms40M"); + arguments.add("-Xmx256M"); +// arguments.add("-XX:PermSize=256M"); // default is 96 + + arguments.add("-server"); + + //same as -cp + String classpath = getClasspath(); + + // two more versions. jar vs classs + if (jarFile != null) { + // JAR is added by the launcher (based in the ini file!) +// arguments.add("-jar"); +// arguments.add(jarFile); + + // interesting note. You CANNOT have a classpath specified on the commandline + // when using JARs!! It must be set in the jar's MANIFEST. + if (!classpath.isEmpty()) { + System.err.println("WHOOPS. You CANNOT have a classpath specified on the commandline when using JARs."); + System.err.println(" It must be set in the JARs MANIFEST instead."); + System.exit(1); + } + + } + // if we are running classes! + else if (mainClass != null) { + arguments.add(mainClass); + + if (!classpath.isEmpty()) { + arguments.add("-classpath"); + arguments.add(classpath); + } + } else { + System.err.println("WHOOPS. You must specify a jar or main class when running java!"); + System.exit(1); + } + + + for (String arg : mainClassArguments) { + if (arg.contains(" ")) { + // individual arguments MUST be in their own element in order to + // be processed properly (this is how it works on the command line!) + String[] split = arg.split(" "); + for (String s : split) { + arguments.add(s); + } + } else { + arguments.add(arg); + } + } + + arguments.addAll(origArguments); + + super.start(); + } +} \ No newline at end of file diff --git a/Dorkbox-Util/src/dorkbox/util/process/NullOutputStream.java b/Dorkbox-Util/src/dorkbox/util/process/NullOutputStream.java new file mode 100644 index 0000000..2c6a181 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/process/NullOutputStream.java @@ -0,0 +1,25 @@ +package dorkbox.util.process; +import java.io.IOException; +import java.io.OutputStream; + +public class NullOutputStream extends OutputStream { + @Override + public void write(int i) throws IOException { + //do nothing + } + + @Override + public void write(byte[] b) throws IOException { + //do nothing + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + //do nothing + } + + @Override + public void flush() throws IOException { + //do nothing + } +} \ No newline at end of file diff --git a/Dorkbox-Util/src/dorkbox/util/process/ProcessProxy.java b/Dorkbox-Util/src/dorkbox/util/process/ProcessProxy.java new file mode 100644 index 0000000..85b63e7 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/process/ProcessProxy.java @@ -0,0 +1,61 @@ +package dorkbox.util.process; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class ProcessProxy extends Thread { + + private final InputStream is; + private final OutputStream os; + + // when reading from the stdin and outputting to the process + public ProcessProxy(String processName, InputStream inputStreamFromConsole, OutputStream outputStreamToProcess) { + is = inputStreamFromConsole; + os = outputStreamToProcess; + + setName(processName); + setDaemon(true); + } + + public void close() { + try { + is.close(); + } catch (IOException e) { + } + } + + @Override + public void run() { + try { + // this thread will read until there is no more data to read. (this is generally what you want) + // the stream will be closed when the process closes it (usually on exit) + int readInt; + + if (os == null) { + // just read so it won't block. + while ((readInt = is.read()) != -1) { + } + } else { + while ((readInt = is.read()) != -1) { + os.write(readInt); + + // flush the output on new line. Works for windows/linux, since \n is always the last char in the sequence. + if (readInt == '\n') { + os.flush(); + } + } + } + } catch (IOException ignore) { + } catch (IllegalArgumentException e) { + } finally { + try { + if (os != null) { + os.flush(); // this goes to the console, so we don't want to close it! + } + is.close(); + } catch (IOException ignore) { + } + } + } +} \ No newline at end of file diff --git a/Dorkbox-Util/src/dorkbox/util/process/ShellProcessBuilder.java b/Dorkbox-Util/src/dorkbox/util/process/ShellProcessBuilder.java new file mode 100644 index 0000000..89adc68 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/process/ShellProcessBuilder.java @@ -0,0 +1,303 @@ +package dorkbox.util.process; + + + + +import java.io.File; +import java.io.InputStream; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import dorkbox.util.OS; + +/** + * If you want to save off the output from the process, set a PrintStream to the following: + * ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(8196); + * PrintStream outputStream = new PrintStream(byteArrayOutputStream); + * ... + * String output = byteArrayOutputStream.toString(); + */ +public class ShellProcessBuilder { + + // TODO: see http://mark.koli.ch/2009/12/uac-prompt-from-java-createprocess-error740-the-requested-operation-requires-elevation.html + // for more information on copying files in windows with UAC protections. + // maybe we want to hook into our launcher executor so that we can "auto elevate" commands? + + private String workingDirectory = null; + private String executableName = null; + private String executableDirectory = null; + + protected List arguments = new ArrayList(); + + private final PrintStream outputStream; + private final PrintStream errorStream; + private final InputStream inputStream; + + private Process process = null; + + // true if we want to save off (usually for debugging) the initial output from this + private boolean debugInfo = false; + + /** + * This will cause the spawned process to pipe it's output to null. + */ + public ShellProcessBuilder() { + this(null, null, null); + } + + public ShellProcessBuilder(PrintStream out) { + this(null, out, out); + } + + public ShellProcessBuilder(InputStream in, PrintStream out) { + this(in, out, out); + } + + public ShellProcessBuilder(InputStream in, PrintStream out, PrintStream err) { + outputStream = out; + errorStream = err; + inputStream = in; + } + + /** + * When launched from eclipse, the working directory is USUALLY the root of the project folder + */ + public final ShellProcessBuilder setWorkingDirectory(String workingDirectory) { + // MUST be absolute path!! + this.workingDirectory = new File(workingDirectory).getAbsolutePath(); + return this; + } + + public final ShellProcessBuilder addArgument(String argument) { + arguments.add(argument); + return this; + } + + public final ShellProcessBuilder addArguments(String... paths) { + for (String path : paths) { + arguments.add(path); + } + return this; + } + + public final ShellProcessBuilder addArguments(List paths) { + arguments.addAll(paths); + return this; + } + + public final ShellProcessBuilder setExecutable(String executableName) { + this.executableName = executableName; + return this; + } + + public ShellProcessBuilder setExecutableDirectory(String executableDirectory) { + // MUST be absolute path!! + this.executableDirectory = new File(executableDirectory).getAbsolutePath(); + return this; + } + + public ShellProcessBuilder addDebugInfo() { + this.debugInfo = true; + return this; + } + + + public void start() { + // if no executable, then use the command shell + if (executableName == null) { + if (OS.isWindows()) { + // windows + executableName = "cmd"; + arguments.add(0, "/c"); + + } else { + // *nix + executableName = "/bin/bash"; + // executableName = "/bin/sh"; + arguments.add(0, "-c"); + } + } else if (workingDirectory != null) { + if (!workingDirectory.endsWith("/") && !workingDirectory.endsWith("\\")) { + workingDirectory += File.separator; + } + } + + if (executableDirectory != null) { + if (!executableDirectory.endsWith("/") && !executableDirectory.endsWith("\\")) { + executableDirectory += File.separator; + } + + executableName = executableDirectory + executableName; + } + + List argumentsList = new ArrayList(); + argumentsList.add(executableName); + + for (String arg : arguments) { + if (arg.contains(" ")) { + // individual arguments MUST be in their own element in order to + // be processed properly (this is how it works on the command line!) + String[] split = arg.split(" "); + for (String s : split) { + argumentsList.add(s); + } + } else { + argumentsList.add(arg); + } + } + + + // if we don't want output... TODO: i think we want to "exec" (this calls exec -c, which calls our program) + // this code as well, since calling it directly won't work + boolean pipeToNull = errorStream == null || outputStream == null; + if (pipeToNull) { + if (OS.isWindows()) { + // >NUL on windows + argumentsList.add(">NUL"); + } else { + // we will "pipe" it to /dev/null on *nix + argumentsList.add(">/dev/null 2>&1"); + } + } + + if (debugInfo) { + errorStream.print("Executing: "); + Iterator iterator = argumentsList.iterator(); + while (iterator.hasNext()) { + String s = iterator.next(); + errorStream.print(s); + if (iterator.hasNext()) { + errorStream.print(" "); + } + } + errorStream.print(OS.LINE_SEPARATOR); + } + + ProcessBuilder processBuilder = new ProcessBuilder(argumentsList); + if (workingDirectory != null) { + processBuilder.directory(new File(workingDirectory)); + } + + // combine these so output is properly piped to null. + if (pipeToNull) { + processBuilder.redirectErrorStream(true); + } + + try { + process = processBuilder.start(); + } catch (Exception ex) { + errorStream.println("There was a problem executing the program. Details:\n"); + ex.printStackTrace(errorStream); + + if (process != null) { + try { + process.destroy(); + process = null; + } catch (Exception e) { + errorStream.println("Error destroying process: \n"); + e.printStackTrace(errorStream); + } + } + } + + if (process != null) { + ProcessProxy writeToProcess_input; + ProcessProxy readFromProcess_output; + ProcessProxy readFromProcess_error; + + + if (pipeToNull) { + NullOutputStream nullOutputStream = new NullOutputStream(); + + processBuilder.redirectErrorStream(true); + + // readers (read process -> write console) + // have to keep the output buffers from filling in the target process. + readFromProcess_output = new ProcessProxy("Process Reader: " + executableName, process.getInputStream(), nullOutputStream); + readFromProcess_error = null; + } + // we want to pipe our input/output from process to ourselves + else { + /** + * Proxy the System.out and System.err from the spawned process back + * to the user's window. This is important or the spawned process could block. + */ + // readers (read process -> write console) + readFromProcess_output = new ProcessProxy("Process Reader: " + executableName, process.getInputStream(), outputStream); + if (errorStream != outputStream) { + readFromProcess_error = new ProcessProxy("Process Reader: " + executableName, process.getErrorStream(), errorStream); + } else { + processBuilder.redirectErrorStream(true); + readFromProcess_error = null; + } + } + + if (inputStream != null) { + /** + * Proxy System.in from the user's window to the spawned process + */ + // writer (read console -> write process) + writeToProcess_input = new ProcessProxy("Process Writer: " + executableName, inputStream, process.getOutputStream()); + } else { + writeToProcess_input = null; + } + + + // the process can be killed in two ways + // If not in eclipse, by this shutdown hook. (clicking the red square to terminate a process will not run it's shutdown hooks) + // Typing "exit" will always terminate the process + Thread hook = new Thread(new Runnable() { + @Override + public void run() { + if (debugInfo) { + errorStream.println("Terminating process: " + executableName); + } + process.destroy(); + } + } + ); + // add a shutdown hook to make sure that we properly terminate our spawned processes. + Runtime.getRuntime().addShutdownHook(hook); + + if (writeToProcess_input != null) { + writeToProcess_input.start(); + } + readFromProcess_output.start(); + if (readFromProcess_error != null) { + readFromProcess_error.start(); + } + + try { + process.waitFor(); + + @SuppressWarnings("unused") + int exitValue = process.exitValue(); + + // wait for the READER threads to die (meaning their streams have closed/EOF'd) + if (writeToProcess_input != null) { + // the INPUT (from stdin). It should be via the InputConsole, but if it's in eclipse,etc -- then this doesn't do anything + // We are done reading input, since our program has closed... + writeToProcess_input.close(); + writeToProcess_input.join(); + } + readFromProcess_output.close(); + readFromProcess_output.join(); + if (readFromProcess_error != null) { + readFromProcess_error.close(); + readFromProcess_error.join(); + } + + // forcibly terminate the process when it's streams have closed. + // this is for cleanup ONLY, not to actually do anything. + process.destroy(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // remove the shutdown hook now that we've shutdown. + Runtime.getRuntime().removeShutdownHook(hook); + } + } +} \ No newline at end of file diff --git a/Dorkbox-Util/src/dorkbox/util/properties/PropertiesProvider.java b/Dorkbox-Util/src/dorkbox/util/properties/PropertiesProvider.java new file mode 100644 index 0000000..110c508 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/properties/PropertiesProvider.java @@ -0,0 +1,127 @@ +package dorkbox.util.properties; + + +import java.awt.Color; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Properties; + +public class PropertiesProvider { + + // the basePath for properties based settings. In JAVA proper, this is by default relative to the jar location. + // in ANDROID dalvik, this must be specified to be the location of the APK plus some extra info. This must be set by the android app. + public static String basePath = ""; + + private final Properties properties = new SortedProperties(); + private final File propertiesFile; + + public PropertiesProvider(File propertiesFile) { + propertiesFile = propertiesFile.getAbsoluteFile(); + // make sure the parent dir exists... + File parentFile = propertiesFile.getParentFile(); + if (parentFile != null) { + parentFile.mkdirs(); + } + + this.propertiesFile = propertiesFile; + + _load(); + } + + private final void _load() { + if (!propertiesFile.canRead() || !propertiesFile.exists()) { + // in this case, our properties file doesn't exist yet... create one! + _save(); + } + + try { + FileInputStream fis = new FileInputStream(propertiesFile); + properties.load(fis); + fis.close(); + + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + // oops! + System.err.println("Properties cannot load!"); + e.printStackTrace(); + } + } + + + private final void _save() { + try { + FileOutputStream fos = new FileOutputStream(propertiesFile); + properties.store(fos, "Settings and configuration file. Strings must be escape formatted!"); + fos.flush(); + fos.close(); + + } catch (FileNotFoundException e) { + e.printStackTrace(); + System.err.println("Properties cannot save!"); + } catch (IOException e) { + // oops! + System.err.println("Properties cannot save!"); + e.printStackTrace(); + } + } + + + public synchronized final void remove(final String key) { + properties.remove(key); + _save(); + } + + public synchronized final void save(final String key, Object value) { + if (key == null || value == null) { + return; + } + + if (value instanceof Color) { + value = ((Color)value).getRGB(); + } + + properties.setProperty(key, value.toString()); + + _save(); + } + + @SuppressWarnings("unchecked") + public synchronized T get(String key, Class clazz) { + if (key == null || clazz == null) { + return null; + } + + String property = properties.getProperty(key); + if (property == null) { + return null; + } + + // special cases + try { + if (clazz.equals(Integer.class)) { + return (T) new Integer(Integer.parseInt(property)); + } + if (clazz.equals(Long.class)) { + return (T) new Long(Long.parseLong(property)); + } + if (clazz.equals(Color.class)) { + return (T) new Color(new Integer(Integer.parseInt(property)), true); + } + + else { + return (T) property; + } + } catch (Exception e) { + throw new RuntimeException("Properties Loader for property: " + key + System.getProperty("line.separator") + e.getMessage()); + } + } + + @Override + public String toString() { + return "PropertiesProvider [" + propertiesFile + "]"; + } +} diff --git a/Dorkbox-Util/src/dorkbox/util/properties/SortedProperties.java b/Dorkbox-Util/src/dorkbox/util/properties/SortedProperties.java new file mode 100644 index 0000000..7298bff --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/properties/SortedProperties.java @@ -0,0 +1,32 @@ +package dorkbox.util.properties; + +import java.util.Collections; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.Properties; +import java.util.Vector; + +public class SortedProperties extends Properties { + + private static final long serialVersionUID = 3988064683926999433L; + + private final Comparator compare = new Comparator() { + @Override + public int compare(Object o1, Object o2) { + return o1.toString().compareTo(o2.toString()); + }}; + + @Override + public synchronized Enumeration keys() { + Enumeration keysEnum = super.keys(); + + Vector vector = new Vector(size()); + for (;keysEnum.hasMoreElements();) { + vector.add(keysEnum.nextElement()); + } + + Collections.sort(vector, this.compare); + + return vector.elements(); + } +} diff --git a/Dorkbox-Util/src/org/bouncycastle/jcajce/provider/asymmetric/dsa/BCDSAPublicKeyAccessor.java b/Dorkbox-Util/src/org/bouncycastle/jcajce/provider/asymmetric/dsa/BCDSAPublicKeyAccessor.java new file mode 100644 index 0000000..475d877 --- /dev/null +++ b/Dorkbox-Util/src/org/bouncycastle/jcajce/provider/asymmetric/dsa/BCDSAPublicKeyAccessor.java @@ -0,0 +1,10 @@ +package org.bouncycastle.jcajce.provider.asymmetric.dsa; + +import java.math.BigInteger; +import java.security.spec.DSAParameterSpec; + +public class BCDSAPublicKeyAccessor { + public static BCDSAPublicKey newInstance(BigInteger bigInteger, DSAParameterSpec dsaParameterSpec) { + return new BCDSAPublicKey(bigInteger, dsaParameterSpec); + } +} diff --git a/Dorkbox-Util/src/org/bouncycastle/jcajce/provider/asymmetric/rsa/BCRSAPublicKeyAccessor.java b/Dorkbox-Util/src/org/bouncycastle/jcajce/provider/asymmetric/rsa/BCRSAPublicKeyAccessor.java new file mode 100644 index 0000000..aa8edae --- /dev/null +++ b/Dorkbox-Util/src/org/bouncycastle/jcajce/provider/asymmetric/rsa/BCRSAPublicKeyAccessor.java @@ -0,0 +1,10 @@ +package org.bouncycastle.jcajce.provider.asymmetric.rsa; + +import org.bouncycastle.crypto.params.RSAKeyParameters; + + +public class BCRSAPublicKeyAccessor { + public static BCRSAPublicKey newInstance(RSAKeyParameters publicKey) { + return new BCRSAPublicKey(publicKey); + } +} diff --git a/Dorkbox-Util/src/org/bouncycastle/jcajce/provider/asymmetric/x509/X509Accessor.java b/Dorkbox-Util/src/org/bouncycastle/jcajce/provider/asymmetric/x509/X509Accessor.java new file mode 100644 index 0000000..3d76357 --- /dev/null +++ b/Dorkbox-Util/src/org/bouncycastle/jcajce/provider/asymmetric/x509/X509Accessor.java @@ -0,0 +1,21 @@ +package org.bouncycastle.jcajce.provider.asymmetric.x509; + +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.util.Date; + +public class X509Accessor { + + /** + * Verify the TIME/DATE of the certificate + * Stupid BC is package private, so this will let us access this method. + */ + public static void verifyDate(java.security.cert.Certificate certificate) throws CertificateExpiredException, CertificateNotYetValidException { + // TODO: when checking the validite of the certificate, it is important to use a date from somewhere other than the + // host computer! (maybe use google? or something...) + // this will validate the DATES of the certificate, to make sure the cert is valid during the correct time period. + + org.bouncycastle.jcajce.provider.asymmetric.x509.X509CertificateObject cert = (org.bouncycastle.jcajce.provider.asymmetric.x509.X509CertificateObject) certificate; + cert.checkValidity(new Date()); + } +} diff --git a/Dorkbox-Util/src/org/bouncycastle/math/ec/ECAccessor.java b/Dorkbox-Util/src/org/bouncycastle/math/ec/ECAccessor.java new file mode 100644 index 0000000..70ed1dd --- /dev/null +++ b/Dorkbox-Util/src/org/bouncycastle/math/ec/ECAccessor.java @@ -0,0 +1,8 @@ +package org.bouncycastle.math.ec; + +public class ECAccessor { + public static void setCoordSystem(ECCurve curve, int coordinateSystem) { + curve.coord = coordinateSystem; + + } +} diff --git a/Dorkbox-Util/test/dorkbox/util/Base64FastTest.java b/Dorkbox-Util/test/dorkbox/util/Base64FastTest.java new file mode 100644 index 0000000..ac6db29 --- /dev/null +++ b/Dorkbox-Util/test/dorkbox/util/Base64FastTest.java @@ -0,0 +1,28 @@ +package dorkbox.util; + + +import static org.junit.Assert.fail; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Random; + +import org.junit.Test; + +public class Base64FastTest { + + @Test + public void base64Test() throws IOException { + byte[] randomData = new byte[1000000]; + new Random().nextBytes(randomData); + + byte[] enc = Base64Fast.encodeToByte(randomData, true); + byte[] dec = Base64Fast.decode(enc); + + if (!Arrays.equals(randomData, dec)) { + fail("base64 test failed"); + } + + randomData = null; + } +} \ No newline at end of file diff --git a/Dorkbox-Util/test/dorkbox/util/StorageTest.java b/Dorkbox-Util/test/dorkbox/util/StorageTest.java new file mode 100644 index 0000000..78f6aa1 --- /dev/null +++ b/Dorkbox-Util/test/dorkbox/util/StorageTest.java @@ -0,0 +1,221 @@ +package dorkbox.util; + + +import static org.junit.Assert.fail; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; + +import org.junit.Test; + +public class StorageTest { + + @Test + public void storageTest() throws IOException { + File tempFile = FileUtil.tempFile("storageTest"); + tempFile.deleteOnExit(); + + Data data = new Data(); + Storage storage = Storage.load(tempFile, data); + storage.setSaveDelay(0); + + if (data.bytes != null) { + fail("storage has data when it shouldn't"); + } + + makeData(data); + storage.save(); + + + Data data2 = new Data(); + storage.load(data2); + + if (!data.equals(data2)) { + fail("storage test not equal"); + } + + data.string = "A different string entirely!"; + storage.setSaveDelay(3000); + storage.save(); + + data2 = new Data(); + storage.load(data2); + if (!data.equals(data2)) { + fail("storage test not copying fields on the fly."); + } + + data2 = new Data(); + storage.load(data2); + if (!data.equals(data2)) { + fail("storage test not equal"); + } + + + try { + Storage.load(tempFile, null); + fail("storage test allowing null objects"); + } catch (Exception e) { + } + + Storage.shutdown(); + } + + + // from kryo unit test. + private void makeData(Data data) { + StringBuilder buffer = new StringBuilder(); + for (int i = 0; i < 3; i++) { + buffer.append('a'); + } + data.string = buffer.toString(); + + data.strings = new String[] {"ab012", "", null, "!@#$", "�����"}; + data.ints = new int[] {-1234567, 1234567, -1, 0, 1, Integer.MAX_VALUE, Integer.MIN_VALUE}; + data.shorts = new short[] {-12345, 12345, -1, 0, 1, Short.MAX_VALUE, Short.MIN_VALUE}; + data.floats = new float[] {0, -0, 1, -1, 123456, -123456, 0.1f, 0.2f, -0.3f, (float)Math.PI, Float.MAX_VALUE, + Float.MIN_VALUE}; + data.doubles = new double[] {0, -0, 1, -1, 123456, -123456, 0.1d, 0.2d, -0.3d, Math.PI, Double.MAX_VALUE, Double.MIN_VALUE}; + data.longs = new long[] {0, -0, 1, -1, 123456, -123456, 99999999999l, -99999999999l, Long.MAX_VALUE, Long.MIN_VALUE}; + data.bytes = new byte[] {-123, 123, -1, 0, 1, Byte.MAX_VALUE, Byte.MIN_VALUE}; + data.chars = new char[] {32345, 12345, 0, 1, 63, Character.MAX_VALUE, Character.MIN_VALUE}; + data.booleans = new boolean[] {true, false}; + data.Ints = new Integer[] {-1234567, 1234567, -1, 0, 1, Integer.MAX_VALUE, Integer.MIN_VALUE}; + data.Shorts = new Short[] {-12345, 12345, -1, 0, 1, Short.MAX_VALUE, Short.MIN_VALUE}; + data.Floats = new Float[] {0f, -0f, 1f, -1f, 123456f, -123456f, 0.1f, 0.2f, -0.3f, (float)Math.PI, Float.MAX_VALUE, + Float.MIN_VALUE}; + data.Doubles = new Double[] {0d, -0d, 1d, -1d, 123456d, -123456d, 0.1d, 0.2d, -0.3d, Math.PI, Double.MAX_VALUE, + Double.MIN_VALUE}; + data.Longs = new Long[] {0l, -0l, 1l, -1l, 123456l, -123456l, 99999999999l, -99999999999l, Long.MAX_VALUE, Long.MIN_VALUE}; + data.Bytes = new Byte[] {-123, 123, -1, 0, 1, Byte.MAX_VALUE, Byte.MIN_VALUE}; + data.Chars = new Character[] {32345, 12345, 0, 1, 63, Character.MAX_VALUE, Character.MIN_VALUE}; + data.Booleans = new Boolean[] {true, false}; + } + + public static class Data { + public String string; + public String[] strings; + public int[] ints; + public short[] shorts; + public float[] floats; + public double[] doubles; + public long[] longs; + public byte[] bytes; + public char[] chars; + public boolean[] booleans; + public Integer[] Ints; + public Short[] Shorts; + public Float[] Floats; + public Double[] Doubles; + public Long[] Longs; + public Byte[] Bytes; + public Character[] Chars; + public Boolean[] Booleans; + + public Data() { + } + + @Override + public int hashCode () { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(this.Booleans); + result = prime * result + Arrays.hashCode(this.Bytes); + result = prime * result + Arrays.hashCode(this.Chars); + result = prime * result + Arrays.hashCode(this.Doubles); + result = prime * result + Arrays.hashCode(this.Floats); + result = prime * result + Arrays.hashCode(this.Ints); + result = prime * result + Arrays.hashCode(this.Longs); + result = prime * result + Arrays.hashCode(this.Shorts); + result = prime * result + Arrays.hashCode(this.booleans); + result = prime * result + Arrays.hashCode(this.bytes); + result = prime * result + Arrays.hashCode(this.chars); + result = prime * result + Arrays.hashCode(this.doubles); + result = prime * result + Arrays.hashCode(this.floats); + result = prime * result + Arrays.hashCode(this.ints); + result = prime * result + Arrays.hashCode(this.longs); + result = prime * result + Arrays.hashCode(this.shorts); + result = prime * result + (this.string == null ? 0 : this.string.hashCode()); + result = prime * result + Arrays.hashCode(this.strings); + return result; + } + + @Override + public boolean equals (Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Data other = (Data)obj; + if (!Arrays.equals(this.Booleans, other.Booleans)) { + return false; + } + if (!Arrays.equals(this.Bytes, other.Bytes)) { + return false; + } + if (!Arrays.equals(this.Chars, other.Chars)) { + return false; + } + if (!Arrays.equals(this.Doubles, other.Doubles)) { + return false; + } + if (!Arrays.equals(this.Floats, other.Floats)) { + return false; + } + if (!Arrays.equals(this.Ints, other.Ints)) { + return false; + } + if (!Arrays.equals(this.Longs, other.Longs)) { + return false; + } + if (!Arrays.equals(this.Shorts, other.Shorts)) { + return false; + } + if (!Arrays.equals(this.booleans, other.booleans)) { + return false; + } + if (!Arrays.equals(this.bytes, other.bytes)) { + return false; + } + if (!Arrays.equals(this.chars, other.chars)) { + return false; + } + if (!Arrays.equals(this.doubles, other.doubles)) { + return false; + } + if (!Arrays.equals(this.floats, other.floats)) { + return false; + } + if (!Arrays.equals(this.ints, other.ints)) { + return false; + } + if (!Arrays.equals(this.longs, other.longs)) { + return false; + } + if (!Arrays.equals(this.shorts, other.shorts)) { + return false; + } + if (this.string == null) { + if (other.string != null) { + return false; + } + } else if (!this.string.equals(other.string)) { + return false; + } + if (!Arrays.equals(this.strings, other.strings)) { + return false; + } + return true; + } + + @Override + public String toString () { + return "Data"; + } + } +} \ No newline at end of file diff --git a/Dorkbox-Util/test/dorkbox/util/crypto/AesByteBufTest.java b/Dorkbox-Util/test/dorkbox/util/crypto/AesByteBufTest.java new file mode 100644 index 0000000..3e1aa08 --- /dev/null +++ b/Dorkbox-Util/test/dorkbox/util/crypto/AesByteBufTest.java @@ -0,0 +1,325 @@ + +package dorkbox.util.crypto; + +import static org.junit.Assert.fail; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +import java.io.IOException; +import java.security.SecureRandom; +import java.util.Arrays; + +import org.bouncycastle.crypto.engines.AESFastEngine; +import org.bouncycastle.crypto.modes.GCMBlockCipher; +import org.bouncycastle.crypto.params.KeyParameter; +import org.bouncycastle.crypto.params.ParametersWithIV; +import org.junit.Test; + +import dorkbox.util.crypto.bouncycastle.GCMBlockCipher_ByteBuf; + +public class AesByteBufTest { + + private static String entropySeed = "asdjhasdkljalksdfhlaks4356268909087s0dfgkjh255124515hasdg87"; + + private String text = "hello, my name is inigo montoya. hello, my name is inigo montoya. hello, my name is inigo montoya." + + "hello, my name is inigo montoya. hello, my name is inigo montoya. hello, my name is inigo montoya." + + "hello, my name is inigo montoya. hello, my name is inigo montoya. hello, my name is inigo montoya." + + "hello, my name is inigo montoya. hello, my name is inigo montoya. hello, my name is inigo montoya." + + "hello, my name is inigo montoya. hello, my name is inigo montoya. hello, my name is inigo montoya." + + "hello, my name is inigo montoya. hello, my name is inigo montoya. hello, my name is inigo montoya." + + "hello, my name is inigo montoya. hello, my name is inigo montoya. hello, my name is inigo montoya." + + "hello, my name is inigo montoya. hello, my name is inigo montoya. hello, my name is inigo montoya." + + "hello, my name is inigo montoya. hello, my name is inigo montoya. hello, my name is inigo montoya." + + "hello, my name is inigo montoya. hello, my name is inigo montoya. hello, my name is inigo montoya." + + "hello, my name is inigo montoya. hello, my name is inigo montoya. hello, my name is inigo montoya." + + "hello, my name is inigo montoya. hello, my name is inigo montoya. hello, my name is inigo montoya." + + "hello, my name is inigo montoya. hello, my name is inigo montoya. hello, my name is inigo montoya." + + "hello, my name is inigo montoya. hello, my name is inigo montoya. hello, my name is inigo montoya." + + "hello, my name is inigo montoya. hello, my name is inigo montoya. hello, my name is inigo montoya." + + "hello, my name is inigo montoya. hello, my name is inigo montoya. hello, my name is inigo montoya." + + "hello, my name is inigo montoya. hello, my name is inigo montoya. hello, my name is inigo montoya."; + + // test input smaller than block size + private byte[] bytes = "hello!".getBytes(); + + // test input larger than block size + private byte[] bytesLarge = text.getBytes(); + + @Test + public void AesGcmEncryptBothA() throws IOException { + + final byte[] SOURCE = bytesLarge; + + SecureRandom rand = new SecureRandom(entropySeed.getBytes()); + + final GCMBlockCipher_ByteBuf aesEngine1 = new GCMBlockCipher_ByteBuf(new AESFastEngine()); + final GCMBlockCipher aesEngine2 = new GCMBlockCipher(new AESFastEngine()); + + final byte[] key = new byte[32]; + final byte[] iv = new byte[16]; + + // note: the IV needs to be VERY unique! + rand.nextBytes(key); // 256bit key (32 bytes) + rand.nextBytes(iv); // 128bit block size (16 bytes) + + final ParametersWithIV aesIVAndKey = new ParametersWithIV(new KeyParameter(key), iv); + + ByteBuf source = Unpooled.wrappedBuffer(SOURCE); + int length = SOURCE.length; + ByteBuf encryptAES = Unpooled.buffer(1024); + int encryptLength = Crypto.AES.encrypt(aesEngine1, aesIVAndKey, source, encryptAES, length); + + byte[] encrypt = new byte[encryptLength]; + System.arraycopy(encryptAES.array(), 0, encrypt, 0, encryptLength); + + byte[] encrypt2 = Crypto.AES.encrypt(aesEngine2, key, iv, SOURCE); + + + if (Arrays.equals(SOURCE, encrypt)) { + fail("bytes should not be equal"); + } + + if (!Arrays.equals(encrypt, encrypt2)) { + fail("bytes not equal"); + } + } + + @Test + public void AesGcmEncryptBothB() throws IOException { + final byte[] SOURCE = bytes; + + SecureRandom rand = new SecureRandom(entropySeed.getBytes()); + + final GCMBlockCipher_ByteBuf aesEngine1 = new GCMBlockCipher_ByteBuf(new AESFastEngine()); + final GCMBlockCipher aesEngine2 = new GCMBlockCipher(new AESFastEngine()); + + final byte[] key = new byte[32]; + final byte[] iv = new byte[16]; + + // note: the IV needs to be VERY unique! + rand.nextBytes(key); // 256bit key (32 bytes) + rand.nextBytes(iv); // 128bit block size (16 bytes) + + final ParametersWithIV aesIVAndKey = new ParametersWithIV(new KeyParameter(key), iv); + + ByteBuf source = Unpooled.wrappedBuffer(SOURCE); + int length = SOURCE.length; + ByteBuf encryptAES = Unpooled.buffer(1024); + int encryptLength = Crypto.AES.encrypt(aesEngine1, aesIVAndKey, source, encryptAES, length); + + byte[] encrypt = new byte[encryptLength]; + System.arraycopy(encryptAES.array(), 0, encrypt, 0, encryptLength); + + + byte[] encrypt2 = Crypto.AES.encrypt(aesEngine2, key, iv, SOURCE); + + + if (Arrays.equals(SOURCE, encrypt)) { + fail("bytes should not be equal"); + } + + if (!Arrays.equals(encrypt, encrypt2)) { + fail("bytes not equal"); + } + } + + @Test + public void AesGcmEncryptBufOnly() throws IOException { + + SecureRandom rand = new SecureRandom(entropySeed.getBytes()); + + GCMBlockCipher_ByteBuf aesEngine1 = new GCMBlockCipher_ByteBuf(new AESFastEngine()); + GCMBlockCipher aesEngine2 = new GCMBlockCipher(new AESFastEngine()); + + byte[] key = new byte[32]; + byte[] iv = new byte[16]; + + // note: the IV needs to be VERY unique! + rand.nextBytes(key); // 256bit key (32 bytes) + rand.nextBytes(iv); // 128bit block size (16 bytes) + + ParametersWithIV aesIVAndKey = new ParametersWithIV(new KeyParameter(key), iv); + ByteBuf source = Unpooled.wrappedBuffer(bytes); + int length = bytes.length; + ByteBuf encryptAES = Unpooled.buffer(1024); + int encryptLength = Crypto.AES.encrypt(aesEngine1, aesIVAndKey, source, encryptAES, length); + + byte[] encrypt = new byte[encryptLength]; + System.arraycopy(encryptAES.array(), 0, encrypt, 0, encryptLength); + + + byte[] decrypt = Crypto.AES.decrypt(aesEngine2, key, iv, encrypt); + + + if (Arrays.equals(bytes, encrypt)) { + fail("bytes should not be equal"); + } + + if (!Arrays.equals(bytes, decrypt)) { + fail("bytes not equal"); + } + } + + @Test + public void AesGcmDecryptBothA() throws IOException { + + final byte[] SOURCE = bytesLarge; + + SecureRandom rand = new SecureRandom(entropySeed.getBytes()); + + final GCMBlockCipher aesEngine1 = new GCMBlockCipher(new AESFastEngine()); + final GCMBlockCipher_ByteBuf aesEngine2 = new GCMBlockCipher_ByteBuf(new AESFastEngine()); + + final byte[] key = new byte[32]; + final byte[] iv = new byte[16]; + + // note: the IV needs to be VERY unique! + rand.nextBytes(key); // 256bit key (32 bytes) + rand.nextBytes(iv); // 128bit block size (16 bytes) + + final byte[] encrypt = Crypto.AES.encrypt(aesEngine1, key, iv, SOURCE); + final ByteBuf encryptAES = Unpooled.wrappedBuffer(encrypt); + final int length = encrypt.length; + + byte[] decrypt1 = Crypto.AES.decrypt(aesEngine1, key, iv, encrypt); + + + ParametersWithIV aesIVAndKey = new ParametersWithIV(new KeyParameter(key), iv); + ByteBuf decryptAES = Unpooled.buffer(1024); + int decryptLength = Crypto.AES.decrypt(aesEngine2, aesIVAndKey, encryptAES, decryptAES, length); + byte[] decrypt2 = new byte[decryptLength]; + System.arraycopy(decryptAES.array(), 0, decrypt2, 0, decryptLength); + + + if (Arrays.equals(SOURCE, encrypt)) { + fail("bytes should not be equal"); + } + + if (!Arrays.equals(decrypt1, decrypt2)) { + fail("bytes not equal"); + } + + if (!Arrays.equals(SOURCE, decrypt1)) { + fail("bytes not equal"); + } + } + + @Test + public void AesGcmDecryptBothB() throws IOException { + + byte[] SOURCE = bytes; + + SecureRandom rand = new SecureRandom(entropySeed.getBytes()); + + final GCMBlockCipher aesEngine1 = new GCMBlockCipher(new AESFastEngine()); + final GCMBlockCipher_ByteBuf aesEngine2 = new GCMBlockCipher_ByteBuf(new AESFastEngine()); + + final byte[] key = new byte[32]; + final byte[] iv = new byte[16]; + + // note: the IV needs to be VERY unique! + rand.nextBytes(key); // 256bit key (32 bytes) + rand.nextBytes(iv); // 128bit block size (16 bytes) + + final byte[] encrypt = Crypto.AES.encrypt(aesEngine1, key, iv, SOURCE); + final ByteBuf encryptAES = Unpooled.wrappedBuffer(encrypt); + final int length = encrypt.length; + + + byte[] decrypt1 = Crypto.AES.decrypt(aesEngine1, key, iv, encrypt); + + + ParametersWithIV aesIVAndKey = new ParametersWithIV(new KeyParameter(key), iv); + ByteBuf decryptAES = Unpooled.buffer(1024); + int decryptLength = Crypto.AES.decrypt(aesEngine2, aesIVAndKey, encryptAES, decryptAES, length); + byte[] decrypt2 = new byte[decryptLength]; + System.arraycopy(decryptAES.array(), 0, decrypt2, 0, decryptLength); + + + if (Arrays.equals(SOURCE, encrypt)) { + fail("bytes should not be equal"); + } + + if (!Arrays.equals(decrypt1, decrypt2)) { + fail("bytes not equal"); + } + + if (!Arrays.equals(SOURCE, decrypt1)) { + fail("bytes not equal"); + } + } + + @Test + public void AesGcmDecryptBufOnlyA() throws IOException { + byte[] SOURCE = bytesLarge; + + SecureRandom rand = new SecureRandom(entropySeed.getBytes()); + + GCMBlockCipher aesEngine1 = new GCMBlockCipher(new AESFastEngine()); + GCMBlockCipher_ByteBuf aesEngine2 = new GCMBlockCipher_ByteBuf(new AESFastEngine()); + + byte[] key = new byte[32]; + byte[] iv = new byte[16]; + + // note: the IV needs to be VERY unique! + rand.nextBytes(key); // 256bit key (32 bytes) + rand.nextBytes(iv); // 128bit block size (16 bytes) + + + byte[] encrypt = Crypto.AES.encrypt(aesEngine1, key, iv, SOURCE); + ByteBuf encryptAES = Unpooled.wrappedBuffer(encrypt); + int length = encrypt.length; + + + ParametersWithIV aesIVAndKey = new ParametersWithIV(new KeyParameter(key), iv); + ByteBuf decryptAES = Unpooled.buffer(1024); + int decryptLength = Crypto.AES.decrypt(aesEngine2, aesIVAndKey, encryptAES, decryptAES, length); + byte[] decrypt = new byte[decryptLength]; + System.arraycopy(decryptAES.array(), 0, decrypt, 0, decryptLength); + + if (Arrays.equals(SOURCE, encrypt)) { + fail("bytes should not be equal"); + } + + if (!Arrays.equals(SOURCE, decrypt)) { + fail("bytes not equal"); + } + } + + @Test + public void AesGcmDecryptBufOnlyB() throws IOException { + byte[] SOURCE = bytes; + + SecureRandom rand = new SecureRandom(entropySeed.getBytes()); + + GCMBlockCipher aesEngine1 = new GCMBlockCipher(new AESFastEngine()); + GCMBlockCipher_ByteBuf aesEngine2 = new GCMBlockCipher_ByteBuf(new AESFastEngine()); + + byte[] key = new byte[32]; + byte[] iv = new byte[16]; + + // note: the IV needs to be VERY unique! + rand.nextBytes(key); // 256bit key (32 bytes) + rand.nextBytes(iv); // 128bit block size (16 bytes) + + + byte[] encrypt = Crypto.AES.encrypt(aesEngine1, key, iv, SOURCE); + ByteBuf encryptAES = Unpooled.wrappedBuffer(encrypt); + int length = encrypt.length; + + + ParametersWithIV aesIVAndKey = new ParametersWithIV(new KeyParameter(key), iv); + ByteBuf decryptAES = Unpooled.buffer(1024); + int decryptLength = Crypto.AES.decrypt(aesEngine2, aesIVAndKey, encryptAES, decryptAES, length); + byte[] decrypt = new byte[decryptLength]; + System.arraycopy(decryptAES.array(), 0, decrypt, 0, decryptLength); + + if (Arrays.equals(SOURCE, encrypt)) { + fail("bytes should not be equal"); + } + + if (!Arrays.equals(SOURCE, decrypt)) { + fail("bytes not equal"); + } + } +} diff --git a/Dorkbox-Util/test/dorkbox/util/crypto/AesTest.java b/Dorkbox-Util/test/dorkbox/util/crypto/AesTest.java new file mode 100644 index 0000000..5c0a371 --- /dev/null +++ b/Dorkbox-Util/test/dorkbox/util/crypto/AesTest.java @@ -0,0 +1,328 @@ + +package dorkbox.util.crypto; + + +import static org.junit.Assert.fail; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.SecureRandom; +import java.util.Arrays; + +import org.bouncycastle.crypto.engines.AESFastEngine; +import org.bouncycastle.crypto.modes.CBCBlockCipher; +import org.bouncycastle.crypto.modes.GCMBlockCipher; +import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher; +import org.junit.Test; + +public class AesTest { + + private static String entropySeed = "asdjhasdkljalksdfhlaks4356268909087s0dfgkjh255124515hasdg87"; + + @Test + public void AesGcm() throws IOException { + byte[] bytes = "hello, my name is inigo montoya".getBytes(); + + SecureRandom rand = new SecureRandom(entropySeed.getBytes()); + + GCMBlockCipher aesEngine = new GCMBlockCipher(new AESFastEngine()); + + byte[] key = new byte[32]; + byte[] iv = new byte[16]; + + // note: the IV needs to be VERY unique! + rand.nextBytes(key); // 256bit key (32 bytes) + rand.nextBytes(iv); // 128bit block size (16 bytes) + + + byte[] encryptAES = Crypto.AES.encrypt(aesEngine, key, iv, bytes); + byte[] decryptAES = Crypto.AES.decrypt(aesEngine, key, iv, encryptAES); + + if (Arrays.equals(bytes, encryptAES)) { + fail("bytes should not be equal"); + } + + if (!Arrays.equals(bytes, decryptAES)) { + fail("bytes not equal"); + } + } + + // Note: this is still tested, but DO NOT USE BLOCK MODE as it does NOT provide authentication. GCM does. + @SuppressWarnings("deprecation") + @Test + public void AesBlock() throws IOException { + byte[] bytes = "hello, my name is inigo montoya".getBytes(); + + SecureRandom rand = new SecureRandom(entropySeed.getBytes()); + + PaddedBufferedBlockCipher aesEngine = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESFastEngine())); + + byte[] key = new byte[32]; + byte[] iv = new byte[16]; + + // note: the IV needs to be VERY unique! + rand.nextBytes(key); // 256bit key + rand.nextBytes(iv); // 16bit block size + + + byte[] encryptAES = Crypto.AES.encrypt(aesEngine, key, iv, bytes); + byte[] decryptAES = Crypto.AES.decrypt(aesEngine, key, iv, encryptAES); + + if (Arrays.equals(bytes, encryptAES)) { + fail("bytes should not be equal"); + } + + if (!Arrays.equals(bytes, decryptAES)) { + fail("bytes not equal"); + } + } + + @Test + public void AesGcmStream() throws IOException { + byte[] originalBytes = "hello, my name is inigo montoya".getBytes(); + ByteArrayInputStream inputStream = new ByteArrayInputStream(originalBytes); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + SecureRandom rand = new SecureRandom(entropySeed.getBytes()); + + GCMBlockCipher aesEngine = new GCMBlockCipher(new AESFastEngine()); + + byte[] key = new byte[32]; + byte[] iv = new byte[16]; + + // note: the IV needs to be VERY unique! + rand.nextBytes(key); // 256bit key + rand.nextBytes(iv); // 128bit block size + + + boolean success = Crypto.AES.encryptStream(aesEngine, key, iv, inputStream, outputStream); + + if (!success) { + fail("crypto was not successful"); + } + + byte[] encryptBytes = outputStream.toByteArray(); + + inputStream = new ByteArrayInputStream(outputStream.toByteArray()); + outputStream = new ByteArrayOutputStream(); + + success = Crypto.AES.decryptStream(aesEngine, key, iv, inputStream, outputStream); + + if (!success) { + fail("crypto was not successful"); + } + + byte[] decryptBytes = outputStream.toByteArray(); + + if (Arrays.equals(originalBytes, encryptBytes)) { + fail("bytes should not be equal"); + } + + if (!Arrays.equals(originalBytes, decryptBytes)) { + fail("bytes not equal"); + } + } + + // Note: this is still tested, but DO NOT USE BLOCK MODE as it does NOT provide authentication. GCM does. + @SuppressWarnings("deprecation") + @Test + public void AesBlockStream() throws IOException { + byte[] originalBytes = "hello, my name is inigo montoya".getBytes(); + ByteArrayInputStream inputStream = new ByteArrayInputStream(originalBytes); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + SecureRandom rand = new SecureRandom(entropySeed.getBytes()); + + PaddedBufferedBlockCipher aesEngine = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESFastEngine())); + + byte[] key = new byte[32]; + byte[] iv = new byte[16]; + + // note: the IV needs to be VERY unique! + rand.nextBytes(key); // 256bit key + rand.nextBytes(iv); // 128bit block size + + + boolean success = Crypto.AES.encryptStream(aesEngine, key, iv, inputStream, outputStream); + + if (!success) { + fail("crypto was not successful"); + } + + byte[] encryptBytes = outputStream.toByteArray(); + + inputStream = new ByteArrayInputStream(outputStream.toByteArray()); + outputStream = new ByteArrayOutputStream(); + + success = Crypto.AES.decryptStream(aesEngine, key, iv, inputStream, outputStream); + + + if (!success) { + fail("crypto was not successful"); + } + + byte[] decryptBytes = outputStream.toByteArray(); + + if (Arrays.equals(originalBytes, encryptBytes)) { + fail("bytes should not be equal"); + } + + if (!Arrays.equals(originalBytes, decryptBytes)) { + fail("bytes not equal"); + } + } + + @Test + public void AesWithIVGcm() throws IOException { + byte[] bytes = "hello, my name is inigo montoya".getBytes(); + + SecureRandom rand = new SecureRandom(entropySeed.getBytes()); + + GCMBlockCipher aesEngine = new GCMBlockCipher(new AESFastEngine()); + + byte[] key = new byte[32]; // 256bit key + byte[] iv = new byte[aesEngine.getUnderlyingCipher().getBlockSize()]; + + // note: the IV needs to be VERY unique! + rand.nextBytes(key); + rand.nextBytes(iv); + + + byte[] encryptAES = Crypto.AES.encryptWithIV(aesEngine, key, iv, bytes); + byte[] decryptAES = Crypto.AES.decryptWithIV(aesEngine, key, encryptAES); + + if (Arrays.equals(bytes, encryptAES)) { + fail("bytes should not be equal"); + } + + if (!Arrays.equals(bytes, decryptAES)) { + fail("bytes not equal"); + } + } + + // Note: this is still tested, but DO NOT USE BLOCK MODE as it does NOT provide authentication. GCM does. + @SuppressWarnings("deprecation") + @Test + public void AesWithIVBlock() throws IOException { + byte[] bytes = "hello, my name is inigo montoya".getBytes(); + + SecureRandom rand = new SecureRandom(entropySeed.getBytes()); + + PaddedBufferedBlockCipher aesEngine = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESFastEngine())); + + byte[] key = new byte[32]; // 256bit key + byte[] iv = new byte[aesEngine.getUnderlyingCipher().getBlockSize()]; + + // note: the IV needs to be VERY unique! + rand.nextBytes(key); + rand.nextBytes(iv); + + + byte[] encryptAES = Crypto.AES.encryptWithIV(aesEngine, key, iv, bytes); + byte[] decryptAES = Crypto.AES.decryptWithIV(aesEngine, key, encryptAES); + + if (Arrays.equals(bytes, encryptAES)) { + fail("bytes should not be equal"); + } + + if (!Arrays.equals(bytes, decryptAES)) { + fail("bytes not equal"); + } + } + + @Test + public void AesWithIVGcmStream() throws IOException { + byte[] originalBytes = "hello, my name is inigo montoya".getBytes(); + ByteArrayInputStream inputStream = new ByteArrayInputStream(originalBytes); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + SecureRandom rand = new SecureRandom(entropySeed.getBytes()); + + GCMBlockCipher aesEngine = new GCMBlockCipher(new AESFastEngine()); + + byte[] key = new byte[32]; + byte[] iv = new byte[16]; + + // note: the IV needs to be VERY unique! + rand.nextBytes(key); // 256bit key + rand.nextBytes(iv); // 128bit block size + + + boolean success = Crypto.AES.encryptStreamWithIV(aesEngine, key, iv, inputStream, outputStream); + + if (!success) { + fail("crypto was not successful"); + } + + byte[] encryptBytes = outputStream.toByteArray(); + + inputStream = new ByteArrayInputStream(outputStream.toByteArray()); + outputStream = new ByteArrayOutputStream(); + + success = Crypto.AES.decryptStreamWithIV(aesEngine, key, inputStream, outputStream); + + if (!success) { + fail("crypto was not successful"); + } + + byte[] decryptBytes = outputStream.toByteArray(); + + if (Arrays.equals(originalBytes, encryptBytes)) { + fail("bytes should not be equal"); + } + + if (!Arrays.equals(originalBytes, decryptBytes)) { + fail("bytes not equal"); + } + } + + // Note: this is still tested, but DO NOT USE BLOCK MODE as it does NOT provide authentication. GCM does. + @SuppressWarnings("deprecation") + @Test + public void AesWithIVBlockStream() throws IOException { + byte[] originalBytes = "hello, my name is inigo montoya".getBytes(); + ByteArrayInputStream inputStream = new ByteArrayInputStream(originalBytes); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + SecureRandom rand = new SecureRandom(entropySeed.getBytes()); + + PaddedBufferedBlockCipher aesEngine = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESFastEngine())); + + byte[] key = new byte[32]; + byte[] iv = new byte[16]; + + // note: the IV needs to be VERY unique! + rand.nextBytes(key); // 256bit key + rand.nextBytes(iv); // 128bit block size + + + boolean success = Crypto.AES.encryptStreamWithIV(aesEngine, key, iv, inputStream, outputStream); + + if (!success) { + fail("crypto was not successful"); + } + + byte[] encryptBytes = outputStream.toByteArray(); + + inputStream = new ByteArrayInputStream(outputStream.toByteArray()); + outputStream = new ByteArrayOutputStream(); + + success = Crypto.AES.decryptStreamWithIV(aesEngine, key, inputStream, outputStream); + + + if (!success) { + fail("crypto was not successful"); + } + + byte[] decryptBytes = outputStream.toByteArray(); + + if (Arrays.equals(originalBytes, encryptBytes)) { + fail("bytes should not be equal"); + } + + if (!Arrays.equals(originalBytes, decryptBytes)) { + fail("bytes not equal"); + } + } +} diff --git a/Dorkbox-Util/test/dorkbox/util/crypto/DsaTest.java b/Dorkbox-Util/test/dorkbox/util/crypto/DsaTest.java new file mode 100644 index 0000000..67e804d --- /dev/null +++ b/Dorkbox-Util/test/dorkbox/util/crypto/DsaTest.java @@ -0,0 +1,136 @@ + +package dorkbox.util.crypto; + + + +import static org.junit.Assert.fail; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.Arrays; + +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.asn1.x509.DSAParameter; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.asn1.x9.X9ObjectIdentifiers; +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.crypto.params.DSAParameters; +import org.bouncycastle.crypto.params.DSAPrivateKeyParameters; +import org.bouncycastle.crypto.params.DSAPublicKeyParameters; +import org.bouncycastle.crypto.util.PrivateKeyFactory; +import org.bouncycastle.crypto.util.PublicKeyFactory; +import org.junit.Test; + + +@SuppressWarnings("deprecation") +public class DsaTest { + private static String entropySeed = "asdjhaffasttjjhgpx600gn,-356268909087s0dfgkjh255124515hasdg87"; + + // Note: this is here just for keeping track of how this is done. This should NOT be used, and instead ECC crypto used instead. + @Test + public void Dsa() { + byte[] bytes = "hello, my name is inigo montoya".getBytes(); + + AsymmetricCipherKeyPair generateKeyPair = Crypto.DSA.generateKeyPair(new SecureRandom(entropySeed.getBytes()), 1024); + DSAPrivateKeyParameters privateKey = (DSAPrivateKeyParameters) generateKeyPair.getPrivate(); + DSAPublicKeyParameters publicKey = (DSAPublicKeyParameters) generateKeyPair.getPublic(); + + + BigInteger[] signature = Crypto.DSA.generateSignature(privateKey, new SecureRandom(entropySeed.getBytes()), bytes); + + boolean verify1 = Crypto.DSA.verifySignature(publicKey, bytes, signature); + + if (!verify1) { + fail("failed signature verification"); + } + + + byte[] bytes2 = "hello, my name is inigo montoya FAILED VERSION".getBytes(); + + if (Arrays.equals(bytes, bytes2)) { + fail("failed to create different byte arrays for testing bad messages"); + } + + + + boolean verify2 = Crypto.DSA.verifySignature(publicKey, bytes2, signature); + + if (verify2) { + fail("failed signature verification with bad message"); + } + } + + @Test + public void DsaJceSerializaion() throws IOException { + + AsymmetricCipherKeyPair generateKeyPair = Crypto.DSA.generateKeyPair(new SecureRandom(entropySeed.getBytes()), 1024); + DSAPrivateKeyParameters privateKey = (DSAPrivateKeyParameters) generateKeyPair.getPrivate(); + DSAPublicKeyParameters publicKey = (DSAPublicKeyParameters) generateKeyPair.getPublic(); + + + // public key as bytes. + DSAParameters parameters = publicKey.getParameters(); + byte[] bs = new SubjectPublicKeyInfo( + new AlgorithmIdentifier(X9ObjectIdentifiers.id_dsa, + new DSAParameter(parameters.getP(), parameters.getQ(), parameters.getG()).toASN1Primitive()), + new ASN1Integer(publicKey.getY())).getEncoded(); + + + + parameters = privateKey.getParameters(); + byte[] bs2 = new PrivateKeyInfo( + new AlgorithmIdentifier(X9ObjectIdentifiers.id_dsa, + new DSAParameter(parameters.getP(), parameters.getQ(), parameters.getG()).toASN1Primitive()), + new ASN1Integer(privateKey.getX())).getEncoded(); + + + + + + DSAPrivateKeyParameters privateKey2 = (DSAPrivateKeyParameters) PrivateKeyFactory.createKey(bs2); + DSAPublicKeyParameters publicKey2 = (DSAPublicKeyParameters) PublicKeyFactory.createKey(bs); + + + + // test via signing + byte[] bytes = "hello, my name is inigo montoya".getBytes(); + + + BigInteger[] signature = Crypto.DSA.generateSignature(privateKey, new SecureRandom(entropySeed.getBytes()), bytes); + + boolean verify1 = Crypto.DSA.verifySignature(publicKey, bytes, signature); + + if (!verify1) { + fail("failed signature verification"); + } + + + boolean verify2 = Crypto.DSA.verifySignature(publicKey2, bytes, signature); + + if (!verify2) { + fail("failed signature verification"); + } + + + + // now reverse who signs what. + BigInteger[] signatureB = Crypto.DSA.generateSignature(privateKey2, new SecureRandom(entropySeed.getBytes()), bytes); + + boolean verifyB1 = Crypto.DSA.verifySignature(publicKey, bytes, signatureB); + + if (!verifyB1) { + fail("failed signature verification"); + } + + + boolean verifyB2 = Crypto.DSA.verifySignature(publicKey2, bytes, signatureB); + + if (!verifyB2) { + fail("failed signature verification"); + } + } + +} diff --git a/Dorkbox-Util/test/dorkbox/util/crypto/EccTest.java b/Dorkbox-Util/test/dorkbox/util/crypto/EccTest.java new file mode 100644 index 0000000..715b971 --- /dev/null +++ b/Dorkbox-Util/test/dorkbox/util/crypto/EccTest.java @@ -0,0 +1,317 @@ + +package dorkbox.util.crypto; + + +import static org.junit.Assert.fail; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.Arrays; + +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.crypto.BasicAgreement; +import org.bouncycastle.crypto.CipherParameters; +import org.bouncycastle.crypto.agreement.ECDHCBasicAgreement; +import org.bouncycastle.crypto.engines.AESFastEngine; +import org.bouncycastle.crypto.engines.IESEngine; +import org.bouncycastle.crypto.modes.CBCBlockCipher; +import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.crypto.params.IESParameters; +import org.bouncycastle.crypto.params.IESWithCipherParameters; +import org.bouncycastle.crypto.params.ParametersWithRandom; +import org.bouncycastle.crypto.signers.ECDSASigner; +import org.bouncycastle.crypto.util.PrivateKeyFactory; +import org.bouncycastle.crypto.util.PublicKeyFactory; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jce.spec.ECParameterSpec; +import org.bouncycastle.util.encoders.Hex; +import org.junit.Test; + +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; + +import dorkbox.util.crypto.serialization.EccPrivateKeySerializer; +import dorkbox.util.crypto.serialization.EccPublicKeySerializer; +import dorkbox.util.crypto.serialization.IesParametersSerializer; +import dorkbox.util.crypto.serialization.IesWithCipherParametersSerializer; + + +public class EccTest { + + private static String entropySeed = "asdjhaffasttjasdasdgfgaerym0698768.,./8909087s0dfgkjgb49bmngrSGDSG#"; + + @Test + public void EccStreamMode() throws IOException { + SecureRandom secureRandom = new SecureRandom(); + + AsymmetricCipherKeyPair key1 = Crypto.ECC.generateKeyPair(Crypto.ECC.p521_curve, secureRandom); + AsymmetricCipherKeyPair key2 = Crypto.ECC.generateKeyPair(Crypto.ECC.p521_curve, secureRandom); + + IESParameters cipherParams = Crypto.ECC.generateSharedParameters(secureRandom); + + IESEngine encrypt = Crypto.ECC.createEngine(); + IESEngine decrypt = Crypto.ECC.createEngine(); + + + // note: we want an ecc key that is AT LEAST 512 bits! (which is equal to AES 256) + // using 521 bits from curve. + CipherParameters private1 = key1.getPrivate(); + CipherParameters public1 = key1.getPublic(); + + CipherParameters private2 = key2.getPrivate(); + CipherParameters public2 = key2.getPublic(); + + byte[] message = Hex.decode("123456784358754934597967249867359283792374987692348750276509765091834790abcdef123456784358754934597967249867359283792374987692348750276509765091834790abcdef123456784358754934597967249867359283792374987692348750276509765091834790abcdef"); + + // test stream mode + byte[] encrypted = Crypto.ECC.encrypt(encrypt, private1, public2, cipherParams, message); + byte[] plaintext = Crypto.ECC.decrypt(decrypt, private2, public1, cipherParams, encrypted); + + if (Arrays.equals(encrypted, message)) { + fail("stream cipher test failed"); + } + + if (!Arrays.equals(plaintext, message)) { + fail("stream cipher test failed"); + } + } + + @Test + public void EccAesMode() throws IOException { + // test AES encrypt mode + SecureRandom secureRandom = new SecureRandom(); + + AsymmetricCipherKeyPair key1 = Crypto.ECC.generateKeyPair(Crypto.ECC.p521_curve, secureRandom); + AsymmetricCipherKeyPair key2 = Crypto.ECC.generateKeyPair(Crypto.ECC.p521_curve, secureRandom); + + + PaddedBufferedBlockCipher aesEngine1 = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESFastEngine())); + PaddedBufferedBlockCipher aesEngine2 = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESFastEngine())); + + IESWithCipherParameters cipherParams = Crypto.ECC.generateSharedParametersWithCipher(secureRandom); + + + IESEngine encrypt = Crypto.ECC.createEngine(aesEngine1); + IESEngine decrypt = Crypto.ECC.createEngine(aesEngine2); + + + // note: we want an ecc key that is AT LEAST 512 bits! (which is equal to AES 256) + // using 521 bits from curve. + CipherParameters private1 = key1.getPrivate(); + CipherParameters public1 = key1.getPublic(); + + CipherParameters private2 = key2.getPrivate(); + CipherParameters public2 = key2.getPublic(); + + byte[] message = Hex.decode("123456784358754934597967249867359283792374987692348750276509765091834790abcdef123456784358754934597967249867359283792374987692348750276509765091834790abcdef123456784358754934597967249867359283792374987692348750276509765091834790abcdef"); + + // test stream mode + byte[] encrypted = Crypto.ECC.encrypt(encrypt, private1, public2, cipherParams, message); + byte[] plaintext = Crypto.ECC.decrypt(decrypt, private2, public1, cipherParams, encrypted); + + if (Arrays.equals(encrypted, message)) { + fail("stream cipher test failed"); + } + + if (!Arrays.equals(plaintext, message)) { + fail("stream cipher test failed"); + } + } + + @Test + public void Ecdh() throws IOException { + // test DH key exchange + SecureRandom secureRandom = new SecureRandom(); + + AsymmetricCipherKeyPair key1 = Crypto.ECC.generateKeyPair(Crypto.ECC.p521_curve, secureRandom); + AsymmetricCipherKeyPair key2 = Crypto.ECC.generateKeyPair(Crypto.ECC.p521_curve, secureRandom); + + BasicAgreement e1 = new ECDHCBasicAgreement(); + BasicAgreement e2 = new ECDHCBasicAgreement(); + + e1.init(key1.getPrivate()); + e2.init(key2.getPrivate()); + + BigInteger k1 = e1.calculateAgreement(key2.getPublic()); + BigInteger k2 = e2.calculateAgreement(key1.getPublic()); + + if (!k1.equals(k2)) { + fail("ECDHC cipher test failed"); + } + } + + @Test + public void EccDsa() throws IOException { + SecureRandom secureRandom = new SecureRandom(); + + AsymmetricCipherKeyPair key1 = Crypto.ECC.generateKeyPair(Crypto.ECC.p521_curve, secureRandom); + + ParametersWithRandom param = new ParametersWithRandom(key1.getPrivate(), new SecureRandom()); + + ECDSASigner ecdsa = new ECDSASigner(); + + ecdsa.init(true, param); + + byte[] message = new BigInteger("345234598734987394672039478602934578").toByteArray(); + BigInteger[] sig = ecdsa.generateSignature(message); + + + ecdsa.init(false, key1.getPublic()); + + if (!ecdsa.verifySignature(message, sig[0], sig[1])) { + fail("ECDSA signature fails"); + } + } + + @Test + public void EccSerialization() { + SecureRandom secureRandom = new SecureRandom(); + + AsymmetricCipherKeyPair key1 = Crypto.ECC.generateKeyPair(Crypto.ECC.p521_curve, secureRandom); + + IESParameters cipherAParams = Crypto.ECC.generateSharedParameters(secureRandom); + IESWithCipherParameters cipherBParams = Crypto.ECC.generateSharedParametersWithCipher(secureRandom); + + + // note: we want an ecc key that is AT LEAST 512 bits! (which is equal to AES 256) + // using 521 bits from curve. + ECPrivateKeyParameters private1 = (ECPrivateKeyParameters) key1.getPrivate(); + ECPublicKeyParameters public1 = (ECPublicKeyParameters) key1.getPublic(); + + + Kryo kryo = new Kryo(); + kryo.register(IESParameters.class, new IesParametersSerializer()); + kryo.register(IESWithCipherParameters.class, new IesWithCipherParametersSerializer()); + kryo.register(ECPublicKeyParameters.class, new EccPublicKeySerializer()); + kryo.register(ECPrivateKeyParameters.class, new EccPrivateKeySerializer()); + + + + // Test output to stream, large buffer. + ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + Output output = new Output(outStream, 4096); + kryo.writeClassAndObject(output, cipherAParams); + output.flush(); + + // Test input from stream, large buffer. + Input input = new Input(new ByteArrayInputStream(outStream.toByteArray()), 4096); + IESParameters cipherAParams2 = (IESParameters) kryo.readClassAndObject(input); + + + if (!Crypto.ECC.compare(cipherAParams, cipherAParams2)) { + fail("cipher parameters not equal"); + } + + // Test output to stream, large buffer. + outStream = new ByteArrayOutputStream(); + output = new Output(outStream, 4096); + kryo.writeClassAndObject(output, cipherBParams); + output.flush(); + + // Test input from stream, large buffer. + input = new Input(new ByteArrayInputStream(outStream.toByteArray()), 4096); + IESWithCipherParameters cipherBParams2 = (IESWithCipherParameters) kryo.readClassAndObject(input); + + if (!Crypto.ECC.compare(cipherBParams, cipherBParams2)) { + fail("cipher parameters not equal"); + } + + + // Test output to stream, large buffer. + outStream = new ByteArrayOutputStream(); + output = new Output(outStream, 4096); + kryo.writeClassAndObject(output, private1); + output.flush(); + + // Test input from stream, large buffer. + input = new Input(new ByteArrayInputStream(outStream.toByteArray()), 4096); + ECPrivateKeyParameters private2 = (ECPrivateKeyParameters) kryo.readClassAndObject(input); + + if (!Crypto.ECC.compare(private1, private2)) { + fail("private keys not equal"); + } + + + // Test output to stream, large buffer. + outStream = new ByteArrayOutputStream(); + output = new Output(outStream, 4096); + kryo.writeClassAndObject(output, public1); + output.flush(); + + // Test input from stream, large buffer. + input = new Input(new ByteArrayInputStream(outStream.toByteArray()), 4096); + ECPublicKeyParameters public2 = (ECPublicKeyParameters) kryo.readClassAndObject(input); + + if (!Crypto.ECC.compare(public1, public2)) { + fail("public keys not equal"); + } + } + + + @Test + public void EccJceSerialization() throws IOException { + AsymmetricCipherKeyPair generateKeyPair = Crypto.ECC.generateKeyPair(Crypto.ECC.p521_curve, new SecureRandom()); + ECPrivateKeyParameters privateKey = (ECPrivateKeyParameters) generateKeyPair.getPrivate(); + ECPublicKeyParameters publicKey = (ECPublicKeyParameters) generateKeyPair.getPublic(); + + + BCECPublicKey bcecPublicKey = new BCECPublicKey("EC", publicKey, (ECParameterSpec) null, BouncyCastleProvider.CONFIGURATION); + byte[] publicBytes = bcecPublicKey.getEncoded(); + + + + // relies on the BC public key. + BCECPrivateKey bcecPrivateKey = new BCECPrivateKey("EC", privateKey, bcecPublicKey, (ECParameterSpec) null, BouncyCastleProvider.CONFIGURATION); + byte[] privateBytes = bcecPrivateKey.getEncoded(); + + + + ECPublicKeyParameters publicKey2 = (ECPublicKeyParameters) PublicKeyFactory.createKey(publicBytes); + ECPrivateKeyParameters privateKey2 = (ECPrivateKeyParameters) PrivateKeyFactory.createKey(privateBytes); + + + + // test via signing + byte[] bytes = "hello, my name is inigo montoya".getBytes(); + + + BigInteger[] signature = Crypto.ECC.generateSignature("SHA384", privateKey, new SecureRandom(entropySeed.getBytes()), bytes); + + boolean verify1 = Crypto.ECC.verifySignature("SHA384", publicKey, bytes, signature); + + if (!verify1) { + fail("failed signature verification"); + } + + boolean verify2 = Crypto.ECC.verifySignature("SHA384", publicKey2, bytes, signature); + + if (!verify2) { + fail("failed signature verification"); + } + + + + // now reverse who signs what. + BigInteger[] signatureB = Crypto.ECC.generateSignature("SHA384", privateKey2, new SecureRandom(entropySeed.getBytes()), bytes); + + boolean verifyB1 = Crypto.ECC.verifySignature("SHA384", publicKey, bytes, signatureB); + + if (!verifyB1) { + fail("failed signature verification"); + } + + boolean verifyB2 = Crypto.ECC.verifySignature("SHA384", publicKey2, bytes, signatureB); + + if (!verifyB2) { + fail("failed signature verification"); + } + } +} diff --git a/Dorkbox-Util/test/dorkbox/util/crypto/RsaTest.java b/Dorkbox-Util/test/dorkbox/util/crypto/RsaTest.java new file mode 100644 index 0000000..c590432 --- /dev/null +++ b/Dorkbox-Util/test/dorkbox/util/crypto/RsaTest.java @@ -0,0 +1,127 @@ + +package dorkbox.util.crypto; + + +import static org.junit.Assert.fail; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.Arrays; + +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.crypto.digests.SHA1Digest; +import org.bouncycastle.crypto.encodings.OAEPEncoding; +import org.bouncycastle.crypto.engines.RSAEngine; +import org.bouncycastle.crypto.generators.RSAKeyPairGenerator; +import org.bouncycastle.crypto.params.RSAKeyGenerationParameters; +import org.bouncycastle.crypto.params.RSAKeyParameters; +import org.bouncycastle.crypto.params.RSAPrivateCrtKeyParameters; +import org.bouncycastle.crypto.signers.PSSSigner; +import org.junit.Test; + +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; + +import dorkbox.util.crypto.serialization.RsaPrivateKeySerializer; +import dorkbox.util.crypto.serialization.RsaPublicKeySerializer; + + +public class RsaTest { + private static String entropySeed = "asdjhaffasttjjhgpx600gn,-356268909087s0dfgkjh255124515hasdg87"; + + @SuppressWarnings("deprecation") + @Test + public void Rsa() { + byte[] bytes = "hello, my name is inigo montoya".getBytes(); + + AsymmetricCipherKeyPair key = Crypto.RSA.generateKeyPair(new SecureRandom(entropySeed.getBytes()), 1024); + + RSAKeyParameters public1 = (RSAKeyParameters) key.getPublic(); + RSAPrivateCrtKeyParameters private1 = (RSAPrivateCrtKeyParameters) key.getPrivate(); + + + RSAEngine engine = new RSAEngine(); + SHA1Digest digest = new SHA1Digest(); + OAEPEncoding rsaEngine = new OAEPEncoding(engine, digest); + + // test encrypt/decrypt + byte[] encryptRSA = Crypto.RSA.encrypt(rsaEngine, public1, bytes); + byte[] decryptRSA = Crypto.RSA.decrypt(rsaEngine, private1, encryptRSA); + + if (Arrays.equals(bytes, encryptRSA)) { + fail("bytes should not be equal"); + } + + if (!Arrays.equals(bytes, decryptRSA)) { + fail("bytes not equal"); + } + + // test signing/verification + PSSSigner signer = new PSSSigner(engine, digest, digest.getDigestSize()); + + byte[] signatureRSA = Crypto.RSA.sign(signer, private1, bytes); + boolean verify = Crypto.RSA.verify(signer, public1, signatureRSA, bytes); + + if (!verify) { + fail("failed signature verification"); + } + } + + + @SuppressWarnings("deprecation") + @Test + public void RsaSerialization () throws IOException { + RSAKeyPairGenerator keyGen = new RSAKeyPairGenerator(); + RSAKeyGenerationParameters params = new RSAKeyGenerationParameters(new BigInteger("65537"), // public exponent + new SecureRandom(entropySeed.getBytes()), //pnrg + 1024, // key length + 8); //the number of iterations of the Miller-Rabin primality test. + keyGen.init(params); + + + AsymmetricCipherKeyPair key = keyGen.generateKeyPair(); + + RSAKeyParameters public1 = (RSAKeyParameters) key.getPublic(); + RSAPrivateCrtKeyParameters private1 = (RSAPrivateCrtKeyParameters) key.getPrivate(); + + + Kryo kryo = new Kryo(); + kryo.register(RSAKeyParameters.class, new RsaPublicKeySerializer()); + kryo.register(RSAPrivateCrtKeyParameters.class, new RsaPrivateKeySerializer()); + + // Test output to stream, large buffer. + ByteArrayOutputStream outStream = new ByteArrayOutputStream(); + Output output = new Output(outStream, 4096); + kryo.writeClassAndObject(output, public1); + output.flush(); + + // Test input from stream, large buffer. + Input input = new Input(new ByteArrayInputStream(outStream.toByteArray()), 4096); + RSAKeyParameters public2 = (RSAKeyParameters) kryo.readClassAndObject(input); + + + if (!Crypto.RSA.compare(public1, public2)) { + fail("public keys not equal"); + } + + + // Test output to stream, large buffer. + outStream = new ByteArrayOutputStream(); + output = new Output(outStream, 4096); + kryo.writeClassAndObject(output, private1); + output.flush(); + + // Test input from stream, large buffer. + input = new Input(new ByteArrayInputStream(outStream.toByteArray()), 4096); + RSAPrivateCrtKeyParameters private2 = (RSAPrivateCrtKeyParameters) kryo.readClassAndObject(input); + + + if (!Crypto.RSA.compare(private1, private2)) { + fail("private keys not equal"); + } + } +} diff --git a/Dorkbox-Util/test/dorkbox/util/crypto/SCryptTest.java b/Dorkbox-Util/test/dorkbox/util/crypto/SCryptTest.java new file mode 100644 index 0000000..4bbe8e7 --- /dev/null +++ b/Dorkbox-Util/test/dorkbox/util/crypto/SCryptTest.java @@ -0,0 +1,59 @@ + +package dorkbox.util.crypto; + + + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; +import java.security.GeneralSecurityException; + +import org.junit.Test; + +import dorkbox.util.Sys; + + +public class SCryptTest { + + @Test + public void SCrypt() throws IOException, GeneralSecurityException { + + byte[] P, S; + int N, r, p, dkLen; + String DK; + + // empty key & salt test missing because unsupported by JCE + + P = "password".getBytes("UTF-8"); + S = "NaCl".getBytes("UTF-8"); + N = 1024; + r = 8; + p = 16; + dkLen = 64; + DK = "FDBABE1C9D3472007856E7190D01E9FE7C6AD7CBC8237830E77376634B3731622EAF30D92E22A3886FF109279D9830DAC727AFB94A83EE6D8360CBDFA2CC0640"; + + assertEquals(DK, Sys.bytesToHex(Crypto.SCrypt.encrypt(P, S, N, r, p, dkLen))); + + + P = "pleaseletmein".getBytes("UTF-8"); + S = "SodiumChloride".getBytes("UTF-8"); + N = 16384; + r = 8; + p = 1; + dkLen = 64; + DK = "7023BDCB3AFD7348461C06CD81FD38EBFDA8FBBA904F8E3EA9B543F6545DA1F2D5432955613F0FCF62D49705242A9AF9E61E85DC0D651E40DFCF017B45575887"; + + assertEquals(DK, Sys.bytesToHex(Crypto.SCrypt.encrypt(P, S, N, r, p, dkLen))); + + + P = "pleaseletmein".getBytes("UTF-8"); + S = "SodiumChloride".getBytes("UTF-8"); + N = 1048576; + r = 8; + p = 1; + dkLen = 64; + DK = "2101CB9B6A511AAEADDBBE09CF70F881EC568D574A2FFD4DABE5EE9820ADAA478E56FD8F4BA5D09FFA1C6D927C40F4C337304049E8A952FBCBF45C6FA77A41A4"; + + assertEquals(DK, Sys.bytesToHex(Crypto.SCrypt.encrypt(P, S, N, r, p, dkLen))); + } +} diff --git a/Dorkbox-Util/test/dorkbox/util/crypto/x509Test.java b/Dorkbox-Util/test/dorkbox/util/crypto/x509Test.java new file mode 100644 index 0000000..f7751f5 --- /dev/null +++ b/Dorkbox-Util/test/dorkbox/util/crypto/x509Test.java @@ -0,0 +1,155 @@ +package dorkbox.util.crypto; + +import static org.junit.Assert.fail; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.Calendar; +import java.util.Date; + +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.crypto.params.DSAPrivateKeyParameters; +import org.bouncycastle.crypto.params.DSAPublicKeyParameters; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.crypto.params.RSAKeyParameters; +import org.bouncycastle.crypto.params.RSAPrivateCrtKeyParameters; +import org.junit.Test; + + +public class x509Test { + + private static String entropySeed = "asdjhaffasdgfaasttjjhgpx600gn,-356268909087s0dfg4-42kjh255124515hasdg87"; + + @Test + public void EcdsaCertificate() throws IOException { + // create the certificate + Calendar expiry = Calendar.getInstance(); + expiry.add(Calendar.DAY_OF_YEAR, 360); + + Date startDate = new Date(); // time from which certificate is valid + Date expiryDate = expiry.getTime(); // time after which certificate is not valid + BigInteger serialNumber = BigInteger.valueOf(System.currentTimeMillis()); // serial number for certificate + + + AsymmetricCipherKeyPair generateKeyPair = Crypto.ECC.generateKeyPair(Crypto.ECC.p521_curve, new SecureRandom()); // key name from Crypto class + ECPrivateKeyParameters privateKey = (ECPrivateKeyParameters) generateKeyPair.getPrivate(); + ECPublicKeyParameters publicKey = (ECPublicKeyParameters) generateKeyPair.getPublic(); + + + + X509CertificateHolder ECDSAx509Certificate = CryptoX509.ECDSA.createCertHolder("SHA384", + startDate, expiryDate, + new X500Name("CN=Test"), new X500Name("CN=Test"), serialNumber, + privateKey, publicKey); + // make sure it's a valid cert. + if (ECDSAx509Certificate != null) { + boolean valid = CryptoX509.ECDSA.validate(ECDSAx509Certificate); + + if (!valid) { + fail("Unable to verify a x509 certificate."); + } + } else { + fail("Unable to create a x509 certificate."); + } + + // now sign something, then verify the signature. + byte[] data = "My keyboard is awesome".getBytes(); + byte[] signatureBlock = CryptoX509.createSignature(data, ECDSAx509Certificate, privateKey); + + boolean verifySignature = CryptoX509.ECDSA.verifySignature(signatureBlock, publicKey); + + if (!verifySignature) { + fail("Unable to verify a x509 certificate signature."); + } + } + + @Test + public void DsaCertificate() throws IOException { + // create the certificate + Calendar expiry = Calendar.getInstance(); + expiry.add(Calendar.DAY_OF_YEAR, 360); + + Date startDate = new Date(); // time from which certificate is valid + Date expiryDate = expiry.getTime(); // time after which certificate is not valid + BigInteger serialNumber = BigInteger.valueOf(System.currentTimeMillis()); // serial number for certificate + + + @SuppressWarnings("deprecation") + AsymmetricCipherKeyPair generateKeyPair = Crypto.DSA.generateKeyPair(new SecureRandom(entropySeed.getBytes()), 1024); + + + DSAPrivateKeyParameters privateKey = (DSAPrivateKeyParameters) generateKeyPair.getPrivate(); + DSAPublicKeyParameters publicKey = (DSAPublicKeyParameters) generateKeyPair.getPublic(); + + + + X509CertificateHolder DSAx509Certificate = CryptoX509.DSA.createCertHolder(startDate, expiryDate, + new X500Name("CN=Test"), new X500Name("CN=Test"), serialNumber, + privateKey, publicKey); + // make sure it's a valid cert. + if (DSAx509Certificate != null) { + boolean valid = CryptoX509.DSA.validate(DSAx509Certificate); + + if (!valid) { + fail("Unable to verify a x509 certificate."); + } + } else { + fail("Unable to create a x509 certificate."); + } + + // now sign something, then verify the signature. + byte[] data = "My keyboard is awesome".getBytes(); + byte[] signatureBlock = CryptoX509.createSignature(data, DSAx509Certificate, privateKey); + + boolean verifySignature = CryptoX509.DSA.verifySignature(signatureBlock, publicKey); + + if (!verifySignature) { + fail("Unable to verify a x509 certificate signature."); + } + } + + @Test + public void RsaCertificate() throws IOException { + // create the certificate + Calendar expiry = Calendar.getInstance(); + expiry.add(Calendar.DAY_OF_YEAR, 360); + + Date startDate = new Date(); // time from which certificate is valid + Date expiryDate = expiry.getTime(); // time after which certificate is not valid + BigInteger serialNumber = BigInteger.valueOf(System.currentTimeMillis()); // serial number for certificate + + @SuppressWarnings("deprecation") + AsymmetricCipherKeyPair generateKeyPair = Crypto.RSA.generateKeyPair(new SecureRandom(entropySeed.getBytes()), 1024); + RSAPrivateCrtKeyParameters privateKey = (RSAPrivateCrtKeyParameters) generateKeyPair.getPrivate(); + RSAKeyParameters publicKey = (RSAKeyParameters) generateKeyPair.getPublic(); + + + X509CertificateHolder RSAx509Certificate = CryptoX509.RSA.createCertHolder(startDate, expiryDate, + new X500Name("CN=Test"), new X500Name("CN=Test"), serialNumber, + privateKey, publicKey); + // make sure it's a valid cert. + if (RSAx509Certificate != null) { + boolean valid = CryptoX509.RSA.validate(RSAx509Certificate); + + if (!valid) { + fail("Unable to verify a x509 certificate."); + } + } else { + fail("Unable to create a x509 certificate."); + } + + // now sign something, then verify the signature. + byte[] data = "My keyboard is awesome".getBytes(); + byte[] signatureBlock = CryptoX509.createSignature(data, RSAx509Certificate, privateKey); + + boolean verifySignature = CryptoX509.RSA.verifySignature(signatureBlock, publicKey); + + if (!verifySignature) { + fail("Unable to verify a x509 certificate signature."); + } + } +}