CryptoUtils.java
- /*
- * CryptoUtils
- *
- * $Id$
- * $URL$
- */
- package gov.usgs.util;
- import java.io.ByteArrayInputStream;
- import java.io.ByteArrayOutputStream;
- import java.io.IOException;
- import java.io.InputStream;
- import java.io.OutputStream;
- import java.math.BigInteger;
- import java.nio.ByteBuffer;
- import java.security.Key;
- import java.security.PublicKey;
- import java.security.PrivateKey;
- import java.security.cert.Certificate;
- import java.security.cert.CertificateException;
- import java.security.cert.CertificateFactory;
- import java.security.interfaces.DSAPrivateKey;
- import java.security.interfaces.DSAPublicKey;
- import java.security.interfaces.RSAPrivateKey;
- import java.security.interfaces.RSAPublicKey;
- import java.security.spec.DSAPublicKeySpec;
- import java.security.spec.DSAPrivateKeySpec;
- import java.security.spec.InvalidKeySpecException;
- import java.security.spec.MGF1ParameterSpec;
- import java.security.spec.PKCS8EncodedKeySpec;
- import java.security.spec.PSSParameterSpec;
- import java.security.spec.RSAPublicKeySpec;
- import java.security.spec.RSAPrivateKeySpec;
- import java.security.spec.X509EncodedKeySpec;
- import java.security.KeyFactory;
- import java.security.KeyPair;
- import java.security.KeyPairGenerator;
- import java.security.Signature;
- import java.security.SignatureException;
- import java.security.InvalidAlgorithmParameterException;
- import java.security.InvalidKeyException;
- import java.security.NoSuchAlgorithmException;
- import java.util.Base64;
- import java.util.LinkedList;
- import java.util.List;
- import javax.crypto.Cipher;
- import javax.crypto.CipherOutputStream;
- import javax.crypto.KeyGenerator;
- import javax.crypto.NoSuchPaddingException;
- import ch.ethz.ssh2.crypto.PEMDecoder;
- /**
- * Encryption and signing utilities.
- */
- public class CryptoUtils {
- /** Algorithm used by AES keys and ciphers. */
- public static final String AES_ALGORITHM = "AES";
- /** Number of bits for AES 128 bit key. */
- public static final int AES_128 = 128;
- /** Number of bits for AES 256 bit key. */
- public static final int AES_256 = 256;
- /** Algorithm used by DSA keys. */
- public static final String DSA_ALGORITHM = "DSA";
- /** Algorithm used for signature with DSA key. */
- public static final String DSA_SIGNATURE_ALGORITHM = "SHA1withDSA";
- /** Number of bits for DSA 1024 bit key. */
- public static final int DSA_1024 = 1024;
- /** Algorithm used by RSA keys and ciphers. */
- public static final String RSA_ALGORITHM = "RSA";
- /** Algorithm used for signature with RSA key. */
- public static final String RSA_SIGNATURE_ALGORITHM = "SHA1withRSA";
- /** Number of bits for RSA 2048 bit key. */
- public static final int RSA_2048 = 2048;
- /** Number of bits for RSA 4096 bit key. */
- public static final int RSA_4096 = 4096;
- /** Signature versions. */
- public enum Version {
- /** Signature enum for v1 */
- SIGNATURE_V1("v1"),
- /** Signature enum for v2 */
- SIGNATURE_V2("v2");
- private String value;
- Version(final String value) {
- this.value = value;
- }
- public String toString() {
- return this.value;
- }
- /**
- * @param value to get a signature from
- * @return a version
- * @throws IllegalArgumentException if unknown version.
- */
- public static Version fromString(final String value) {
- if (SIGNATURE_V1.value.equals(value)) {
- return SIGNATURE_V1;
- } else if (SIGNATURE_V2.value.equals(value)) {
- return SIGNATURE_V2;
- } else {
- throw new IllegalArgumentException("Invalid signature version");
- }
- }
- }
- /** v2 Algorithm for DSA signature */
- public static final String SIGNATURE_V2_DSA_ALGORITHM = "SHA256withDSA";
- /** v2 Algorithm for RSA signature */
- public static final String SIGNATURE_V2_RSA_ALGORITHM = "RSASSA-PSS";
- /**
- * Process a data stream using a cipher.
- *
- * If cipher is initialized to ENCRYPT_MODE, the input stream will be
- * encrypted. If cipher is initialized to DECRYPT_MODE, the input stream
- * will be decrypted.
- *
- * @param cipher
- * an initialized cipher.
- * @param in
- * the data to encrypt.
- * @param out
- * where encrypted data is written.
- * @throws NoSuchAlgorithmException if invalid encrypt/decrypt algorithm
- * @throws NoSuchPaddingException on padding error
- * @throws InvalidKeyException if key is not RSA or DSA.
- * @throws IOException if IO error occurs
- */
- public static void processCipherStream(final Cipher cipher,
- final InputStream in, final OutputStream out)
- throws NoSuchAlgorithmException, NoSuchPaddingException,
- InvalidKeyException, IOException {
- CipherOutputStream cos = new CipherOutputStream(out, cipher);
- StreamUtils.transferStream(in, cos);
- }
- /**
- * Create and initialize an encrypting cipher using key.getAlgorithm() as
- * transformation.
- *
- * @param key
- * the key used to encrypt.
- * @return a cipher used to encrypt.
- * @throws NoSuchAlgorithmException on invalid algorithm
- * @throws NoSuchPaddingException on invalid padding
- * @throws InvalidKeyException if key is not RSA or DSA.
- */
- public static Cipher getEncryptCipher(final Key key)
- throws NoSuchAlgorithmException, NoSuchPaddingException,
- InvalidKeyException {
- Cipher cipher = Cipher.getInstance(key.getAlgorithm());
- cipher.init(Cipher.ENCRYPT_MODE, key);
- return cipher;
- }
- /**
- * Create and initialize a decrypting cipher using key.getAlgorithm as
- * transformation.
- *
- * @param key
- * the key used to decrypt.
- * @return a cipher used to decrypt.
- * @throws NoSuchAlgorithmException on invalid algorithm
- * @throws NoSuchPaddingException on invalid padding
- * @throws InvalidKeyException if key is not RSA or DSA.
- */
- public static Cipher getDecryptCipher(final Key key)
- throws NoSuchAlgorithmException, NoSuchPaddingException,
- InvalidKeyException {
- Cipher cipher = Cipher.getInstance(key.getAlgorithm());
- cipher.init(Cipher.DECRYPT_MODE, key);
- return cipher;
- }
- /**
- * Create and configure a signature object based on key type.
- *
- * @param key
- * Key used to sign/verify.
- * @param version
- * SIGNATURE_V1 or SIGNATURE_V2
- * @return
- * Configured Signature object
- * @throws InvalidKeyException
- * if key is not RSA or DSA.
- * @throws NoSuchAlgorithmException
- * on invalid algorithm
- * @throws SignatureException
- * on signature error
- */
- public static Signature getSignature(final Key key, final Version version)
- throws InvalidKeyException, NoSuchAlgorithmException,
- SignatureException {
- Signature signature = null;
- if (version == Version.SIGNATURE_V1) {
- if (key instanceof DSAPrivateKey || key instanceof DSAPublicKey) {
- signature = Signature.getInstance(DSA_SIGNATURE_ALGORITHM);
- } else if (key instanceof RSAPrivateKey || key instanceof RSAPublicKey) {
- signature = Signature.getInstance(RSA_SIGNATURE_ALGORITHM);
- }
- } else if (version == Version.SIGNATURE_V2) {
- if (key instanceof DSAPrivateKey || key instanceof DSAPublicKey) {
- signature = Signature.getInstance(SIGNATURE_V2_DSA_ALGORITHM);
- } else if (key instanceof RSAPrivateKey || key instanceof RSAPublicKey) {
- signature = Signature.getInstance(SIGNATURE_V2_RSA_ALGORITHM);
- }
- } else {
- throw new IllegalArgumentException("Unexpected signature version " + version);
- }
- if (signature == null) {
- throw new InvalidKeyException("Expected DSA or RSA key");
- }
- return signature;
- }
- /**
- *
- * @param key Key used to sign/verify.
- * @param version SIGNATURE_V1 or SIGNATURE_V2
- * @param signature A signature
- * @throws InvalidAlgorithmParameterException
- * on invalid or inappropriate algorithm parameters
- */
- public static void configureSignature(final Key key, final Version version,
- final Signature signature) throws InvalidAlgorithmParameterException {
- if (version == Version.SIGNATURE_V2
- && (key instanceof RSAPrivateKey || key instanceof RSAPublicKey)) {
- int keySize;
- if (key instanceof RSAPrivateKey) {
- keySize = ((RSAPrivateKey)key).getModulus().bitLength();
- } else {
- keySize = ((RSAPublicKey)key).getModulus().bitLength();
- }
- // match python cryptography calculation:
- // https://github.com/pyca/cryptography/blob/b16561670320c65a18cce41d0db0c42ab68350a9/src/cryptography/hazmat/primitives/asymmetric/padding.py#L73
- // 32 = (sha)256 / 8
- int maxSaltLength = ((keySize + 6) / 8) - 32 - 2;
- signature.setParameter(
- new PSSParameterSpec("SHA-256", "MGF1",
- MGF1ParameterSpec.SHA256, maxSaltLength, 1));
- }
- }
- /**
- * A convenience method that chooses a signature algorithm based on the key
- * type. Works with DSA and RSA keys.
- *
- * @param privateKey a private key
- * @param data data to sign
- * @return signature as hex encoded string
- * @throws InvalidAlgorithmParameterException
- * on invalid or inappropriate algorithm parameters
- * @throws InvalidKeyException
- * if key is not RSA or DSA.
- * @throws NoSuchAlgorithmException
- * on invalid algorithm
- * @throws SignatureException
- * on signature error
- */
- public static String sign(final PrivateKey privateKey, final byte[] data)
- throws InvalidAlgorithmParameterException, InvalidKeyException,
- NoSuchAlgorithmException, SignatureException {
- // use v1 by default
- return sign(privateKey, data, Version.SIGNATURE_V1);
- }
- /**
- * Generate a signature.
- *
- * @param privateKey
- * private key to use, should be acceptable by signature
- * instance.
- * @param data
- * data/hash to sign.
- * @param version
- * the signature version.
- * @return signature as hex encoded string.
- * @throws InvalidAlgorithmParameterException
- * on invalid or inappropriate algorithm parameters
- * @throws NoSuchAlgorithmException
- * on invalid algorithm
- * @throws InvalidKeyException
- * if key is not RSA or DSA.
- * @throws SignatureException
- * on signature error
- */
- public static String sign(final PrivateKey privateKey, final byte[] data,
- final Version version) throws InvalidAlgorithmParameterException,
- InvalidKeyException, NoSuchAlgorithmException, SignatureException {
- final Signature signature = getSignature(privateKey, version);
- signature.initSign(privateKey);
- configureSignature(privateKey, version, signature);
- signature.update(data);
- return Base64.getEncoder().encodeToString(signature.sign());
- }
- /**
- * A convenience method that chooses a signature algorithm based on the key
- * type. Works with DSA and RSA keys.
- *
- * @param publicKey
- * public key corresponding to private key that generated
- * signature.
- * @param data
- * data/hash to verify
- * @param allegedSignature
- * to try and verify with
- * @return boolean
- * @throws InvalidAlgorithmParameterException
- * on invalid or inappropriate algorithm parameters
- * @throws InvalidKeyException
- * if key is not RSA or DSA.
- * @throws NoSuchAlgorithmException
- * on invalid algorithm
- * @throws SignatureException
- * on signature error
- */
- public static boolean verify(final PublicKey publicKey, final byte[] data,
- final String allegedSignature)
- throws InvalidAlgorithmParameterException, InvalidKeyException,
- NoSuchAlgorithmException, SignatureException {
- return verify(publicKey, data, allegedSignature, Version.SIGNATURE_V1);
- }
- /**
- * Verify a signature.
- *
- * @param publicKey
- * public key corresponding to private key that generated
- * signature.
- * @param data
- * the data/hash that was signed.
- * @param allegedSignature
- * the signature being verified.
- * @param version
- * signature version.
- * @return true if computed signature matches allegedSignature.
- * @throws InvalidAlgorithmParameterException
- * on invalid or inappropriate algorithm parameters
- * @throws NoSuchAlgorithmException
- * on invalid algorithm
- * @throws InvalidKeyException
- * if key is not RSA or DSA.
- * @throws SignatureException
- * on signature error
- */
- public static boolean verify(final PublicKey publicKey, final byte[] data,
- final String allegedSignature, final Version version)
- throws InvalidAlgorithmParameterException, InvalidKeyException,
- NoSuchAlgorithmException, SignatureException {
- final Signature signature = getSignature(publicKey, version);
- signature.initVerify(publicKey);
- configureSignature(publicKey, version, signature);
- signature.update(data);
- return signature.verify(Base64.getDecoder().decode(allegedSignature));
- }
- /**
- * A convenience method to encrypt a byte array.
- *
- * @param key
- * a key that can be used to encrypt.
- * @param toEncrypt
- * the data to encrypt.
- * @return encrypted byte array.
- * @throws InvalidKeyException
- * if key is not RSA or DSA.
- * @throws NoSuchAlgorithmException
- * on invalid algorithm
- * @throws NoSuchPaddingException
- * on invalid padding
- * @throws IllegalArgumentException
- * on illegal args passed to function
- * @throws IOException
- * on IO error
- */
- public static byte[] encrypt(final Key key, final byte[] toEncrypt)
- throws InvalidKeyException, NoSuchAlgorithmException,
- NoSuchPaddingException, IllegalArgumentException, IOException {
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- processCipherStream(getEncryptCipher(key),
- StreamUtils.getInputStream(toEncrypt), baos);
- return baos.toByteArray();
- }
- /**
- * A convenience method to decrypt a byte array.
- *
- * @param key
- * a key that can be used to decrypt.
- * @param toDecrypt
- * the data to decrypt.
- * @return decrypted byte array.
- * @throws InvalidKeyException
- * if key is not RSA or DSA.
- * @throws NoSuchAlgorithmException
- * on invalid algorithm
- * @throws NoSuchPaddingException
- * on invalid padding
- * @throws IllegalArgumentException
- * on illegal args passed to function
- * @throws IOException
- * on IO error
- */
- public static byte[] decrypt(final Key key, final byte[] toDecrypt)
- throws InvalidKeyException, NoSuchAlgorithmException,
- NoSuchPaddingException, IllegalArgumentException, IOException {
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- processCipherStream(getDecryptCipher(key),
- StreamUtils.getInputStream(toDecrypt), baos);
- return baos.toByteArray();
- }
- /**
- * Generate a new symmetric encryption key.
- *
- * @param bits
- * how many bits. This should be AES_128 or AES256.
- * @return generated AES key.
- * @throws NoSuchAlgorithmException
- * on invalid algorithm
- */
- public static Key generateAESKey(final int bits)
- throws NoSuchAlgorithmException {
- KeyGenerator gen = KeyGenerator.getInstance(AES_ALGORITHM);
- gen.init(bits);
- return gen.generateKey();
- }
- /**
- * Generate a new asymmetric encryption key pair.
- *
- * @param bits
- * how many bits. Must be a valid RSA size.
- * @return generated RSA key pair.
- * @throws NoSuchAlgorithmException
- * on invalid algorithm
- */
- public static KeyPair generateRSAKeyPair(final int bits)
- throws NoSuchAlgorithmException {
- KeyPairGenerator gen = KeyPairGenerator.getInstance(RSA_ALGORITHM);
- gen.initialize(bits);
- return gen.generateKeyPair();
- }
- /**
- * Generate a new asymmetric signature key pair.
- *
- * @param bits
- * how many bits. Must be a valid DSA size.
- * @return generated DSA key pair.
- * @throws NoSuchAlgorithmException
- * on invalid algorithm
- */
- public static KeyPair generateDSAKeyPair(final int bits)
- throws NoSuchAlgorithmException {
- KeyPairGenerator gen = KeyPairGenerator.getInstance(DSA_ALGORITHM);
- gen.initialize(bits);
- return gen.generateKeyPair();
- }
- /**
- * Read a X509 encoded certificate. May be DER or PEM encoded.
- *
- * @param bytes
- * the certificate data as a byte array.
- * @return parsed certificate.
- * @throws CertificateException
- * on certificate issue
- * @throws IOException
- * on IO error
- */
- public static Certificate readCertificate(final byte[] bytes)
- throws CertificateException, IOException {
- byte[] data = bytes;
- if (((char) data[0]) == '-') {
- data = convertPEMToDER(new String(data));
- }
- Certificate certificate = CertificateFactory.getInstance("X.509")
- .generateCertificate(new ByteArrayInputStream(data));
- return certificate;
- }
- /**
- * Read a X509 encoded public key. May be DER or PEM encoded.
- *
- * @param bytes
- * the key data as a byte array.
- * @return parsed public key.
- * @throws IOException
- * on IO error
- * @throws NoSuchAlgorithmException
- * on invalid algorithm
- */
- public static PublicKey readPublicKey(final byte[] bytes)
- throws IOException, NoSuchAlgorithmException {
- byte[] data = bytes;
- // decode from PEM format
- if (((char) data[0]) == '-') {
- data = convertPEMToDER(new String(data));
- }
- X509EncodedKeySpec spec = new X509EncodedKeySpec(data);
- try {
- return KeyFactory.getInstance(DSA_ALGORITHM).generatePublic(spec);
- } catch (InvalidKeySpecException e) {
- try {
- return KeyFactory.getInstance(RSA_ALGORITHM).generatePublic(
- spec);
- } catch (InvalidKeySpecException e2) {
- // ignore
- }
- }
- return null;
- }
- /**
- * Read a PKCS#8 encoded private key. May be DER or PEM encoded.
- *
- * @param bytes
- * the key data as a byte array.
- * @return parsed private key.
- * @throws IOException
- * on IO error
- * @throws NoSuchAlgorithmException
- * on invalid algorithm
- */
- public static PrivateKey readPrivateKey(final byte[] bytes)
- throws IOException, NoSuchAlgorithmException {
- byte[] data = bytes;
- // decode from PEM format
- if (((char) data[0]) == '-') {
- data = convertPEMToDER(new String(data));
- }
- PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(data);
- try {
- return KeyFactory.getInstance(DSA_ALGORITHM).generatePrivate(spec);
- } catch (InvalidKeySpecException e) {
- try {
- return KeyFactory.getInstance(RSA_ALGORITHM).generatePrivate(
- spec);
- } catch (InvalidKeySpecException e2) {
- // ignore
- }
- }
- return null;
- }
- /**
- * Read an OpenSSH private key from a stream.
- *
- * @param bytes
- * the byte array containing an OpenSSH private key.
- * @param password
- * password if the key is encrypted.
- * @return decoded PrivateKey.
- * @throws IOException
- * on IO error
- * @throws InvalidKeySpecException
- * when key has invalid specifications
- * @throws NoSuchAlgorithmException
- * on invalid algorithm
- */
- public static PrivateKey readOpenSSHPrivateKey(final byte[] bytes,
- final String password) throws IOException,
- NoSuchAlgorithmException, InvalidKeySpecException {
- PrivateKey key = null;
- // this returns an ethz DSAPrivateKey or RSAPrivateKey
- Object obj = PEMDecoder.decode(new String(bytes).toCharArray(),
- password);
- if (obj instanceof ch.ethz.ssh2.signature.DSAPrivateKey) {
- ch.ethz.ssh2.signature.DSAPrivateKey ethzDSAKey = (ch.ethz.ssh2.signature.DSAPrivateKey) obj;
- key = (DSAPrivateKey) KeyFactory.getInstance("DSA")
- .generatePrivate(
- new DSAPrivateKeySpec(ethzDSAKey.getX(), ethzDSAKey
- .getP(), ethzDSAKey.getQ(), ethzDSAKey
- .getG()));
- } else if (obj instanceof ch.ethz.ssh2.signature.RSAPrivateKey) {
- ch.ethz.ssh2.signature.RSAPrivateKey ethzRSAKey = (ch.ethz.ssh2.signature.RSAPrivateKey) obj;
- key = (RSAPrivateKey) KeyFactory.getInstance("RSA")
- .generatePrivate(
- new RSAPrivateKeySpec(ethzRSAKey.getN(), ethzRSAKey
- .getD()));
- }
- return key;
- }
- /**
- * Read an OpenSSH PublicKey from a stream.
- *
- * @param bytes
- * bytes to read.
- * @return a publicKey
- * @throws IOException
- * on IO error
- * @throws NoSuchAlgorithmException
- * on invalid algorithm
- * @throws InvalidKeySpecException
- * when key has invalid specifications
- */
- public static PublicKey readOpenSSHPublicKey(final byte[] bytes)
- throws IOException, InvalidKeySpecException,
- NoSuchAlgorithmException {
- // format is <type><space><base64data><space><comment>
- String[] line = new String(bytes).trim().split(" ", 3);
- String type = line[0];
- String content = line[1];
- // String comment = line[2];
- ByteBuffer buf = ByteBuffer.wrap(Base64.getDecoder().decode(content));
- // format of decoded content is: <type><keyparams>
- // where type and each param is a DER string
- String decodedType = new String(readDERString(buf));
- if (!decodedType.equals(type)) {
- throw new IllegalArgumentException("expected " + type + ", got "
- + decodedType);
- }
- if (type.equals("ssh-dss")) {
- // dsa key params are p, q, g, y
- BigInteger p = new BigInteger(readDERString(buf));
- BigInteger q = new BigInteger(readDERString(buf));
- BigInteger g = new BigInteger(readDERString(buf));
- BigInteger y = new BigInteger(readDERString(buf));
- return KeyFactory.getInstance(DSA_ALGORITHM).generatePublic(
- new DSAPublicKeySpec(y, p, q, g));
- } else if (type.equals("ssh-rsa")) {
- // rsa key params are e, y
- BigInteger e = new BigInteger(readDERString(buf));
- BigInteger y = new BigInteger(readDERString(buf));
- return KeyFactory.getInstance(RSA_ALGORITHM).generatePublic(
- new RSAPublicKeySpec(y, e));
- } else {
- throw new InvalidKeySpecException("Unknown key type '" + type + "'");
- }
- }
- /**
- * This method reads a DER encoded byte string from a ByteBuffer.
- *
- * A DER encoded string has
- *
- * length = 4 bytes big-endian integer<br>
- * string = length bytes
- *
- * @param buf
- * buffer containing DER encoded bytes.
- * @return bytes the decoded bytes.
- */
- public static byte[] readDERString(ByteBuffer buf) {
- int length = buf.getInt();
- if (length > 8192) {
- throw new IllegalArgumentException("DER String Length " + length
- + " > 8192");
- }
- byte[] bytes = new byte[length];
- buf.get(bytes);
- return bytes;
- }
- /**
- * Read a PEM format.
- *
- * This does not currently support encrypted PEM formats.
- *
- * @param string
- * string containing PEM formatted data.
- * @return DER formatted data.
- * @throws IOException
- * on IO error
- */
- public static byte[] convertPEMToDER(final String string)
- throws IOException {
- List<String> lines = StringUtils.split(string, "\n");
- String header = lines.remove(0);
- String footer = lines.remove(lines.size() - 1);
- String type;
- if (header.startsWith("-----BEGIN ") && header.endsWith("-----")) {
- type = header;
- type = type.replace("-----BEGIN ", "");
- type = type.replace("-----", "");
- if (type.contains("ENCRYPTED")) {
- throw new IllegalArgumentException(
- "Encrypted keys are not supported.");
- }
- if (footer.equals("-----END " + type + "-----")) {
- // expected match
- return Base64.getMimeDecoder().decode(
- StringUtils.join(new LinkedList<Object>(lines), "\n"));
- } else {
- throw new IllegalArgumentException("Unexpected PEM footer '"
- + footer + "'");
- }
- } else {
- throw new IllegalArgumentException("Unexpected PEM header '"
- + header + "'");
- }
- }
- }