package com.sap.jam.oauth.client; import java.security.KeyFactory; import java.security.PublicKey; import java.security.PrivateKey; import java.security.Signature; import java.security.NoSuchAlgorithmException; import java.security.InvalidKeyException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.interfaces.RSAPrivateCrtKey; import java.security.interfaces.RSAPublicKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.io.ByteArrayInputStream; import javax.crypto.Cipher; /** * This utility class has various helpers for working with signatures, certificates, * and private keys. * * For example, generation of a signature, ie, signing data with a private key 2) Verification of such * signatures, ie, verifying signature with a public key/certificate * * Public facing functions fall into one of these two buckets, variations are * overloaded based on different input parameters and renamed based on different * output values (ie, we like to handle base64 encoding so the caller doesn't * need to) * * the notion of a "raw signature" is one that is stored as a java byte array, * and has not been base64 encoded */ public final class SignatureUtil { private final static String SIGNATURE_ALGORITHM = "SHA1withRSA"; private final static String PRIVATE_KEY_ALGORITHM = "RSA"; private final static String PUBLIC_CERT_ALGORITHM = "X.509"; /** * Signature Generation functions all functions take the form signature = * f(private key, signature base) * */ public static String generateBase64Signature( final String privateKeyBase64, final byte[] signatureBase) { return Base64Util.encode(generateRawSignature(makePrivateKey(privateKeyBase64), signatureBase)); } public static String generateBase64Signature(final PrivateKey privateKey, final byte[] signatureBase) { return Base64Util.encode(generateRawSignature(privateKey, signatureBase)); } public static byte[] generateRawSignature(final String privateKeyBase64, final byte[] signatureBase) { return generateRawSignature(makePrivateKey(privateKeyBase64), signatureBase); } public static byte[] generateRawSignature(final PrivateKey privateKey, final byte[] signatureBase) { try { final Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM); sig.initSign(privateKey); sig.update(signatureBase); return sig.sign(); } catch (final InvalidKeyException e) { throw new RuntimeException("Invalid private key: " + e.getMessage(), e); } catch (final NoSuchAlgorithmException e) { throw new RuntimeException("Unknown signature generation algorithm (" + SIGNATURE_ALGORITHM + ") " + e.getMessage(), e); } catch (final java.security.SignatureException e) { throw new RuntimeException("Error encountered while signing: " + e.getMessage(), e); } } /** * Signature Verification functions all functions take the form success = * f(public key, signature, signature base) * */ public static boolean verifyBase64Signature(final String base64Certificate, final String base64Signature, final byte[] signatureBase) { return verifyRawSignature(makePublicKey(base64Certificate), getRawSignatureFromBase64(base64Signature), signatureBase); } public static boolean verifyBase64Signature(final PublicKey publicKey, final String base64Signature, final byte[] signatureBase) { return verifyRawSignature(publicKey, getRawSignatureFromBase64(base64Signature), signatureBase); } public static boolean verifyRawSignature(final String base64Certificate, final byte[] rawSignature, final byte[] signatureBase) { return verifyRawSignature(makePublicKey(base64Certificate), rawSignature, signatureBase); } public static boolean verifyRawSignature(final PublicKey publicKey, final byte[] rawSignature, final byte[] signatureBase) { try { final Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM); sig.initVerify(publicKey); sig.update(signatureBase); return sig.verify(rawSignature); } catch (final NoSuchAlgorithmException e) { throw new RuntimeException("Unknown signature verification algorithm (" + SIGNATURE_ALGORITHM + ") " + e.getMessage(), e); } catch (final InvalidKeyException e) { throw new RuntimeException("Invalid public key: " + e.getMessage(), e); } catch (final java.security.SignatureException e) { throw new RuntimeException("Error encountered while verifying signature: " + e.getMessage(), e); } } /** * Utility helper functions prevent duplication of work over the various * overloads */ /** * convert basee64 signature to a raw signature */ private static byte[] getRawSignatureFromBase64(final String signature) { return Base64Util.decode(signature); } /** * convert a base64 encoded certificate into a java object public key */ public static PublicKey makePublicKey(final String certificateBase64) { if (certificateBase64 == null || certificateBase64.isEmpty()) { throw new IllegalArgumentException("Supplied 'certificateBase64' argument is null or empty."); } try { final CertificateFactory cf = CertificateFactory.getInstance(PUBLIC_CERT_ALGORITHM); final Certificate certificate = cf.generateCertificate(new ByteArrayInputStream(Base64Util.decode(certificateBase64))); return certificate.getPublicKey(); } catch (final CertificateException e) { throw new RuntimeException("Unable to generate certificates (" + PUBLIC_CERT_ALGORITHM + ") " + e.getMessage(), e); } } /** * convert a base64 encoded private key into a java object private key */ public static PrivateKey makePrivateKey(final String privateKeyBase64) { if (privateKeyBase64 == null || privateKeyBase64.isEmpty()) { throw new IllegalArgumentException("Supplied 'privateKeyBase64' argument is null or empty."); } try { final KeyFactory key_factory = KeyFactory.getInstance(PRIVATE_KEY_ALGORITHM); final PKCS8EncodedKeySpec private_key_spec = new PKCS8EncodedKeySpec(Base64Util.decode(privateKeyBase64)); return key_factory.generatePrivate(private_key_spec); } catch (final NoSuchAlgorithmException e) { throw new RuntimeException("Invalid algorithm (" + PRIVATE_KEY_ALGORITHM + "): " + e.getMessage(), e); } catch (final InvalidKeySpecException e) { throw new RuntimeException("Invalid KeySpec: " + e.getMessage(), e); } } public static X509Certificate makeCertificate(String certificateBase64) { if (certificateBase64 == null || certificateBase64.isEmpty()) { throw new IllegalArgumentException("Supplied 'certificateBase64' argument is null or empty."); } try { byte[] certRaw = Base64Util.decode(certificateBase64); CertificateFactory certFactory = CertificateFactory.getInstance(PUBLIC_CERT_ALGORITHM); return (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certRaw)); } catch (Exception e) { throw new RuntimeException("Unable to deserialize supplied X509 certificate.", e); } } /** * @return true if encrypting then decrypting a small test string roundtrips. */ public static boolean validatePrivateKeyCertificateCompatibility(PrivateKey privateKey, X509Certificate certificate) { return validatePrivateKeyPublicKeyCompatibility(privateKey, certificate.getPublicKey()); } /** * @return true if encrypting then decrypting a small test string roundtrips. */ public static boolean validatePrivateKeyPublicKeyCompatibility(PrivateKey privateKey, PublicKey publicKey) { String plainData = "Some text for encryption"; try { byte[] encryptedData = encrypt(plainData.getBytes("UTF-8"), publicKey); byte[] decryptedData = decrypt(encryptedData, privateKey); String plainDataRoundTrip = new String(decryptedData, "UTF-8"); if (plainData.equals(plainDataRoundTrip)) { return true; } } catch (Exception e) { return false; } return false; } static byte[] encrypt(byte[] plainData, PublicKey pubKey) throws Exception { Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.ENCRYPT_MODE, pubKey); byte[] encryptedData = cipher.doFinal(plainData); return encryptedData; } static byte[] decrypt(byte[] encryptedData, PrivateKey privateKey) throws Exception { Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.DECRYPT_MODE, privateKey); byte[] decryptedData = cipher.doFinal(encryptedData); return decryptedData; } /** * Returns XML suitable for use in .NET code with the RSACryptoServiceProvider.FromXmlString method. * This RSACryptoServiceProvider object can be used in the .NET version of the OAuth libraries in the areas * where we use a PrivateKey object in the Java libraries. * An explanation of the XML used for key formats is here: http://msdn.microsoft.com/en-us/library/system.security.cryptography.rsa.toxmlstring.aspx * Thanks to http://www.jensign.com/JavaScience/PvkConvert/ for pointing out that leading zeros must be trimmed for .NET. */ public static String privateKeyToDotNetXml(PrivateKey privateKey) { try{ StringBuilder sb = new StringBuilder(); RSAPrivateCrtKey rsaPrivateKey = (RSAPrivateCrtKey)privateKey; sb.append("<RSAKeyValue>") ; sb.append("<Modulus>" + Base64Util.encode(removeLeadingZeros(rsaPrivateKey.getModulus().toByteArray())) + "</Modulus>"); sb.append("<Exponent>" + Base64Util.encode(removeLeadingZeros(rsaPrivateKey.getPublicExponent().toByteArray())) + "</Exponent>"); sb.append("<P>" + Base64Util.encode(removeLeadingZeros(rsaPrivateKey.getPrimeP().toByteArray())) + "</P>"); sb.append("<Q>" + Base64Util.encode(removeLeadingZeros(rsaPrivateKey.getPrimeQ().toByteArray())) + "</Q>"); sb.append("<DP>" +Base64Util.encode(removeLeadingZeros(rsaPrivateKey.getPrimeExponentP().toByteArray())) + "</DP>"); sb.append("<DQ>" + Base64Util.encode(removeLeadingZeros(rsaPrivateKey.getPrimeExponentQ().toByteArray())) + "</DQ>"); sb.append("<InverseQ>" + Base64Util.encode(removeLeadingZeros(rsaPrivateKey.getCrtCoefficient().toByteArray())) + "</InverseQ>"); sb.append("<D>" + Base64Util.encode(removeLeadingZeros(rsaPrivateKey.getPrivateExponent().toByteArray())) + "</D>"); sb.append("</RSAKeyValue>") ; return sb.toString(); } catch(Exception e) { throw new IllegalArgumentException("Could not convert PrivateKey to Dot Net XML.", e); } } /** * Returns XML suitable for use in .NET code with the RSACryptoServiceProvider.FromXmlString method. * This RSACryptoServiceProvider object can be used in the .NET version of the OAuth libraries in the areas * where we use a PublicKey object in the Java libraries. * An explanation of the XML used for key formats is here: http://msdn.microsoft.com/en-us/library/system.security.cryptography.rsa.toxmlstring.aspx * Thanks to http://www.jensign.com/JavaScience/PvkConvert/ for pointing out that leading zeros must be trimmed for .NET. */ public static String publicKeyToDotNetXml(PublicKey publicKey) { try{ StringBuilder sb = new StringBuilder(); RSAPublicKey rsaPublicKey = (RSAPublicKey)publicKey; sb.append("<RSAKeyValue>") ; sb.append("<Modulus>" + Base64Util.encode(removeLeadingZeros(rsaPublicKey.getModulus().toByteArray())) + "</Modulus>"); sb.append("<Exponent>" + Base64Util.encode(removeLeadingZeros(rsaPublicKey.getPublicExponent().toByteArray())) + "</Exponent>"); sb.append("</RSAKeyValue>") ; return sb.toString(); } catch(Exception e) { throw new IllegalArgumentException("Could not convert PublicKey to Dot Net XML.", e); } } private static byte[] removeLeadingZeros(byte[] data) { int len = data.length; if (len < 1) { throw new IllegalArgumentException("non-empty byte array expected."); } int pos; for (pos = 0; pos < len && data[pos] == 0; ++pos) { } if (pos >= len) { throw new IllegalArgumentException("array of zeros not expected."); } if (pos > 0) { byte[] trimmedData = new byte[len - pos]; System.arraycopy(data, pos, trimmedData, 0, len - pos); return trimmedData; } return data; } }