eds-core/src/main/java/net/haugr/eds/core/jce/Crypto.java
/*
* EDS, Encrypted Data Share - open source Cryptographic Sharing system.
* Copyright (c) 2016-2024, haugr.net
* mailto: eds AT haugr DOT net
*
* EDS is free software; you can redistribute it and/or modify it under the
* terms of the Apache License, as published by the Apache Software Foundation.
*
* EDS is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the Apache License for more details.
*
* You should have received a copy of the Apache License, version 2, along with
* this program; If not, you can download a copy of the License
* here: https://www.apache.org/licenses/
*/
package net.haugr.eds.core.jce;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import net.haugr.eds.api.common.Constants;
import net.haugr.eds.core.enums.KeyAlgorithm;
import net.haugr.eds.core.exceptions.CryptoException;
import net.haugr.eds.core.model.Settings;
/**
* <p>This library contain all the Cryptographic Operations, needed for EDS, to
* support the features required. JCA, Java Cryptography Architecture, contains
* all the features needed, and is flexible enough, that it can be extended by
* providing different vendors - which will then allow using stronger encryption
* if needed.</p>
*
* <p>EDS uses two (three) types of Encryption. Symmetric Encryption of all the
* actual Data to be shared and Asymmetric Encryption to storing the Symmetric
* keys. Additionally, all Private Key are be stored encrypted, and a Key is
* derived (using PBKDF2) from the Credentials to unlock it.</p>
*
* <p>The default Algorithms and Key sizes have been chosen, so they will work
* with a standard Java 8 (build 161+) installation, these uses the maximum key
* size allowed, if an older Java is used, then either install the Java 8
* Security extension from Oracle, or change the default configuration of EDS
* accordingly.</p>
*
* <p>Although Cryptography is the cornerstone of the EDS, there is no attempts
* made towards creating or inventing various Algorithms. The risk of making
* mistakes is too high. Instead, the EDS relies on the wisdom and maturity of
* existing JCE implementations in Java.</p>
*
* @author Kim Jensen
* @since EDS 1.0
*/
public final class Crypto {
private final MasterKey masterKey;
private final Settings settings;
public Crypto(final Settings settings) {
masterKey = MasterKey.getInstance(settings);
this.settings = settings;
}
// =========================================================================
// Public Methods to generate Keys
// =========================================================================
/**
* <p>Converts the given Salted Password to a Key, which can be used for the
* initial Cryptographic Operations. With the help of the PBKDF2 algorithm,
* it creates a symmetric Key over 'n' iterations, where 'n' is configurable
* as it may be required to have stronger checks. However, for the Key to be
* of a good enough Quality, it should be having a length of at least 16
* characters and the same applies to the Salt.</p>
*
* @param algorithm PBE Algorithm to generate Account Symmetric Key
* @param secret Provided Passphrase or Secret
* @param salt System specific Salt
* @return Symmetric Key
* @throws CryptoException if an error occurred
*/
public SecretEDSKey generatePasswordKey(final KeyAlgorithm algorithm, final byte[] secret, final String salt) {
try {
final char[] extendedSecret = convertSecret(secret);
final byte[] secretSalt = stringToBytes(salt);
final SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm.getTransformationValue());
final KeySpec spec = new PBEKeySpec(extendedSecret, secretSalt, settings.getPasswordIterations(), algorithm.getLength());
final SecretKey tmpKey = factory.generateSecret(spec);
final SecretKey secretKey = new SecretKeySpec(tmpKey.getEncoded(), algorithm.getName());
final SecretEDSKey key = new SecretEDSKey(algorithm.getDerived(), secretKey);
key.setSalt(new IVSalt(salt));
return key;
} catch (IllegalArgumentException | NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new CryptoException(e.getMessage(), e);
}
}
/**
* <p>Converting the given secret (byte array) into a char array with the
* salt from the system appended without using the String Object is not
* trivial.</p>
*
* <p>From <a href="https://stackoverflow.com/a/9855338">Stackoverflow</a>,
* it is clear that the solution can be complex, but as all which is needed
* here is a way to convert the bytes to chars so a Key can be generated, a
* simpler conversion may be sufficient.</p>
*
* @param secret Provided Passphrase or Secret
* @return Extended secret as char array
*/
private char[] convertSecret(final byte[] secret) {
final char[] secretChars = MasterKey.generateSecretChars(secret);
final char[] salt = settings.getSalt().toCharArray();
final char[] chars = new char[secretChars.length + salt.length];
System.arraycopy(secretChars, 0, chars, 0, secretChars.length);
System.arraycopy(salt, 0, chars, secretChars.length, salt.length);
return chars;
}
public static SecretEDSKey generateSymmetricKey(final KeyAlgorithm algorithm) {
try {
final KeyGenerator generator = KeyGenerator.getInstance(algorithm.getName(), algorithm.getProvider());
generator.init(algorithm.getLength());
final SecretKey key = generator.generateKey();
return new SecretEDSKey(algorithm, key);
} catch (IllegalArgumentException | NoSuchAlgorithmException | NoSuchProviderException e) {
throw new CryptoException(e.getMessage(), e);
}
}
public static EDSKeyPair generateAsymmetricKey(final KeyAlgorithm algorithm) {
try {
final KeyPairGenerator generator = KeyPairGenerator.getInstance(algorithm.getName());
generator.initialize(algorithm.getLength());
final KeyPair keyPair = generator.generateKeyPair();
return new EDSKeyPair(algorithm, keyPair);
} catch (IllegalArgumentException | NoSuchAlgorithmException e) {
throw new CryptoException(e.getMessage(), e);
}
}
public String generateChecksum(final byte[] bytes) {
try {
final MessageDigest digest = MessageDigest.getInstance(settings.getHashAlgorithm().getAlgorithm());
final byte[] hashed = digest.digest(bytes);
return Base64.getEncoder().encodeToString(hashed);
} catch (IllegalArgumentException | NoSuchAlgorithmException e) {
throw new CryptoException(e.getMessage(), e);
}
}
// =========================================================================
// Standard Cryptographic Operations; Sign, Verify, Encrypt & Decrypt
// =========================================================================
public byte[] sign(final PrivateKey key, final byte[] message) {
try {
final Signature signer = Signature.getInstance(settings.getSignatureAlgorithm().getTransformationValue());
signer.initSign(key);
signer.update(message);
return signer.sign();
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
throw new CryptoException(e.getMessage(), e);
}
}
public boolean verify(final PublicKey key, final byte[] message, final byte[] signature) {
try {
final Signature verifier = Signature.getInstance(settings.getSignatureAlgorithm().getTransformationValue());
verifier.initVerify(key);
verifier.update(message);
return verifier.verify(signature);
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException | IllegalArgumentException e) {
throw new CryptoException(e.getMessage(), e);
}
}
public byte[] encryptWithMasterKey(final byte[] toEncrypt) {
return encrypt(masterKey.getKey(), toEncrypt);
}
public String encryptWithMasterKey(final String toEncrypt) {
final byte[] bytes = stringToBytes(toEncrypt);
final byte[] encrypted = encryptWithMasterKey(bytes);
return Base64.getEncoder().encodeToString(encrypted);
}
public String decryptWithMasterKey(final String toDecrypt) {
final byte[] encrypted = Base64.getDecoder().decode(toDecrypt);
final byte[] decrypted = decrypt(masterKey.getKey(), encrypted);
return bytesToString(decrypted);
}
public static byte[] encrypt(final SecretEDSKey key, final byte[] toEncrypt) {
try {
final Cipher cipher = prepareCipher(key, Cipher.ENCRYPT_MODE);
return cipher.doFinal(toEncrypt);
} catch (BadPaddingException | IllegalBlockSizeException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidAlgorithmParameterException | InvalidKeyException e) {
throw new CryptoException(e.getMessage(), e);
}
}
public static byte[] encrypt(final PublicEDSKey key, final byte[] toEncrypt) {
try {
final Cipher cipher = prepareCipher(key, Cipher.ENCRYPT_MODE);
return cipher.doFinal(toEncrypt);
} catch (ClassCastException | BadPaddingException | IllegalBlockSizeException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidAlgorithmParameterException | InvalidKeyException e) {
throw new CryptoException(e.getMessage(), e);
}
}
public static byte[] decrypt(final SecretEDSKey key, final byte[] toDecrypt) {
try {
final Cipher cipher = prepareCipher(key, Cipher.DECRYPT_MODE);
return cipher.doFinal(toDecrypt);
} catch (BadPaddingException | IllegalBlockSizeException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidAlgorithmParameterException | InvalidKeyException e) {
throw new CryptoException(e.getMessage(), e);
}
}
public static byte[] decrypt(final PrivateEDSKey key, final byte[] toDecrypt) {
try {
final Cipher cipher = prepareCipher(key, Cipher.DECRYPT_MODE);
return cipher.doFinal(toDecrypt);
} catch (ClassCastException | BadPaddingException | IllegalBlockSizeException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidAlgorithmParameterException | InvalidKeyException e) {
throw new CryptoException(e.getMessage(), e);
}
}
private static Cipher prepareCipher(final AbstractEDSKey<?> key, final int type) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException {
AlgorithmParameterSpec iv = null;
final String instanceName;
if (key.getAlgorithm().getType() == KeyAlgorithm.Type.ASYMMETRIC) {
instanceName = key.getAlgorithm().getName();
} else if (key.getAlgorithm().getType() == KeyAlgorithm.Type.SYMMETRIC) {
final KeyAlgorithm algorithm = key.getAlgorithm();
instanceName = algorithm.getTransformationValue();
iv = switch (key.getAlgorithm().getTransformation()) {
case AES_CBC ->
// SonarQube rule S3329 (http://localhost:9000/coding_rules?open=squid:S3329&rule_key=squid:S3329)
// is marking this place as a vulnerability, as it cannot
// ascertain that the salt is generated randomly using
// SecureRandom, and also stored armored in the database.
// As the same salt must be used for both encryption and
// decryption - the rule is simply not good enough.
new IvParameterSpec(((SecretEDSKey) key).getSalt().getBytes());
case AES_GCM_128, AES_GCM_192, AES_GCM_256 ->
new GCMParameterSpec(Constants.GCM_IV_LENGTH, ((SecretEDSKey) key).getSalt().getBytes());
default ->
// Unreachable Code by design, only 2 AES transformation
// Algorithms exists, and they are both checked.
throw new CryptoException("Cannot prepare Cipher for this Symmetric Algorithm " + key.getAlgorithm().getTransformation() + '.');
};
} else {
throw new CryptoException("Cannot prepare Cipher for this Algorithm Type " + key.getAlgorithm().getType() + '.');
}
final Cipher cipher;
cipher = Cipher.getInstance(instanceName);
cipher.init(type, key.getKey(), iv);
return cipher;
}
// =========================================================================
// Key Protection, Encrypting & Armoring - De-armoring & Decrypting Keys
// =========================================================================
/**
* The Public RSA Key stored in EDS, is simply saved in x.509 format, stored
* Base64 encoded.
*
* @param key Public RSA key to armor (Base64 encoded x.509 Key)
* @return String representation of the Key
*/
public static String armoringPublicKey(final Key key) {
final X509EncodedKeySpec keySpec = new X509EncodedKeySpec(key.getEncoded());
final byte[] rawKey = keySpec.getEncoded();
return Base64.getEncoder().encodeToString(rawKey);
}
public PublicKey dearmoringPublicKey(final String armoredKey) {
try {
final KeyAlgorithm algorithm = settings.getAsymmetricAlgorithm();
final KeyFactory keyFactory = KeyFactory.getInstance(algorithm.getName());
final byte[] rawKey = Base64.getDecoder().decode(armoredKey);
final KeySpec x509KeySpec = new X509EncodedKeySpec(rawKey);
return keyFactory.generatePublic(x509KeySpec);
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
throw new CryptoException(e.getMessage(), e);
}
}
/**
* The Private RSA Key stored in EDS, is stored encrypted, so it cannot be
* extracted without some effort. To do this, a Key is needed, together with
* a Salt which the Initial Vector is based on. The encrypted Key, is then
* converted into PKCS8 and converted using Base64 encoding. The result of
* this will make the key safe for storage in the database.
*
* @param encryptionKey Symmetric Key to encrypt the Private RSA Key with
* @param privateKey The Private RSA Key to encrypt and armor
* @return Armored (PKCS8 and Base64 encoded encrypted key)
*/
public static String encryptAndArmorPrivateKey(final SecretEDSKey encryptionKey, final Key privateKey) {
final PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKey.getEncoded());
final byte[] rawKey = keySpec.getEncoded();
final byte[] encryptedKey = encrypt(encryptionKey, rawKey);
return Base64.getEncoder().encodeToString(encryptedKey);
}
public PrivateKey dearmoringPrivateKey(final SecretEDSKey decryptionKey, final String armoredKey) {
try {
// We only need the name of the Asymmetric Algorithm here, not the
// keySize. As all Asymmetric Algorithms share the same basic
// algorithm and thus name, we can use the Asymmetric Algorithm
// from the Settings.
final KeyAlgorithm algorithm = settings.getAsymmetricAlgorithm();
final KeyFactory keyFactory = KeyFactory.getInstance(algorithm.getName());
final byte[] dearmored = Base64.getDecoder().decode(armoredKey);
final byte[] rawKey = decrypt(decryptionKey, dearmored);
final KeySpec keySpec = new PKCS8EncodedKeySpec(rawKey);
return keyFactory.generatePrivate(keySpec);
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new CryptoException(e.getMessage(), e);
}
}
public static String encryptAndArmorCircleKey(final PublicEDSKey publicKey, final SecretEDSKey circleKey) {
final byte[] encryptedCircleKey = encrypt(publicKey, circleKey.getEncoded());
return Base64.getEncoder().encodeToString(encryptedCircleKey);
}
public static SecretEDSKey extractCircleKey(final KeyAlgorithm algorithm, final PrivateEDSKey privateKey, final String armoredCircleKey) {
final byte[] dearmoredCircleKey = Base64.getDecoder().decode(armoredCircleKey);
final byte[] decryptedCircleKey = decrypt(privateKey, dearmoredCircleKey);
final SecretKey key = new SecretKeySpec(decryptedCircleKey, algorithm.getName());
return new SecretEDSKey(algorithm, key);
}
/**
* <p>The RSA KeyPair for each Member Account, is stored with an encrypted
* Private Key and armored and the Public Key armored. This way, it is easy
* to verify that the Key's are correctly stored as they are stored purely
* as text and nothing else.</p>
*
* <p>To recreate the Key Pair the Private Key has to be decrypted and then
* both the Public and Private Keys must be converted.</p>
*
* @param algorithm EDS Key Algorithm
* @param key Symmetric Key to decrypt the Private Key with
* @param salt Base for the Initial Vector, used for decrypting
* @param armoredPublicKey Armored unencrypted Public Key
* @param armoredPrivateKey Armored and encrypted Private Key
* @return RSA KeyPair with the Public and Private Keys
* @throws CryptoException if an error occurred
*/
public EDSKeyPair extractAsymmetricKey(final KeyAlgorithm algorithm, final SecretEDSKey key, final String salt, final String armoredPublicKey, final String armoredPrivateKey) {
key.setSalt(new IVSalt(salt));
// Extracting the Public & Private Keys
final PublicKey publicKey = dearmoringPublicKey(armoredPublicKey);
final PrivateKey privateKey = dearmoringPrivateKey(key, armoredPrivateKey);
// Build the EDSKeyPair
final KeyPair keyPair = new KeyPair(publicKey, privateKey);
return new EDSKeyPair(algorithm, keyPair);
}
public byte[] stringToBytes(final String string) {
return string.getBytes(settings.getCharset());
}
public String bytesToString(final byte[] bytes) {
return new String(bytes, settings.getCharset());
}
}