Java tutorial
/* * Copyright (c) 2016, LamaCrypt * All rights reserved. * * The LamaCrypt client software and its source code are available * under the LamaCrypt Software License: * https://github.com/LamaCrypt/desktop-client/blob/master/LICENSE.md */ package ch.lamacrypt.internal.crypto; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.nio.file.Files; import java.security.MessageDigest; import java.security.SecureRandom; import java.util.Arrays; import javax.crypto.SecretKey; import javax.xml.bind.DatatypeConverter; import org.bouncycastle.crypto.generators.SCrypt; import org.bouncycastle.util.encoders.Hex; /** * General purpose cryptographic class * <p> * Methods which are used for cryptographic yet general purpose are defined * here, so that the *Cipher class files only contains * encryption/decryption-related code. * * @author LamaGuy */ public abstract class GPCrypto { private static final SecureRandom rand = new SecureRandom(); private static final int KDF_N = (int) Math.pow(2, 22), KDF_r = 8, KDF_p = 1; public static final int SANITIZATION_COUNT = 10000; /** * Generates a random array of bytes * * @param size width of the byte array * @return a random array of bytes, of length size */ public static byte[] randomGen(int size) { byte[] randBytes = new byte[size]; rand.nextBytes(randBytes); return randBytes; } /** * Fills a byte array 10'000 times with random values to prevent future * retrieval of its original state * * @param array byte array to sanitize */ public static void sanitize(byte[] array, int iterCnt) { for (int i = 0; i < iterCnt; i++) { rand.nextBytes(array); } } /** * Fills a char array 10'000 times with random values to prevent future * retrieval of its original state * * @param c char array to sanitize */ public static void sanitize(char[] c) { for (int i = 0; i < SANITIZATION_COUNT; i++) { Arrays.fill(c, (char) rand.nextInt()); } } /** * Overwrites a file with random bytes to prevent future retrieval of its * original state * * @param input file to sanitize * @param passCount number of passes to make * @throws Exception */ public static void sanitize(File input, int passCount) throws Exception { final FileOutputStream fos = new FileOutputStream(input); if (input.length() > Math.pow(2, 20)) { final FileInputStream fis = new FileInputStream(input); byte[] buffer = new byte[1024]; int r; while ((r = fis.read(buffer)) > 0) { fos.write(buffer, 0, r); } fis.close(); } else { for (int i = 0; i < passCount; i++) { rand.nextBytes(Files.readAllBytes(input.toPath())); } } fos.close(); } /** * Overwrites many byte arrays 10'000 times * * @param arrays arrays to overwrite */ public static void eraseByteArrays(byte[]... arrays) { for (byte[] array : arrays) { sanitize(array, SANITIZATION_COUNT); } } /** * Overwrites many SecretKey objects 10'000 times * * @param keys SecretKey objects to overwrite */ public static void eraseKeys(SecretKey... keys) { for (SecretKey key : keys) { GPCrypto.sanitize(key.getEncoded(), SANITIZATION_COUNT); } } /** * Hashes a given string with SHA384 * <p> * The input is read as a byte array, using UTF-8 as character encoding. * * @param input message * @return message digest * @throws Exception */ public static byte[] SHA384(String input) throws Exception { MessageDigest md = MessageDigest.getInstance("SHA384", "BC"); return md.digest(input.getBytes("UTF-8")); } /** * Derives a given string (usually a password) with scrypt, using the * following default values: * <ul> * <li>N=2^19</li> * <li>p=8</li> * <li>r=4</li> * </ul> * * @param password secret value to derive the key from * @param salt salt to use for this invocation * @param dkLen output size, in bytes * @return key derived from supplied password * @throws java.io.UnsupportedEncodingException */ public static byte[] scrypt(char[] password, byte[] salt, int dkLen) throws UnsupportedEncodingException { byte[] passBytes = charToByte(password), digest = SCrypt.generate(passBytes, salt, KDF_N, KDF_r, KDF_p, dkLen); sanitize(passBytes, SANITIZATION_COUNT); return digest; } /** * Converts a char array into a byte array, using UTF-8 as the encoding * charset * <p> * Note: does not change the input char array by processing a clone of it * * * @param c char array to convert * @return byte array representation of the char array */ public static byte[] charToByte(char[] c) { CharBuffer charBuffer = CharBuffer.wrap(c.clone()); ByteBuffer byteBuffer = Charset.forName("UTF-8").encode(charBuffer); byte[] bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit()); sanitize(charBuffer.array()); sanitize(byteBuffer.array(), SANITIZATION_COUNT); return bytes; } /** * Retrieves the SCrypt parameters from a formatted digest in the following * format: * <p> * N$r$p$salt$digest * * @param digest * @return the components of the SCrypt digest: * <ol> * <li>hash</li> * <li>salt</li> * <li>N</li> * <li>r</li> * <li>p</li> * <li>hash size, in bytes</li> * </ol> */ public static Object[] decodeDigest(String digest) { String tmp; int N = Integer.parseInt(digest.substring(0, digest.indexOf("$"))); tmp = digest.substring(digest.indexOf("$") + 1, digest.length()); int r = Integer.parseInt(tmp.substring(0, tmp.indexOf("$"))); tmp = tmp.substring(tmp.indexOf("$") + 1, tmp.length()); int p = Integer.parseInt(tmp.substring(0, tmp.indexOf("$"))); tmp = tmp.substring(tmp.indexOf("$") + 1, tmp.length()); String salt = tmp.substring(0, tmp.indexOf("$")); tmp = tmp.substring(tmp.indexOf("$") + 1, tmp.length()); return new Object[] { tmp, salt, N, r, p, tmp.length() / 2 }; } /** * Formats the hash in the following format: * <p> * N$r$p$salt$digest * * @param hash * @param salt * * @return * * @throws UnsupportedEncodingException */ public static String encodeDigest(byte[] hash, byte[] salt) throws UnsupportedEncodingException { return KDF_N + "$" + KDF_r + "$" + KDF_p + "$" + Hex.toHexString(salt) + "$" + Hex.toHexString(hash); } /** * Derives a key, i.e. a hash, from a given password, using the following * scrypt parameters: * <ul> * <li>salt=random 32B</li> * <li>N=2^19</li> * <li>r=8</li> * <li>p=4</li> * * </ul> * * @param pass * @return * @throws UnsupportedEncodingException */ public static String deriveKey(char[] pass) throws UnsupportedEncodingException { byte[] salt = GPCrypto.randomGen(32); return encodeDigest(scrypt(pass, salt, 32), salt); } /** * Converts a byte array into an integer * * @param b byte array to convert * @return integer value of the byte array */ public static int byteArrayToInt(byte[] b) { int value = 0; for (int i = 0; i < 4; i++) { int shift = (4 - 1 - i) * 8; value += (b[i] & 0x000000FF) << shift; } return value; } /** * Converts an integer into a byte array * * @param i integer to convert * @return byte array representation of the integer */ public static byte[] intToByteArray(int i) { byte[] ret = new byte[4]; ret[3] = (byte) (i & 0xFF); ret[2] = (byte) ((i >> 8) & 0xFF); ret[1] = (byte) ((i >> 16) & 0xFF); ret[0] = (byte) ((i >> 24) & 0xFF); return ret; } /** * Checks whether the specified string is a string representation of an * hexadecimal value * * @param hex hexadecimal string * @return */ public static boolean checkHex(String hex) { try { DatatypeConverter.parseHexBinary(hex); } catch (IllegalArgumentException ex) { return false; } return true; } /** * Checks whether the specified string is a representation of a 256-bit * secret key * * @param key string representation of a 256-bit secret key * @return true the string represents a 32 byte hex value */ public static boolean checkKey(String key) { return checkHex(key) && key.length() == 64; } /** * Checks whether the specified string is a representation of a 16 byte uuid * * @param uuid string representation of a 16 byte uuid * @return true the string represents a 16 byte hex value */ public static boolean checkUUID(String uuid) { return checkHex(uuid) && uuid.length() == 32; } /** * * Compares two byte arrays in constant time to prevent timing attacks * * @param a array a * @param b array b * @return {@code true} if the arrays are equal, else {@code false} */ public static boolean isEqual(byte[] a, byte[] b) { if (a.length != b.length) { return false; } int result = 0; for (int i = 0; i < a.length; i++) { result |= a[i] ^ b[i]; } return result == 0; } }