Tuesday, April 1, 2014

Java/.NET encryption

Where I work we have what I would call a Ferarri of security and single sign on technology - we are using ADFS as our SSO technology.  For the most part we've been successful in getting vendor to comply and partner with us to ensure that our customers experiences are secure as possible.  With vendors (SP) who use .NET we encourage them to use WS-Federation, while those using JAVA or PHP we suggest using SAML 2.0 to interface with our ADFS IdM.  Then there's a third classification of vendor - those who won't, or those who simply can't get it together enough to hook up to our IdM.  There's an even more of a special category of vendor who can't really understand why doing a HTTPS GET with a base-64 encoded string that contains your customer's login ID and password is called "completely unacceptable" during a conference bridge.  Well sadly enough I had to work such a vendor recently - I even attempted to make the process more secure by rigging up a handshake that could be easy to implement and secure enough for the customer.

This handshake was rather easy - really.  We'd take care of performing the authentication and initial authorization steps of the login process.  In other words we'd ensure that the customer provides a good user name and password and that they have the product which would give them some entitlement to enter the vendor supplied web site.  In pseudo code these were the steps this handshake needed to take.
  • Format a plain text string with the following elements: &CustId=xxxx&TimeStamp=<Julian date/time value>.  The CustId element was taken from the assertions being sent - while the time stamp was used as a random filler for the encryption process.
  • Take plan text and encrypt the string using a shared certificate.
  • Convert the binary encrypted data into a Base-64 string.
  • Sanitize the Base-64 string so that it could be passed along as parameter to the vendor web site.
To obtain necessary information needed by the vendor (e.g. the CustId) the steps above needed to be reversed.  The value after "&CustId=" could then be used to look up the customer and provide them with the tools necessary to perform the actions they wish.

Our technology stack is .NET - the vendors is Java.  I haven't been in the Java space in over a decade at this point.  But I was pretty certain that whatever encrypted string I could come up could easily be processed on the Java side by a developer.

Here's the code to build the handshake data.
public static string EncryptQueryString(string handshakeName, string plainStringToEncrypt)  
     {  
       string digitalCertificateName   
         = CommonConfigurationManager.GetNonSsoHandshakeEncryptCert(handshakeName);  
       X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);  
       StorePermission sp = new StorePermission(PermissionState.Unrestricted);  
       sp.Flags = StorePermissionFlags.OpenStore;  
       sp.Assert();  
       X509Certificate2 certX5092 = null;  
       store.Open(OpenFlags.IncludeArchived);  
       if (digitalCertificateName.Length > 0)  
       {  
         foreach (X509Certificate2 cert in store.Certificates)  
         {  
           if (cert.SubjectName.Name != null &&   
             cert.SubjectName.Name.Contains(digitalCertificateName))  
           {  
             certX5092 = cert;  
             break;  
           }  
         }  
         if (certX5092 == null)  
         {  
           throw new Exception("No Certificate could be found in name " + digitalCertificateName);  
         }  
       }  
       else  
       {  
         certX5092 = store.Certificates[0];  
       }  
       string plainString = plainStringToEncrypt.Trim();  
       byte[] cipherbytes = Encoding.UTF8.GetBytes(plainString);  
       RSACryptoServiceProvider rsa = (RSACryptoServiceProvider)certX5092.PublicKey.Key;  
       byte[] cipher = rsa.Encrypt(cipherbytes, false);  
       string cipherText = Convert.ToBase64String(cipher);  
       return cipherText;  
     }  

So there's a great deal going on here...but essentially I'll find a certificate of the name found in the config file for this handshake interface.  When found in the certificate store I'll then use it to encrypt the plain text string to an array of bytes, which will be converted to a Base-64 string.  Code further up the chain will fix the Base-64 string to ensure that characters are properly changed to HTML escape characters if necessary.

I then got my initial feedback from the vendor.  And it wasn't encouraging - it indicated that the vendor didn't really understand how this all worked and lacked the basic skills necessary to pull this off.  I can only imagine what would have happened if we forced them into SAML 2.0 - well I can and when it does happen it won't be pretty.  After a few back and forths and some hints I gave up and wrote the Java side of this for them.  Which reverses the process so we can get the "CustId" from the encrypted string.

