dorkbox.util.crypto.Crypto Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of Utilities Show documentation
Show all versions of Utilities Show documentation
Utilities for use within Java projects
/*
* Copyright 2010 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.util.crypto;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.security.Security;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import javax.crypto.Cipher;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.PBEParametersGenerator;
import org.bouncycastle.crypto.digests.MD5Digest;
import org.bouncycastle.crypto.digests.SHA1Digest;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.digests.SHA512Digest;
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.lwjgl.util.xxhash.XXH32State;
import org.lwjgl.util.xxhash.XXHash;
import org.slf4j.Logger;
/**
* 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
*
*
* To determine if we have hardware accelerated AES java -XX:+PrintFlagsFinal -version | grep UseAES
*
* Per NIST SP800-38D,
* The total number of invocations of the authenticated encryption function shall not exceed 232, including all IV lengths and all instances of the authenticated encryption function with the given key.
*
*/
public final
class Crypto {
private
Crypto() {
}
// CUSTOM_HEADER USE
// check to see if our extra data is OURS. if so, process it
// cafeʞ, as UN signed bytes is: [254, 202, 202, 158], or as hex: FECA CA9E
// cafeʞ, as signed bytes is: [-2, -54, -54, -98]
private static final byte[] CUSTOM_HEADER = new byte[] {(byte) -2, (byte) -54, (byte) -54, (byte) -98};
public static
void addProvider() {
// make sure we only add it once (in case it's added elsewhere...)
Provider provider = Security.getProvider(BouncyCastleProvider.PROVIDER_NAME);
if (provider == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
/**
* Determines if cryptography restrictions apply.
* Restrictions apply if the value of {@link Cipher#getMaxAllowedKeyLength(String)} returns a value smaller than {@link Integer#MAX_VALUE} if there are any restrictions according to the JavaDoc of the method.
* This method is used with the transform "AES/CBC/PKCS5Padding"
as this is an often used algorithm that is an implementation requirement for Java SE.
*
* @return true
if restrictions apply, false
otherwise
*/
public static boolean restrictedCryptography() {
try {
return Cipher.getMaxAllowedKeyLength("AES/CBC/PKCS5Padding") < Integer.MAX_VALUE;
} catch (final NoSuchAlgorithmException e) {
throw new IllegalStateException("The transform \"AES/CBC/PKCS5Padding\" is not available (the availability of this algorithm is mandatory for Java SE implementations)", e);
}
}
public static
byte[] hashFileMD5(File file) {
MD5Digest digest = new MD5Digest();
return hashFile(file, digest, null);
}
public static
byte[] hashFileSHA1(File file) {
SHA1Digest digest = new SHA1Digest();
return hashFile(file, digest, null);
}
public static
byte[] hashFileSHA256(File file) {
SHA256Digest digest = new SHA256Digest();
return hashFile(file, digest, null);
}
public static
byte[] hashFileSHA512(File file) {
SHA512Digest digest = new SHA512Digest();
return hashFile(file, digest, null);
}
/**
* Return the hash of the file or NULL if file is invalid
*
* @param logger
* may be null, if no log output is necessary
*/
public static
byte[] hashFile(File file, Digest digest, Logger logger) {
return hashFile(file, digest, 0L, file.length(), logger);
}
/**
* Return the hash of the file or NULL if file is invalid
*
* @param logger
* may be null, if no log output is necessary
*/
public static
byte[] hashFile(File file, Digest digest, long startPosition, long endPosition, Logger logger) {
if (file.isFile() && file.canRead()) {
InputStream inputStream = null;
try {
inputStream = new FileInputStream(file);
long skip = inputStream.skip(startPosition);
if (skip != startPosition) {
throw new RuntimeException("Unable to skip " + startPosition + " bytes. Only skippped " + skip + " instead");
}
long size = file.length() - startPosition;
long lengthFromEnd = size - endPosition;
if (lengthFromEnd > 0 && lengthFromEnd < size) {
size -= lengthFromEnd;
}
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int readBytes;
digest.reset();
while (size > 0) {
//noinspection NumericCastThatLosesPrecision
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);
}
} catch (Exception e) {
if (logger != null) {
logger.error("Error hashing file: {}", file.getAbsolutePath(), e);
} else {
e.printStackTrace();
}
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
byte[] digestBytes = new byte[digest.getDigestSize()];
digest.doFinal(digestBytes, 0);
return digestBytes;
}
else {
return null;
}
}
/**
* Return the xxhash of the file as or 0 if file is invalid
*
* @param logger
* may be null, if no log output is necessary
*/
public static
int xxHashFile(File file, long lengthFromEnd, Logger logger) {
if (file.isFile() && file.canRead()) {
InputStream inputStream = null;
// used to initialize the hash value, use whatever value you want, but always the same
int seed = 0x9747b28c; // must match number in C (in Auth::xxHash32())
XXH32State state = XXHash.XXH32_createState();
XXHash.XXH32_reset(state, seed);
try {
inputStream = new FileInputStream(file);
long size = file.length();
if (lengthFromEnd > 0 && lengthFromEnd < size) {
size -= lengthFromEnd;
}
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
ByteBuffer bbuffer = ByteBuffer.wrap(buffer);
int readBytes;
while (size > 0) {
//noinspection NumericCastThatLosesPrecision
int maxToRead = (int) Math.min(bufferSize, size);
readBytes = inputStream.read(buffer, 0, maxToRead);
size -= readBytes;
if (readBytes == 0) {
//wtf. finally still gets called.
return 0;
}
bbuffer.limit(readBytes);
XXHash.XXH32_update(state, bbuffer);
}
} catch (Exception e) {
if (logger != null) {
logger.error("Error hashing file: {}", file.getAbsolutePath(), e);
} else {
e.printStackTrace();
}
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return XXHash.XXH32_digest(state);
}
else {
return 0;
}
}
static
int toInt(final byte[] bytes) {
int number = 0;
switch (bytes.length) {
default:
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;
}
/**
* Specifically, to return the hash of the ALL files/directories inside the jar, minus the action specified (LGPL) files.
*/
public static
byte[] hashJarContentsExcludeAction(File jarDestFilename, Digest digest, int action) throws IOException {
JarFile jarDestFile = new JarFile(jarDestFilename);
try {
Enumeration jarElements = jarDestFile.entries();
boolean okToHash;
boolean hasAction;
byte[] buffer = new byte[2048];
int read;
digest.reset();
while (jarElements.hasMoreElements()) {
JarEntry jarEntry = jarElements.nextElement();
String name = jarEntry.getName();
okToHash = !jarEntry.isDirectory();
if (!okToHash) {
continue;
}
// data with NO extra data will NOT BE HASHED
// data that matches our action bitmask WILL NOT BE HASHED
okToHash = false;
hasAction = false;
byte[] extraData = jarEntry.getExtra();
if (extraData == null || extraData.length == 0) {
okToHash = false;
}
else if (extraData.length >= 4) {
for (int i = 0; i < CUSTOM_HEADER.length; i++) {
if (extraData[i] != CUSTOM_HEADER[i]) {
throw new RuntimeException("Unexpected extra data in zip assigned. Aborting");
}
}
// this means we matched our header
if (extraData[4] > 0) {
hasAction = true;
// we have an ACTION describing how it was compressed, etc
int fileAction = toInt(new byte[] {extraData[5], extraData[6], extraData[7], extraData[8]});
if ((fileAction & action) != action) {
okToHash = true;
}
}
else {
okToHash = true;
}
}
else {
throw new RuntimeException("Unexpected extra data in zip assigned. Aborting");
}
// skips hashing lgpl files. (technically, whatever our action bitmask is...)
// we want to hash everything BY DEFAULT. we ALSO want to hash the NAME, LOAD ACTION TYPE, and the contents
if (okToHash) {
// System.err.println("HASHING: " + name);
// hash the file name
byte[] bytes = name.getBytes(StandardCharsets.US_ASCII);
digest.update(bytes, 0, bytes.length);
if (hasAction) {
// hash the action - since we don't want to permit anyone to change this after we sign the file
digest.update(extraData, 5, 4);
}
// hash the contents
InputStream inputStream = jarDestFile.getInputStream(jarEntry);
while ((read = inputStream.read(buffer)) > 0) {
digest.update(buffer, 0, read);
}
inputStream.close();
}
//else {
// System.err.println("Skipping: " + name);
//}
}
} catch (Exception e) {
throw new RuntimeException("Unexpected extra data in zip assigned. Aborting");
} finally {
jarDestFile.close();
}
byte[] digestBytes = new byte[digest.getDigestSize()];
digest.doFinal(digestBytes, 0);
return digestBytes;
}
/**
* 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;
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;
}
/**
* 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
byte[] PBKDF2(char[] password, byte[] salt, int iterationCount) {
// will also zero out the password.
byte[] charToBytes = Crypto.charToBytesPassword_UTF16(password);
return PBKDF2(charToBytes, salt, iterationCount);
}
/**
* Secure way to generate an AES key based on a password.
*
* @param password
* The password that you want to mix
* @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
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
byte[] charToBytesPassword_UTF16(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 < password.length; i++) {
//noinspection NumericCastThatLosesPrecision
passwordBytes[2 * i] = (byte) (((int) password[i] & 0xFF00) >> 8);
//noinspection NumericCastThatLosesPrecision
passwordBytes[2 * i + 1] = (byte) ((int) password[i] & 0x00FF);
}
// asterisk out the password
Arrays.fill(password, '*');
return passwordBytes;
}
}