• Main Page
  • Related Pages
  • Packages
  • Classes
  • Files
  • File List

crypto/AsymmetricCipher.java

Go to the documentation of this file.
00001 
00002 package crypto;
00003 
00004 import java.io.BufferedReader;
00005 import java.io.BufferedWriter;
00006 import java.io.ByteArrayInputStream;
00007 import java.io.ByteArrayOutputStream;
00008 import java.io.FileNotFoundException;
00009 import java.io.FileReader;
00010 import java.io.FileWriter;
00011 import java.io.IOException;
00012 import java.io.ObjectInputStream;
00013 import java.io.Serializable;
00014 import java.security.InvalidKeyException;
00015 import java.security.KeyPair;
00016 import java.security.KeyPairGenerator;
00017 import java.security.NoSuchAlgorithmException;
00018 import java.security.PrivateKey;
00019 import java.security.PublicKey;
00020 import java.security.Security;
00021 import java.security.Signature;
00022 import java.security.SignatureException;
00023 import java.security.SignedObject;
00024 import java.text.SimpleDateFormat;
00025 import java.util.Calendar;
00026 
00027 import javax.crypto.Cipher;
00028 import javax.crypto.NoSuchPaddingException;
00029 import javax.crypto.SecretKey;
00030 
00031 import utils.Log;
00032 import utils.Base64;
00033 
00034 /**
00035  *  Implements asymmetric cipher with public and private keys used to retrieve
00036  *  secret key (used for symmetric ciphering of peer-to-peer datagram packets)
00037  *  from remote peer. Remote peer sends its secret key encrypted with our public key.
00038  *  
00039  *  Transmission of the SecretKey can be schematically shown:
00040  *  <pre>
00041  *  Send secret key:
00042  *  secretKey >> serialize >> encrypt (with PubKey) >> encode to Base64 >> transmit
00043  *  
00044  *  Reconstruct secret key:
00045  *  receive >> decode from Base64 >> decrypt (with PrivKey) >> deserialize >> secretKey
00046  *  </pre> 
00047  *  
00048  *  @author Mikica B Kocic
00049  */
00050 public class AsymmetricCipher
00051 {
00052     /**
00053      *  Asymmetric cipher algorithm
00054      */
00055     private final static String algorithm = "RSA";
00056     
00057     /**
00058      *  Default key size for algorithm
00059      */
00060     private final static int keySize = 1024;
00061 
00062     /**
00063      *  Padding to be used when ciphering/deciphering.
00064      *  JCE does not support RSA/CBC so the CBC mode is built 
00065      *  on the top of ECB in AsymmetricCipher.decrypt().
00066      */
00067     private final static String padding = "/ECB/PKCS1PADDING";
00068 
00069     /**
00070      *  Message digest used for creating/validating signatures
00071      */
00072     private final static String digest  = "SHA1";
00073     
00074     /**
00075      *  The name of the file holding saved private key
00076      */
00077     private final static String privateKeyFile = "mykf-private-key.txt";
00078     
00079     /**
00080      *  The name of the file holding saved public key
00081      */
00082     private final static String publicKeyFile = "mykf-public-key.txt";
00083 
00084     /**
00085      *  Private key used for deciphering and signing messages
00086      */
00087     private PrivateKey privateKey = null;
00088     
00089     /**
00090      *  Public key corresponding to our private key
00091      */
00092     private PublicKey publicKey = null;
00093     
00094     /**
00095      *  The comment (description) of the key pair
00096      */
00097     private String keyPairComment = null;
00098 
00099     /**
00100      *  Instance of the decrypting engine based on our private key
00101      */
00102     private Cipher cipher = null;
00103     
00104     /**
00105      *  Our public key: serialized and encoded as Base64 string.
00106      */
00107     private String serializedPublicKey = null;
00108 
00109     /**
00110      *  Generates a pair of keys and serializes public key as Base64 string.
00111      */
00112     public AsymmetricCipher ()
00113     {
00114         boolean loadedKeys = loadSavedKeyPair ();
00115         boolean sanity = false;
00116         
00117         while( true )
00118         {
00119             if ( ! loadedKeys  ) {
00120                 generateKeyPair ();
00121             }
00122     
00123             instantiateCipher ();
00124             
00125             serializePublicKey ();
00126             
00127             if ( ! isActive () ) {
00128                 destruct (); // make sure everyting is clean
00129                 if ( loadedKeys ) {
00130                     Log.warn( "Load key pair inactive; Generating new key pair..." );
00131                     continue;
00132                 } else {
00133                     Log.error( "AsymmetricCipher: Generated key pair inactive." );
00134                     return;
00135                 }
00136             }
00137     
00138             sanity = sanityCheck ();
00139             if ( sanity ) {
00140                 break; // everything is ok
00141             }
00142             
00143             if ( loadedKeys )  {
00144                 Log.warn( "Sanity check failed on loaded key pair: Retrying..." );
00145                 loadedKeys = false;
00146             } else {
00147                 Log.error( "AsymmetricCipher: Sanity check failed on generated key pair." );
00148                 destruct ();
00149                 return;
00150             }
00151         }
00152 
00153         /* Save the key pair (if the key pair is generated i.e. not loaded)
00154          */
00155         if ( isActive () && sanity && ! loadedKeys ) {
00156             saveKeyPair ();
00157             exportPublicKey( null );
00158         }
00159     }
00160     
00161     /**
00162      *  Destructs object (makes it inactive)
00163      */
00164     private void destruct ()
00165     {
00166         this.privateKey = null;
00167         this.publicKey = null;
00168         this.cipher = null;
00169         this.serializedPublicKey = null;
00170     }
00171     
00172     /**
00173      *  Load saved key pair
00174      */
00175     private boolean loadSavedKeyPair ()
00176     {
00177         this.privateKey = null;
00178         this.publicKey = null;
00179         
00180         String keyFilePath = CipherEngine.getPrivateKeyDirectory() 
00181                            + AsymmetricCipher.privateKeyFile;
00182 
00183         Object oPair = loadObject( keyFilePath );
00184         
00185         if ( oPair != null && ( oPair instanceof NamedKeyPair ) ) 
00186         {
00187             NamedKeyPair keyPair = (NamedKeyPair) oPair;
00188             this.privateKey = keyPair.privateKey;
00189             this.publicKey = keyPair.publicKey;
00190             this.keyPairComment = keyPair.comment;
00191             
00192             Log.attn( "Loaded private key '" + this.keyPairComment 
00193                     + "' from file '" + AsymmetricCipher.privateKeyFile + "'" );
00194         }
00195 
00196         return ( this.privateKey != null && this.publicKey != null ); 
00197     }
00198 
00199     /**
00200      *  Saves private/public key pair with description (this.keyPairComment)
00201      */
00202     private void saveKeyPair ()
00203     {
00204         if ( ! isActive () ) {
00205             return;
00206         }
00207 
00208         String keyFilePath = CipherEngine.getPrivateKeyDirectory() 
00209                            + AsymmetricCipher.privateKeyFile;
00210         
00211         if ( saveObject( 
00212                 new NamedKeyPair( this.publicKey, this.privateKey, this.keyPairComment ),
00213                 keyFilePath, null ) )
00214         {
00215             Log.attn( "Private key saved as '" + keyFilePath + "'" );
00216             
00217             /* Change file permissions using native OS 'chmod' command (ignoring Windows), 
00218              * so that no one but the owner might read its contents.
00219              */
00220             String osName = System.getProperty( "os.name" ).toLowerCase();
00221             if ( ! osName.matches( "^.*windows.*$" ) ) 
00222             {
00223                 try 
00224                 {
00225                     Runtime.getRuntime().exec( new String[] { "chmod", "400", keyFilePath } );
00226                 }
00227                 catch( IOException e ) 
00228                 {
00229                     Log.trace( "Failed to do chmod; OS = " + osName );
00230                     Log.exception( Log.TRACE, e );
00231                 }
00232             }
00233         }
00234     }
00235 
00236     /**
00237      *  Generates a key pair
00238      */
00239     private void generateKeyPair ()
00240     {
00241         this.privateKey = null;
00242         this.publicKey = null;
00243         this.keyPairComment = null;
00244         
00245         try
00246         {
00247             KeyPairGenerator keyGen = KeyPairGenerator.getInstance( algorithm );
00248             keyGen.initialize( keySize );
00249             
00250             KeyPair keyPair = keyGen.generateKeyPair ();
00251             this.privateKey = keyPair.getPrivate ();
00252             this.publicKey  = keyPair.getPublic ();
00253             
00254             /* Default comment is name of the cipher plus time-stamp 
00255              */
00256             Calendar cal = Calendar.getInstance ();
00257             SimpleDateFormat sdf = new SimpleDateFormat( "yyyy-MM-dd-HHmmssSSS" );
00258             this.keyPairComment = algorithm.toLowerCase () + "-key-" 
00259                                 + sdf.format( cal.getTime() ); 
00260 
00261             Log.attn( "Generated a new " + algorithm + "/" 
00262                     + keySize + " key pair: '" + this.keyPairComment + "'" );
00263         }
00264         catch( NoSuchAlgorithmException e )
00265         {
00266             StringBuffer algos = new StringBuffer( "Available algorithms:" );
00267             
00268             for( String s : Security.getAlgorithms( "Cipher" ) ) {
00269                 algos.append( " " ).append( s );
00270             }
00271             
00272             Log.exception( Log.ERROR, e );
00273             Log.warn( algos.toString () );
00274         }
00275     }
00276 
00277     /**
00278      *  Instantiates a cipher
00279      */
00280     private void instantiateCipher ()
00281     {
00282         this.cipher = null;
00283 
00284         if ( this.privateKey == null || this.publicKey == null ) {
00285             return; // must have key pair
00286         }
00287 
00288         try
00289         {
00290             this.cipher = Cipher.getInstance( algorithm + padding );
00291 
00292             Log.trace( "Instantiated asymmetric cipher: " + this.cipher.getAlgorithm () );
00293         }
00294         catch( NoSuchAlgorithmException e )
00295         {
00296             StringBuffer algos = new StringBuffer( "Available algorithms:" );
00297             
00298             for( String s : Security.getAlgorithms( "Cipher" ) ) {
00299                 algos.append( " " ).append( s );
00300             }
00301             
00302             Log.exception( Log.ERROR, e );
00303             Log.warn( algos.toString () );
00304         }
00305         catch( NoSuchPaddingException e )
00306         {
00307             Log.exception( Log.ERROR, e );
00308         }
00309     }
00310 
00311     /**
00312      *  Serializes the public key, signs it and encodes in Base64 format
00313      */
00314     private void serializePublicKey ()
00315     {
00316         if ( this.privateKey == null || this.publicKey == null || this.cipher == null ) {
00317             return; // must have key pair and cipher
00318         }
00319 
00320         try
00321         {
00322             SignedObject signedPublicKey = this.signObject( this.publicKey );
00323   
00324             this.serializedPublicKey = Base64.encodeObject( signedPublicKey, Base64.GZIP );
00325 
00326             Log.trace( "Serialized Public Key in Base64; length = " 
00327                     + this.serializedPublicKey.length () );
00328         }
00329         catch( IOException e )
00330         {
00331             Log.exception( Log.ERROR, e );
00332         }
00333     }
00334 
00335     /**
00336      *  Returns if cipher is properly initialized
00337      */
00338     public boolean isActive ()
00339     {
00340         return this.cipher != null && this.serializedPublicKey != null;
00341     }
00342     
00343     /**
00344      *  Returns serialized public key used for encryption of datagrams as 
00345      *  Base64 string.
00346      */
00347     public String getSerializedAndSignedPublicKey ()
00348     {
00349         return this.serializedPublicKey;
00350     }
00351 
00352     /**
00353      *  Save public key into file
00354      *  
00355      *  @param fileName file where to save public key; if null, it will be
00356      *                  the default: AssymmetricCipher.publicKeyFile 
00357      */
00358     public void exportPublicKey( String fileName )
00359     {
00360         if ( ! isActive () ) {
00361             return;
00362         }
00363 
00364         if ( fileName == null ) {
00365             fileName = CipherEngine.getPrivateKeyDirectory()
00366                      + AsymmetricCipher.publicKeyFile;
00367         }
00368 
00369         if ( saveObject( 
00370                 new NamedPublicKey( this.publicKey, this.keyPairComment ), 
00371                 fileName,
00372                 "  " + this.keyPairComment + "\n" ) )
00373         {
00374             Log.attn( "Public key exported to '" + fileName + "'" );
00375         }
00376     }
00377 
00378     /**
00379      *  Returns serializable named publicKey (with comment) encoded as Base64
00380      */
00381     public String getNamedPublicKey ()
00382     {
00383         StringBuffer sb = new StringBuffer ();
00384 
00385         try {
00386             String encodedKey = Base64.encodeObject( new NamedPublicKey( 
00387                     this.publicKey, this.keyPairComment ), Base64.GZIP );
00388             
00389             sb.append( encodedKey );
00390             sb.append( " " );
00391             sb.append( this.keyPairComment );
00392         }
00393         catch( IOException e )
00394         {
00395             Log.exception( Log.ERROR, e );
00396         }
00397 
00398         return sb.toString ();
00399     }
00400     
00401     /**
00402      *  Saves serializable object encoded in Base64 to file
00403      */
00404     public static boolean saveObject( Serializable object, String fileName, String comment )
00405     {
00406         boolean result = false;
00407 
00408         try {
00409             String text = Base64.encodeObject( object, Base64.GZIP );
00410             
00411             BufferedWriter out = new BufferedWriter( new FileWriter( fileName ) );
00412             
00413             out.write( text );
00414             
00415             if ( comment != null ) {
00416                 out.write( comment );
00417             }
00418 
00419             out.flush ();
00420             out.close ();
00421             
00422             Log.trace( "Saved " + object.getClass().toString () + " into " + fileName );
00423             
00424             result = true;
00425         }
00426         catch( IOException e )
00427         {
00428             Log.exception( Log.ERROR, e );
00429         }
00430 
00431         return result;
00432     }
00433     
00434     /**
00435      *  Loads serializable object encoded in Base64 from file
00436      */
00437     public static Object loadObject( String fileName )
00438     {
00439         Object object = null;
00440 
00441         try {
00442             StringBuffer sb = new StringBuffer ();
00443             
00444             BufferedReader in = new BufferedReader( new FileReader( fileName ) );
00445             String line;
00446             while( ( line = in.readLine () ) != null ) {
00447                 sb.append( line );
00448             }
00449             in.close ();
00450 
00451             object = Base64.decodeToObject( sb.toString () );
00452         }
00453         catch( FileNotFoundException e )
00454         {
00455             Log.exception( Log.TRACE, e );
00456         }
00457         catch( IOException e )
00458         {
00459             Log.exception( Log.WARN, e );
00460         }
00461         catch( ClassNotFoundException e )
00462         {
00463             Log.exception( Log.ERROR, e );
00464         }
00465 
00466         return object;
00467     }
00468 
00469     /**
00470      *  Sanity check whether PublicEncryptor works with PrivateEncryption
00471      */
00472     private boolean sanityCheck ()
00473     {
00474         /* Random data 
00475          */
00476         byte[] plainText = new byte[ 2048 ];
00477         for ( int i = 0; i < plainText.length; ++i ) {
00478             plainText[i] = (byte)( Math.random() * 256 );
00479         }
00480         
00481         /* Simulate encryption at remote end and local decryption
00482          */
00483         PublicEncryptor testPublic = new PublicEncryptor( this.serializedPublicKey, null );
00484         byte[] cipherText = testPublic.encrypt( plainText );
00485         byte[] output =  this.decrypt( cipherText );
00486         
00487         if ( ! java.util.Arrays.equals( plainText, output ) )
00488         {
00489             Log.error( "Public encryption / private decryption sanity check failed." );
00490 
00491             Log.trace( Log.toHex( plainText ) );
00492             Log.trace( Log.toHex( cipherText ) );
00493             Log.trace( Log.toHex( output ) );
00494             
00495             return false;
00496         }
00497         
00498         return true;
00499     }
00500     
00501     /**
00502      *  Decrypts cipher text using the private key. 
00503      *  Emulates CBC (cipher-block chaining) using plain ECB.
00504      *  Why? -- Because JCE does not support RSA/CBC cipher (only RSA/ECB).
00505      *  
00506      *  See <a href="http://en.wikipedia.org/wiki/Block_cipher_modes_of_operation#Cipher-block_chaining_.28CBC.29" target="_new">
00507      *  Cipher-block chaining (CBC)</a>
00508      *  \image html cbc_decryption.png
00509      *  
00510      *  @see PublicEncryptor#encrypt
00511      */
00512     public byte[] decrypt( byte[] cipherText )
00513     {
00514         if ( this.cipher == null )  {
00515             return null;
00516         }
00517         
00518         byte[] output = null;
00519         
00520         synchronized( this.cipher )
00521         {
00522             try 
00523             {
00524                 this.cipher.init( Cipher.DECRYPT_MODE, privateKey );
00525 
00526                 int blockSize = cipher.getOutputSize( 1 );
00527                 
00528                 byte[] xorBlock = new byte[ blockSize ];
00529                 
00530                 ByteArrayOutputStream bOut = new ByteArrayOutputStream ();
00531 
00532                 for ( int pos = 0; pos < cipherText.length; pos += blockSize ) 
00533                 {
00534                     int len = Math.min( cipherText.length - pos, blockSize );
00535                     
00536                     byte[] plainBlock = this.cipher.doFinal( cipherText, pos, len );
00537                     
00538                     for ( int i = 0; i < plainBlock.length; ++i ) {
00539                         plainBlock[i] = (byte)( plainBlock[i] ^ xorBlock[i] );
00540                     }
00541                     
00542                     bOut.write( plainBlock );
00543 
00544                     System.arraycopy( cipherText, pos, xorBlock, 0, blockSize );
00545                 }
00546                 
00547                 output = bOut.toByteArray ();
00548             }
00549             catch( Exception e )
00550             {
00551                 Log.exception( Log.ERROR, e );
00552             }
00553         }
00554 
00555         return output;
00556     }
00557     
00558     /**
00559      *  Signs object using private key
00560      */
00561     public SignedObject signObject( Serializable object )
00562     {
00563         String signatureAlgorithm = digest + "with" + this.privateKey.getAlgorithm ();
00564         
00565         Signature signature = null;
00566         SignedObject result = null;
00567 
00568         try 
00569         {
00570             signature = Signature.getInstance( signatureAlgorithm );
00571             signature.initSign( this.privateKey );
00572             result = new SignedObject( object, this.privateKey, signature ); 
00573         }
00574         catch( NoSuchAlgorithmException e )
00575         {
00576             Log.exception( Log.ERROR, e );
00577         }
00578         catch( InvalidKeyException e )
00579         {
00580             Log.exception( Log.ERROR, e );
00581         }
00582         catch( SignatureException e )
00583         {
00584             Log.exception( Log.ERROR, e );
00585         }
00586         catch( IOException e )
00587         {
00588             Log.exception( Log.ERROR, e );
00589         }
00590         
00591         return result;
00592     }
00593     
00594     /**
00595      *  Reconstructs secret key from Base64 respresentation of encrypted 
00596      *  (using our public key) serialized secret key.
00597      */
00598     public SymmetricCipher deserializeEncryptedSecretKey( String serializedSecretKey )
00599     {
00600         SymmetricCipher result = null;
00601         
00602         ByteArrayInputStream bIn = null; 
00603         ObjectInputStream oIn = null;
00604         
00605         try
00606         {
00607             byte[] cipherText = Base64.decode( serializedSecretKey );
00608             byte[] plainText = decrypt( cipherText );
00609             
00610             bIn = new ByteArrayInputStream( plainText );
00611             
00612             oIn = new ObjectInputStream( bIn );
00613             
00614             Object object = oIn.readObject ();
00615             
00616             oIn.close ();
00617             bIn.close ();
00618 
00619             String verificator = null;
00620             SignedObject signedObject = null;
00621 
00622             if ( object instanceof SignedObject ) 
00623             {
00624                 signedObject = (SignedObject) object;
00625                 verificator = PublicEncryptor.verifyObject( signedObject );
00626                 object = signedObject.getObject ();
00627             }
00628             
00629             if ( object instanceof SecretKey ) 
00630             {
00631                 result = new SymmetricCipher( (SecretKey)object, verificator );
00632             }
00633             else 
00634             {
00635                 Log.error( "Invalid object when trying to deserialize encrypted secret key" );
00636             }
00637         }
00638         catch( Exception e ) 
00639         {
00640             Log.exception( Log.ERROR, e );
00641         }
00642         
00643         return result;
00644     }
00645 }

Generated on Thu Dec 16 2010 14:44:42 for VoIP Kryptofon by  doxygen 1.7.2