Java tutorial
/** * Copyright 2017-2019 Nitor Creations Oy * * 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 io.nitor.api.backend.session; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import javax.crypto.Cipher; import javax.crypto.Mac; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.io.IOException; import java.io.InputStream; import java.nio.file.Path; import java.nio.file.Paths; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Arrays; import static io.nitor.api.backend.util.Helpers.toBytes; import static java.lang.Integer.toHexString; import static java.nio.file.Files.isReadable; import static java.nio.file.Files.readAllBytes; import static java.nio.file.Files.write; import static java.nio.file.StandardOpenOption.CREATE; import static java.nio.file.StandardOpenOption.DSYNC; import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; import static java.util.Arrays.copyOf; import static java.util.Arrays.copyOfRange; import static java.util.stream.Collectors.toList; import static javax.crypto.Cipher.DECRYPT_MODE; import static javax.crypto.Cipher.ENCRYPT_MODE; public class Encryptor { private static final Logger logger = LogManager.getLogger(Encryptor.class); private static final String CRYPTO_ALGORITHM = "AES/CBC/PKCS5Padding"; private static final String MAC_ALGORITHM = "HmacSHA512"; private static final int MAC_LENGTH = 64; private static final int IV_LENGTH = 16; private static final int AES_KEY_LENGTH = 32; private static final int AES_AUTH_LENGTH = 32; private static final ThreadLocal<CryptoState> CRYPTO_POOL = ThreadLocal.withInitial(Encryptor::buildCrypto); private final SecretKeySpec symmetricKey; private final SecretKeySpec hmacSecret; static { // create instance synchronously for eager failure buildCrypto(); } public Encryptor(JsonObject cryptConf) { try { Path secretFile = Paths.get(cryptConf.getString("secretFile", ".secret")); JsonArray secretGenerator = cryptConf.getJsonArray("secretGenerator"); byte[] secret; if (isReadable(secretFile)) { logger.info("Reading existing secret from " + secretFile); secret = readAllBytes(secretFile); if (secret.length != AES_KEY_LENGTH + AES_AUTH_LENGTH) { throw new RuntimeException("Corrupted secret file '" + secretFile.toAbsolutePath() + "'"); } } else if (secretGenerator != null) { logger.info("Generating a new secret using " + secretGenerator); secret = generateSecret(secretGenerator); if (secret.length < AES_KEY_LENGTH + AES_AUTH_LENGTH) { throw new RuntimeException("Too small secret generated"); } } else { logger.info("Generating a new secret and writing it to " + secretFile); secret = new byte[AES_KEY_LENGTH + AES_AUTH_LENGTH]; CRYPTO_POOL.get().random.nextBytes(secret); write(secretFile, secret, CREATE, TRUNCATE_EXISTING, DSYNC); } logger.info("Using secret " + toHexString(Arrays.hashCode(secret))); byte[] aesKey = copyOf(secret, AES_KEY_LENGTH); byte[] aesAuthData = copyOfRange(secret, AES_KEY_LENGTH, AES_KEY_LENGTH + AES_AUTH_LENGTH); if (aesKey.length != AES_KEY_LENGTH || aesAuthData.length != AES_AUTH_LENGTH) { throw new RuntimeException("Wrong length of key or auth data"); } this.symmetricKey = new SecretKeySpec(aesKey, "AES"); this.hmacSecret = new SecretKeySpec(aesAuthData, MAC_ALGORITHM); } catch (Exception e) { throw new RuntimeException("Could not create cipher", e); } } private byte[] generateSecret(JsonArray secretGenerator) { try { Process p = new ProcessBuilder() .command(secretGenerator.stream().map(Object::toString).collect(toList())) .redirectErrorStream(true).start(); byte[] inputData; try (InputStream in = p.getInputStream()) { inputData = toBytes(in); } p.destroy(); MessageDigest digest = MessageDigest.getInstance("SHA-512"); return digest.digest(inputData); } catch (IOException | NoSuchAlgorithmException e) { throw new RuntimeException("Failed to execute secret generator", e); } } public byte[] decrypt(byte[] crypted) { try { CryptoState crypto = CRYPTO_POOL.get(); crypto.hmac.init(hmacSecret); crypto.hmac.update(crypted, MAC_LENGTH, crypted.length - MAC_LENGTH); if (!MessageDigest.isEqual(crypto.hmac.doFinal(), copyOf(crypted, MAC_LENGTH))) { throw new Exception("Invalid mac"); } crypto.cipher.init(DECRYPT_MODE, symmetricKey, new IvParameterSpec(crypted, MAC_LENGTH, IV_LENGTH)); return crypto.cipher.doFinal(crypted, MAC_LENGTH + IV_LENGTH, crypted.length - MAC_LENGTH - IV_LENGTH); } catch (Exception e) { throw new RuntimeException("Decrypt failed"); // drop the root cause } } public byte[] encrypt(byte[] data) { try { CryptoState crypto = CRYPTO_POOL.get(); byte[] iv = new byte[IV_LENGTH]; crypto.random.nextBytes(iv); crypto.cipher.init(ENCRYPT_MODE, symmetricKey, new IvParameterSpec(iv)); byte[] ciptertext = crypto.cipher.doFinal(data); crypto.hmac.init(hmacSecret); crypto.hmac.update(iv); byte[] mac = crypto.hmac.doFinal(ciptertext); byte[] encrypted = new byte[mac.length + iv.length + ciptertext.length]; System.arraycopy(mac, 0, encrypted, 0, mac.length); System.arraycopy(iv, 0, encrypted, mac.length, iv.length); System.arraycopy(ciptertext, 0, encrypted, mac.length + iv.length, ciptertext.length); return encrypted; } catch (Exception e) { throw new RuntimeException("Encrypt failed"); // drop the root cause } } static CryptoState buildCrypto() { try { return new CryptoState(Cipher.getInstance(CRYPTO_ALGORITHM), Mac.getInstance(MAC_ALGORITHM), new SecureRandom()); } catch (Exception e) { throw new RuntimeException(e); } } static class CryptoState { final Cipher cipher; final Mac hmac; final SecureRandom random; CryptoState(Cipher cipher, Mac hmac, SecureRandom random) { this.cipher = cipher; this.hmac = hmac; this.random = random; } } }