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

crypto/PublicEncryptor.java

Go to the documentation of this file.
00001 
00002 package crypto;
00003 
00004 import java.io.BufferedReader;
00005 import java.io.ByteArrayOutputStream;
00006 import java.io.File;
00007 import java.io.FileNotFoundException;
00008 import java.io.FileReader;
00009 import java.io.IOException;
00010 import java.io.ObjectOutputStream;
00011 import java.io.Serializable;
00012 import java.security.NoSuchAlgorithmException;
00013 import java.security.PublicKey;
00014 import java.security.Signature;
00015 import java.security.SignedObject;
00016 import java.util.ArrayList;
00017 
00018 import javax.crypto.Cipher;
00019 import javax.crypto.NoSuchPaddingException;
00020 
00021 import utils.Log;
00022 import utils.Base64;
00023 
00024 /**
00025  *  Implements public part of the asymmetric cipher (with public key) used to send 
00026  *  encrypted local secret key (used for symmetric ciphering of peer-to-peer 
00027  *  datagram packets) to remote peer.
00028  *  
00029  *  The class holds also list of authorized public keys, which is used to verify
00030  *  signed objects received from peers.
00031  *  
00032  *  @author Mikica B Kocic
00033  */
00034 public class PublicEncryptor
00035 {
00036     /**
00037      *  Padding to be used when ciphering/deciphering
00038      *  JCE does not support RSA/CBC so the CBC mode is built 
00039      *  on the top of ECB in PublicEncryptor.encrypt().
00040      */
00041     private final static String padding = "/ECB/PKCS1PADDING";
00042 
00043     /**
00044      *  Message digest used for creating/validating signatures
00045      */
00046     private final static String digest  = "SHA1";
00047 
00048     /**
00049      *  The name of the file holding authorized public keys of remote peers
00050      */
00051     private final static String authorizedKeysFile = "mykf-authorized-keys.txt";
00052 
00053     /**
00054      *  Authorized public keys (loaded from file)
00055      */
00056     private static ArrayList<NamedPublicKey> authorizedKeys = null;
00057 
00058     /**
00059      *  Public key
00060      */
00061     private PublicKey publicKey = null;
00062     
00063     /**
00064      *  Instance of the encrypting engine based on remote public key
00065      */
00066     private Cipher cipher = null;
00067     
00068     /**
00069      *  Remote public key: serialized and encoded as Base64 string.
00070      */
00071     private String serializedPublicKey = null;
00072 
00073     /**
00074      *  Contains name of the verificator (i.e the name associated with authorized public
00075      *  key that has verified this public key). Not null indicates that the public key 
00076      *  was successfully verified.
00077      */
00078     private String verificator = null;
00079 
00080     /**
00081      *  Deserializes public key from the Base64 string and instantiates PublicEncryptor.
00082      *  Verifies public key with the public key retrieved from the authorized keys.
00083      */
00084     public PublicEncryptor( String serializedPublicKey, String remoteUserId )
00085     {
00086         /* Deserialize and verify public key used for encryption.
00087          */
00088         this.serializedPublicKey = serializedPublicKey;
00089 
00090         try
00091         {
00092             Object object = Base64.decodeToObject( this.serializedPublicKey );
00093             SignedObject signedObject = null;
00094 
00095             if ( object instanceof SignedObject ) 
00096             {
00097                 signedObject = (SignedObject) object;
00098                 this.verificator = PublicEncryptor.verifyObject( signedObject );
00099                 object = signedObject.getObject ();
00100             }
00101             
00102             if ( object instanceof PublicKey ) 
00103             {
00104                 this.publicKey = (PublicKey)object;
00105                 String algorithm = this.publicKey.getAlgorithm (); 
00106                 this.cipher = Cipher.getInstance( algorithm + padding );
00107             }
00108         }
00109         catch( ClassNotFoundException e )
00110         {
00111             Log.exception( Log.WARN, e );
00112         }
00113         catch( NoSuchAlgorithmException e )
00114         {
00115             Log.exception( Log.WARN, e );
00116         }
00117         catch( NoSuchPaddingException e )
00118         {
00119             Log.exception( Log.WARN, e );
00120         }
00121         catch( IOException e )
00122         {
00123             Log.exception( Log.WARN, e );
00124         }
00125 
00126         /* If failed to create serializedPublicKey, then fail for good.
00127          */
00128         if ( this.publicKey == null )
00129         {
00130             this.cipher = null;
00131         }
00132     }
00133 
00134     /**
00135      *  Create empty authorized keys file if it does not exist
00136      *  and adjust permissions.
00137      */
00138     private static void createEmptyAuthorizedPublicKeys( String filename )
00139     {
00140         try
00141         {
00142             File file = new File( filename );
00143 
00144             if (  ! file.exists () ) {
00145                 file.createNewFile ();
00146             }
00147 
00148             if (  file.exists () )
00149             {
00150                 /* Change permissions using native OS 'chmod' command (ignoring Windows), 
00151                  * so that no one but the owner might read its contents.
00152                  */
00153                 String osName = System.getProperty( "os.name" ).toLowerCase();
00154                 if ( ! osName.matches( "^.*windows.*$" ) ) 
00155                 {
00156                     try 
00157                     {
00158                         Runtime.getRuntime().exec( new String[] { "chmod", "go=", filename } );
00159                     }
00160                     catch( IOException e ) 
00161                     {
00162                         Log.trace( "Failed to do chmod; OS = " + osName );
00163                         Log.exception( Log.TRACE, e );
00164                     }
00165                 }
00166             }
00167         }
00168         catch( Exception e )
00169         {
00170             Log.exception( Log.ERROR, e );
00171         }
00172     }
00173 
00174     /**
00175      *  Loads authorized keys
00176      */
00177     public static void loadAuthorizedPublicKeys ()
00178     {
00179         StringBuffer report = new StringBuffer ();
00180 
00181         ArrayList<NamedPublicKey> newAuthKeys = new ArrayList<NamedPublicKey> ();
00182 
00183         try 
00184         {
00185             String filePath = CipherEngine.getPrivateKeyDirectory ()
00186                             + authorizedKeysFile;
00187             
00188             createEmptyAuthorizedPublicKeys( filePath );
00189 
00190             FileReader inf = new FileReader( filePath );
00191             BufferedReader ins = new BufferedReader( inf );
00192 
00193             String line;
00194             while ( ( line = ins.readLine () ) != null ) 
00195             {
00196                 /* Split line into 'words'; Our key should be the first word 
00197                  */
00198                 String[] parts = line.trim().split( "\\s{1,}" );
00199                 
00200                 /* Skip empty lines
00201                  */
00202                 if ( parts.length < 1 || parts[0].length () <= 0 ) {
00203                     continue;
00204                 }
00205 
00206                 /* Skip lines starting with '#' (comments) 
00207                  */
00208                 if ( parts[0].equals( "#" ) ) {
00209                     continue;
00210                 }
00211 
00212                 /* Now, deserialize public key from the first word
00213                  */
00214                 String encodedKey = parts[0];
00215                 Object object = null;
00216 
00217                 try {
00218                     object = Base64.decodeToObject( encodedKey.toString () );
00219                 }
00220                 catch( IOException e )
00221                 {
00222                     Log.warn( "Failed to deserialize authorized key at line: [" + encodedKey + "]" );
00223                     Log.exception( Log.WARN, e );
00224                 }
00225                 catch( ClassNotFoundException e )
00226                 {
00227                     Log.warn( "Failed to deserialize authorized key at line: [" + encodedKey + "]" );
00228                     Log.exception( Log.WARN, e );
00229                 }
00230 
00231                 if ( object != null && ( object instanceof NamedPublicKey ) ) 
00232                 {
00233                     NamedPublicKey authKey = (NamedPublicKey) object; 
00234                     newAuthKeys.add( authKey );
00235                     if ( report.length () != 0 ) {
00236                         report.append( ", " );
00237                     }
00238                     report.append( authKey.comment );
00239                 }
00240                 else if ( object != null )
00241                 {
00242                     Log.warn( "Line: [" + encodedKey + "]" );
00243                     Log.warn( "Ignored class: " + object.getClass().toString () );
00244                 }
00245             }
00246         }
00247         catch( FileNotFoundException e )
00248         {
00249             Log.exception( Log.TRACE, e );
00250         }
00251         catch( IOException e )
00252         {
00253             Log.exception( Log.WARN, e );
00254         }
00255 
00256         if ( newAuthKeys.size() > 1 ) 
00257         {
00258             report.insert( 0,  "Loaded " + newAuthKeys.size() + " authorized keys: " );
00259             Log.attn( report.toString () );
00260         }
00261         else if ( newAuthKeys.size() == 1 ) 
00262         {
00263             report.insert( 0,  "Loaded authorized key: " );
00264             Log.attn( report.toString () );
00265         }
00266 
00267         authorizedKeys = newAuthKeys;
00268     }
00269     
00270     /**
00271      *  Verifies signed object with a public key from the authorized public keys
00272      *  
00273      *  @return not null if verified with the name associated to authorized public key
00274      */
00275     public static String verifyObject( SignedObject object )
00276     {
00277         if ( authorizedKeys == null ) {
00278             return null;
00279         }
00280 
00281         String verificator = null;
00282 
00283         for ( NamedPublicKey authKey : authorizedKeys )
00284         {
00285             try 
00286             {
00287                 String signAlgorithm = digest + "with" + authKey.publicKey.getAlgorithm ();
00288                 
00289                 Signature signature = Signature.getInstance( signAlgorithm );
00290                 
00291                 if ( object.verify( authKey.publicKey, signature ) ) {
00292                     verificator = authKey.comment;
00293                     break;
00294                 }
00295             }
00296             catch( Exception e )
00297             {
00298                 /* ignore all errors; search until exhausted keys or verify succeeds */
00299             }
00300         }
00301         
00302         return verificator;
00303     }
00304 
00305     /**
00306      *  Returns if cipher is properly initialized
00307      */
00308     public boolean isActive ()
00309     {
00310         return this.cipher != null;
00311     }
00312 
00313     /**
00314      *  Returns if public key was verified
00315      */
00316     public boolean isVerified ()
00317     {
00318         return this.verificator != null;
00319     }
00320 
00321     /**
00322      *  Returns if name of the verificator from authorized keys that verified this public key
00323      *  
00324      *  @return name of the verificator; May be null indicating not verified public key
00325      */
00326     public String getVerificatorName ()
00327     {
00328         return this.verificator;
00329     }
00330 
00331     /**
00332      *  Encrypts plain text using public key.
00333      *  Emulates CBC (cipher-block chaining) using plain ECB.
00334      *  Why? -- Because JCE does not support RSA/CBC cipher (only RSA/ECB).
00335      *
00336      *  See <a href="http://en.wikipedia.org/wiki/Block_cipher_modes_of_operation#Cipher-block_chaining_.28CBC.29" target="_new">
00337      *  Cipher-block chaining (CBC)</a>
00338      *  \image html cbc_encryption.png
00339      *  
00340      *  @see AsymmetricCipher#decrypt
00341      */
00342     public byte[] encrypt( byte[] plainText )
00343     {
00344         if ( this.cipher == null ) {
00345             return null;
00346         }
00347 
00348         byte[] output = null;
00349         
00350         synchronized( this.cipher ) 
00351         {
00352             try 
00353             {
00354                 this.cipher.init( Cipher.ENCRYPT_MODE, publicKey );
00355                 
00356                 /* cipher.getBlockSize () returns always 0, so for the RSA
00357                  * we can calculate block size as output size - 11 octets overhead.
00358                  * e.g. 117 octets for 1024-bit RSA key size (= 1024/8 - 11 )
00359                  */
00360                 int blockSize = cipher.getOutputSize( 1 ) - 11;
00361                 
00362                 ByteArrayOutputStream bOut = new ByteArrayOutputStream ();
00363                 
00364                 byte[] xorBlock = new byte[ blockSize ];
00365 
00366                 for ( int pos = 0; pos < plainText.length; pos += blockSize ) 
00367                 {
00368                     int len = Math.min( plainText.length - pos, blockSize );
00369                     
00370                     for ( int i = 0; i < len; ++i ) {
00371                         xorBlock[i] = (byte)( xorBlock[i] ^ plainText[ pos + i ] );
00372                     }
00373 
00374                     byte[] cipherBlock = this.cipher.doFinal( xorBlock, 0, len );
00375                     bOut.write( cipherBlock );
00376 
00377                     System.arraycopy( cipherBlock, 0, xorBlock, 0, blockSize );
00378                 }
00379 
00380                 output = bOut.toByteArray ();
00381             }
00382             catch( Exception e )
00383             {
00384                 Log.exception( Log.ERROR, e );
00385             }
00386         }
00387 
00388         return output;
00389     }
00390     
00391     /**
00392      *  Returns Base64 of encrypted (using our public key) object.
00393      */
00394     public String encryptAndSerialize( Serializable object )
00395     {
00396         // TODO sign object first with our public key !
00397 
00398         String result = null;
00399         
00400         /* Serialize instance of the secret key into secret key file
00401          */
00402         ByteArrayOutputStream bOut = null; 
00403         ObjectOutputStream oOut = null;
00404         
00405         try
00406         {
00407             bOut = new ByteArrayOutputStream ();
00408             oOut = new ObjectOutputStream( bOut );
00409             oOut.writeObject( object );
00410             
00411             byte[] plainText = bOut.toByteArray ();
00412             
00413             oOut.close ();
00414             bOut.close ();
00415             
00416             result = Base64.encodeBytes( encrypt( plainText ), Base64.GZIP );
00417         }
00418         catch( Exception e ) 
00419         {
00420             Log.exception( Log.ERROR, e );
00421         }
00422         
00423         return result;
00424     }
00425 }

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