public static void main(String[] args) {  
           // TODO Auto-generated method stub  
           String uriString = "a%2bbO7VR6tQ4EJd5voT5gWxdUC4ELTQR5dvzDL4a8f71AQYK2TTW%2bvzbg%2fjoBqlAMrS9HfAYTWMfVuBSLY4Lk0ksWKg249cprtbxzlOFaPL19M9GQSrQLwwKNfx8MWyuiI2YZlgteM8cJBPIq5y9S2JU40jx38fEULEBTp83%2fsdylcUTSx9sGO1MWGRkW2ux2a6FiP7xcIYiqldAwnx8KRasCL3iC7jKb8oyPj9g3r7gHzs1jOLpwUahNC4%2fR9skXfQBqSDV3CuApuXCoUIKi6QNOlHp6iLjkpuz1vNAZJgQn4U0hlmZ%2bJDW3sl7v09VdPlTjQgIrlKroYNH1csBmng%3d%3d";  
           String uriString1 = "N7hoRdMeapijdzo4z35lcOdT%2b4x7cMk7LvkJMLClax5YCPsqcopeyGIyZKlb0NZ%2baAjEtTq%2fSgc43QvXarx9YX1GWxXmrCyZykSirZOpKdRiMp%2fswNLRsYaPyIj4UBZAmvMoDm%2bW3fXX%2bwslGJMPg10AjXGHC7O6G8%2f64yO7zCBF3j5i8HI7lEhlMtIfE7%2fn%2bm5dhwkC9ZEtEpZMOiFlqnqT2OIhzzMySzyNFk6Y1lUWrjk7S6%2bL1a7Gihr%2fYjjPX9Pt6RPQ9gCo4oFbfNjtqbPYHttEiJerVCNm6eYP4AiU%2f0c54YAA2DfRxDhuQW%2bq%2b%2flS83RahAy4JyRJVy%2b5ug%3d%3d";  
           String uriString2 = "opbLFa5UpBEK1wDtVNZ0j7srfqx447fMTAZThTL4Cr4xWYrzIpJv9qrAy3yKG73Lt7fUGWk7q%2foiy0f2r2ZjexI8lKvnAbrtD6URK3G1NFswY6PsH99YrsKdz2%2f2qvvbWfjlntqFrOitIS3Ndyt2PPBVmLCiWFSDy%2fxjE%2bL3XYo2VdEWAL%2fjpyzHhfxC1C84nytAINXDoECKVeU6n2zCg9%2bKAeM4keNpawRtFJXlB4nYj8sUayQy4LedfZk5JR%2bBMq5HWED6QCuGoAbZ7D9ablIMY%2bqKfZOd5zjSFN57qsM7Lgozdu5F80bxKDqrvR3C7GFK19YInxlEKy%2fpo8mmNg%3d%3d";  
           String unEncodeString = "";  
           final String PRIVATE_KEYFILE = "private_key.der";  
           final String CERT_FILE = "binary_cert.cer";  
           try {  
                unEncodeString = unEncodeUri(uriString2);  
                byte[] encryptedStuff = Base64.decodeBase64(unEncodeString);  
                PrivateKey privateKey = getPrivateKey(PRIVATE_KEYFILE);  
                InputStream inStream = new FileInputStream(CERT_FILE);  
                CertificateFactory cf = CertificateFactory.getInstance("X.509");  
                X509Certificate cert = (X509Certificate)cf.generateCertificate(inStream);  
                Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "SunJCE");  
                cipher.init(Cipher.DECRYPT_MODE, privateKey);  
                byte[] unenCryptedData = cipher.doFinal(encryptedStuff);  
                System.out.println(new String(unenCryptedData));  
           } catch (UnsupportedEncodingException | FileNotFoundException | CertificateException | NoSuchAlgorithmException | NoSuchPaddingException e) {  
                e.printStackTrace();  
           } catch (InvalidKeyException e) {  
                e.printStackTrace();  
           } catch(BadPaddingException e) {  
                e.printStackTrace();                 
           } catch (IllegalBlockSizeException e) {  
                e.printStackTrace();  
           } catch (NoSuchProviderException e) {  
                e.printStackTrace();  
           }  
           System.out.println(unEncodeString);  
      }  
      private static PrivateKey getPrivateKey(String privateKeyFile)  
      {  
           try {  
                RandomAccessFile raf = new RandomAccessFile(privateKeyFile, "r");  
                byte[] buff = new byte[(int)raf.length()];  
                raf.readFully(buff);  
                raf.close();  
                PKCS8EncodedKeySpec kspec = new PKCS8EncodedKeySpec(buff);  
                KeyFactory kf;  
                kf = KeyFactory.getInstance("RSA");  
                return kf.generatePrivate(kspec);  
           } catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException e) {  
                // TODO Auto-generated catch block  
                e.printStackTrace();  
           }  
           return null;   
      }  
      private static String unEncodeUri(String uriString) throws UnsupportedEncodingException  
      {  
           URLDecoder mydecoder = new URLDecoder();  
           return mydecoder.decode(uriString);  
      }  

There are a couple of tricky parts here.  First the Cipher instance MUST be obtained with the parameter above ("RSA/ECB/PKCS1Padding").  Not doing this will cause you to get an error because of the padding that .NET adds to the encrypted bytes.  Second, you must read in a private key file and obtain the private key in order to decrypt the string.  The private key file can be generated from certificate provided via the tool OpenSSL.  The best command I found for this is below:

openssl pkcs8 -topk8 -nocrypt -outform DER privatekeyfile.key host.pk8

Prior to decrypting and pulling out the results needed the Base-64 string must be converted to back to a non-HTML safe Base-64 string.  Then it must be converted into an array of bytes from the Base-64 string.  This is accomplished in the method unEncodeUri and by calling the Apache common method of Base64.decodeBase64(unEncodeString) which will do the job well.  Once accomplished decrypted the string is rather easy.  Finally in the line System.out.println(new String(unenCryptedData)) you can have the encrypted data displayed on the console.