/*
KeyringEditor

Copyright 2004 Markus Griessnig
Vienna University of Technology
Institute of Computer Technology

KeyringEditor is based on:
Java Keyring v0.6
Copyright 2004 Frank Taylor <[email protected]>

These programs are distributed in the hope that they will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.
*/

// Crypto.java

// 29.10.2004

// 06.11.2004: removed entryId from decrypt()
// 17.11.2004: setPassword uses char[] (security reason); enrypt rest=0 bug
// 01.12.2004: Keyring database format 5 support added
// 04.12.2004: AES support added

import java.util.*; // Arrays.equals
import javax.crypto.*; // SecretKey
import javax.crypto.spec.*; // DESedeKeySpec
import java.security.*; // MessageDigest

/**
* This class is used to encrypt and decrypt entries.
*/
public class Crypto {
       /**
        * Check for odd parity
        */
       protected static final int odd_parity[]={
                 1,  1,  2,  2,  4,  4,  7,  7,  8,  8, 11, 11, 13, 13, 14, 14,
                16, 16, 19, 19, 21, 21, 22, 22, 25, 25, 26, 26, 28, 28, 31, 31,
                32, 32, 35, 35, 37, 37, 38, 38, 41, 41, 42, 42, 44, 44, 47, 47,
                49, 49, 50, 50, 52, 52, 55, 55, 56, 56, 59, 59, 61, 61, 62, 62,
                64, 64, 67, 67, 69, 69, 70, 70, 73, 73, 74, 74, 76, 76, 79, 79,
                81, 81, 82, 82, 84, 84, 87, 87, 88, 88, 91, 91, 93, 93, 94, 94,
                97, 97, 98, 98,100,100,103,103,104,104,107,107,109,109,110,110,
               112,112,115,115,117,117,118,118,121,121,122,122,124,124,127,127,
               128,128,131,131,133,133,134,134,137,137,138,138,140,140,143,143,
               145,145,146,146,148,148,151,151,152,152,155,155,157,157,158,158,
               161,161,162,162,164,164,167,167,168,168,171,171,173,173,174,174,
               176,176,179,179,181,181,182,182,185,185,186,186,188,188,191,191,
               193,193,194,194,196,196,199,199,200,200,203,203,205,205,206,206,
               208,208,211,211,213,213,214,214,217,217,218,218,220,220,223,223,
               224,224,227,227,229,229,230,230,233,233,234,234,236,236,239,239,
               241,241,242,242,244,244,247,247,248,248,251,251,253,253,254,254};

       // Keyring database format 4
       /**
        * Salt size in byte (Database format 4)
        */
       protected static final int SALT_SIZE = 4;
       /**
        * Maximum size of salt + password in byte (Database format 4)
        */
       protected static final int MD5_CBLOCK = 64;
       /**
        * MD5 hash size in byte (Database format 4)
        */
       protected static final int MD5_DIGEST_LENGTH = 16;
       /**
        * Cipher block size in byte (Database format 4)
        */
       protected static final int KDESBLOCKSIZE = 8;

       // Keyring database format 4
       /**
        * Password information (Database format 4)
        */
       protected byte[] recordZero; // password information
       /**
        * Databse version
        */
       private int version;

       // Keyring database format 5
       /**
        * Salt (Database format 5)
        */
       protected byte[] salt = new byte[8];
       /**
        * Hash (Database format 5)
        */
       protected byte[] hash = new byte[8];
       /**
        * Iterations (Database format 5)
        */
       protected int iter;
       /**
        * Cipher type (Database format 5),
        * NO_CIPHER = 0,
        * DES3_EDE_CBC_CIPHER = 1,
        * AES_128_CBC_CIPHER = 2,
        * AES_256_CBC_CIPHER = 3
        */
       protected int type; // keyring: cipher

       protected SecretKey key = null;
       protected Cipher cipher = null;

       /**
        * Constructor for database format 4.
        *
        * @param recordZero Password information
        * @param version equals 4
        */
       public Crypto(byte[] recordZero, int version) {
               this.recordZero = recordZero; // null if Keyring database format 5
               this.version = version;
       }

       /**
        * Constructor for database format 5.
        *
        * @param recordZero equals null
        * @param version equals 5
        * @param salt Salt
        * @param hash Hash
        * @param iter Iterations
        * @param type Cipher type
        */
       public Crypto(byte[] recordZero, int version, byte[] salt, byte[] hash, int iter, int type) {
               this.recordZero = recordZero; // null if Keyring database format 5
               this.version = version;
               this.salt = salt;
               this.hash = hash;
               this.iter = iter;
               this.type = type;
       }

       // setPassword ------------------------------------------------
       //
       // Source: keyring-link-0.1.1/keyring.c (gnukeyring.sourceforge.net)
       //
       // The master password is not stored in the database. Instead,
       // an MD5 hash of the password and a random 32-bit salt is stored and
       // checked against entered values. (Keyring crypto)
       //
       /**
        * This method calls the setPassword method according to database version.
        *
        * @param password Database password
        */
       public void setPassword(char[] password) throws Exception {
               switch(version) {
                       case 4: setPassword_4(password); break;
                       case 5: setPassword_5(password); break;
               }
       }

       /**
        * This method checks the entered password and generates record encryption key for database format 5.
        *
        * @param password Database password
        */
       public void setPassword_5(char[] password) throws Exception {
               int index;
               int[] cipherlen = {0, 24, 16, 32}; // keylength in byte
               byte[] pass = new byte[password.length];

               for(int i=0;i<password.length;i++) {
                       pass[i] = (byte)(0xFF & password[i]);
               }

               // PKCS#5 PBKDF2
               // Key Derivation function
               byte[] deskey = pbkdf2(pass, salt, iter, cipherlen[type]);

               // set odd parity
               if(type == 1) { // TripleDES
                       for(int i=0; i<24; i++) {
                               index = (int)(0xff & deskey[i]);
                               deskey[i] = (byte)odd_parity[index];
                       }
               }

               // SHA1
               byte[] digest = getMessageDigest(deskey, salt);

               byte[] help = Model.sliceBytes(digest, 0, hash.length);

               // check password
               if(!Arrays.equals(hash, help)) {
                       throw new Exception("Password incorrect.");
               }

               // for security reason set each element to zero
               for(int i=0;i<pass.length;i++) {
                       pass[i] = 0;
               }

               // setup cipher according to cipher type
               switch(type) {
                       case 1:
                       key = SecretKeyFactory.getInstance("DESede").generateSecret(new DESedeKeySpec(deskey));
                               cipher = Cipher.getInstance("TripleDES/CBC/NoPadding");
                               break;
                       case 2:
                               key = new SecretKeySpec(Model.sliceBytes(deskey, 0, 16), "AES"); // 128 bit
                               cipher = Cipher.getInstance("AES/CBC/NoPadding");
                               break;
                       case 3:
                               key = new SecretKeySpec(Model.sliceBytes(deskey, 0, 32), "AES"); // 256 bit
                               cipher = Cipher.getInstance("AES/CBC/NoPadding");
                               break;
                       default:
                               throw new Exception("Cipher " + type + " not supported.");
               }
       }

       /**
        * This method returns a SHA1 Message digest.
        *
        * @param key Key
        * @param salt Salt
        *
        * @return SHA1 Message digest
        */
       public byte[] getMessageDigest(byte[] key, byte[] salt) throws Exception {
               MessageDigest md = MessageDigest.getInstance("SHA1");
               md.update(key);
               md.update(salt);

               return md.digest();
       }

       /**
        * This method is an implementation of PKCS#5 PBKDF2.
        *
        * @param pass Database password
        * @param salt Salt
        * @param iter Iterations
        * @param keylen Keylength of choosen cipher type
        *
        * @return Record encryption key
        */
       public byte[] pbkdf2(byte[] pass, byte[] salt, int iter, int keylen) throws Exception {
               // PKCS#5 PBKDF2
               // Key Derivation function
               int SHA_DIGEST_LENGTH = 20;
               int blocklen;
               int i = 1;
               byte itmp[] = new byte[4];
               int pos = 0;
               byte digtmp[] = new byte[SHA_DIGEST_LENGTH];
               byte p[] = new byte[keylen];
               int j, k;

           Mac mac = Mac.getInstance("HmacSHA1");
       SecretKeySpec key = new SecretKeySpec(pass, "HmacSHA1");
       mac.init(key);

               while(keylen > 0) {

                       if(keylen > SHA_DIGEST_LENGTH)
                               blocklen = SHA_DIGEST_LENGTH;
                       else
                               blocklen = keylen;

                       itmp[0] = (byte)(0xff & (i >> 24));
                       itmp[1] = (byte)(0xff & (i >> 16));
                       itmp[2] = (byte)(0xff & (i >> 8));
                       itmp[3] = (byte)(0xff & i);

                       mac.reset();
                       mac.update(salt);
                       digtmp = mac.doFinal(itmp);
                       System.arraycopy(digtmp, 0, p, pos, blocklen);

                       for(j = 1; j < iter; j++) {
                               mac.reset();
                               digtmp = mac.doFinal(digtmp);

                               for(k = 0; k < blocklen; k++) p[pos+k] ^= digtmp[k];
                       }

                       keylen = keylen - blocklen;
                       pos = pos + blocklen;
                       i++;
               }

               return p;
       }

       /**
        * This method is an implementation of RFC 2104 (HMAC). Not used.
        */
       public byte[] hmac(byte[] text, byte[] key, String hashfunction) throws Exception {
               // rfc 2104
               int BLOCKSIZE = 64; // byte length
               byte[] ipad = new byte[BLOCKSIZE];
               byte[] opad = new byte[BLOCKSIZE];
               byte[] digest;

               Arrays.fill(ipad, (byte)0x00);
               Arrays.fill(opad, (byte)0x00);
               System.arraycopy(key, 0, ipad, 0, key.length);
               System.arraycopy(key, 0, opad, 0, key.length);

               for(int i=0;i<BLOCKSIZE;i++) {
                       ipad[i] = (byte)(0x36 ^ ipad[i]);
                       opad[i] = (byte)(0x5C ^ opad[i]);
               }

               // hashfunction = "MD5" or "SHA1"
               MessageDigest md = MessageDigest.getInstance(hashfunction);
               md.update(ipad);
               md.update(text);
               digest = md.digest();

               md.reset();
               md.update(opad);
               md.update(digest);
               digest = md.digest();

               return digest;
       }

       /**
        * This method checks the entered password and generates record encryption key for database format 4.
        *
        * @param password Database password
        */
       public void setPassword_4(char[] password) throws Exception {
               byte[] hash;
               byte[] desKeyData = new byte[24]; // 8 byte * 3
               byte[] snib;
               int i;

               byte[] pass = new byte[password.length];

               for(i=0;i<password.length;i++) {
                       pass[i] = (byte)(0xFF & password[i]);
               }

               // for security reason set each element to zero
               for(i=0;i<password.length;i++) {
                       password[i] = 0;
               }

               // Keyring supports passwords of up to 40 characters
               if(pass.length > 40) {
                       throw new Exception("Password too long.");
               }

               // check password
               hash = checkPasswordHash_4(recordZero, pass);

               if(!Arrays.equals(hash, Model.sliceBytes(recordZero, SALT_SIZE, MD5_DIGEST_LENGTH))) {
                       throw new Exception("Password incorrect.");
               }

               // --------------------------------------------------------
               // generate record encryption key
               // --------------------------------------------------------

               /*
               The master password is also used to generate a record encryption key.
               The 128-bit MD5 hash of the master password is split into two 64-bit keys, K1 and K2.
               (DES ignores the top bit of each byte, so the key has 112 effective unknown bits.)
               These are used to generate record data encrypted as Enc(K1, Dec(K2, Enc(K1, Data))).
               Each 8-byte data block is independently encrypted by the same key.
               (Keyring crypto)
               */

               // calc_snib()
               MessageDigest md = MessageDigest.getInstance("MD5");
               md.update(pass);
               snib = md.digest(); // 128 bit md5 hash
               //System.out.println("snib.length = " + snib.length);

               // generate the DES keypair (snib = A,B; desKeyData = A,B,A)
               for(i=0; i<16; i++) {
                       desKeyData[i] = snib[i];

                       if(i < 8) {
                               desKeyData[i + 16] = snib[i];
                       }
               }

               // setup SecretKey and Cipher
               key = SecretKeyFactory.getInstance("DESede").generateSecret(new DESedeKeySpec(desKeyData));

               cipher = Cipher.getInstance("TripleDES/ECB/NoPadding");
               // ECB Electronic Codebook Mode

               // for security reason set each element to zero
               for(i=0;i<pass.length;i++) {
                       pass[i] = 0;
               }
       }

       /**
        * This method generates a MD5 hash of the password and the salt.
        *
        * @param pass Database password
        * @param salt Salt
        *
        * @return MD5 hash
        */
       protected byte[] checkPasswordHash_4(byte[] salt, byte[] pass) throws Exception {
               byte[] digest = new byte[MD5_DIGEST_LENGTH]; // 128 bit md5 hash
               byte[] msg = new byte[MD5_CBLOCK]; // salt + password
               int i;

               Arrays.fill(msg, (byte)0);

               // 32 bit salt
               for(i=0; i<SALT_SIZE; i++) {
                       msg[i] = salt[i];
               }

               // strncpy(msg + kSaltSize, pass, MD5_CBLOCK - 1 - kSaltSize);
               for(i=0; i<pass.length; i++) {
                       msg[i + SALT_SIZE] = pass[i]; // S A L T P A S S W O R D 0 ...
               }

               MessageDigest md = MessageDigest.getInstance("MD5");
               md.update(msg);
               digest = md.digest(); // output: 128 bit digest of entered password

               // return !memcmp(digest, rec0+kSaltSize, MD5_DIGEST_LENGTH);
               return digest;
       }

       // decrypt ----------------------------------------------------
       /**
        * This method calls the decrypt method according to database version.
        *
        * @param cipherText Text to decrypt
        * @param fieldName Field to return (account, password, notes, datetype)
        * @param iv Initialisation vector (only in database format 5)
        *
        * @return String with decrypted text or null if no fieldName was specified
        */
       public Object decrypt(byte[] cipherText, String fieldName, byte[] iv) {
               Object temp = null;

               switch(version) {
                       case 4: temp = decrypt_4(cipherText, fieldName); break;
                       case 5: temp = decrypt_5(cipherText, fieldName, iv); break;
               }

               return temp;
       }

       // Keyring database format 5
       /**
        * This method decrypts a ciphertext in database format 5.
        *
        * @param encrypted Text to decrypt
        * @param fieldName Field to return (account, password, notes, datetype)
        * @param iv Initialisation vector (only in database format 5)
        *
        * @return String with decrypted text or null if no fieldName was specified
        */
       public Object decrypt_5(byte[] encrypted, String fieldName, byte[] iv) {
       String account = null;
       String password = null;
       String notes = null;
       byte[] datetype = null;
       byte[] plain = null;
               int pos = 0;
               int len;
               int reallen;
               int label;

           AlgorithmParameters params = null;

           try {
                       // initialize
               switch(type) {
                       case 1: // TripleDES
                               params = AlgorithmParameters.getInstance("DES");
                                       params.init(new IvParameterSpec(iv));
                                       cipher.init(Cipher.DECRYPT_MODE, key, params);
                                       plain = cipher.update(encrypted);
                               break;
                       case 2: // AES 128 bit
                       case 3: // AES 256 bit
                               params = AlgorithmParameters.getInstance("AES");
                                       params.init(new IvParameterSpec(iv));
                                       cipher.init(Cipher.DECRYPT_MODE, key, params);
                                       plain = cipher.doFinal(encrypted);
                                       //plain = cipher.update(encrypted);

                               break;
               }
               }
               catch(Exception e) {
               e.printStackTrace(System.err);
               return "Could not decrypt data.";
               }

               // mg
               //Model.printHexByteArray("decrypt_5", encrypted);
               //Model.printHexByteArray("decrypt_5", plain);

               len = (int)Model.sliceNumber(plain, pos, 2); // length of field

               while(len != 0xffff) {
                       reallen = (len + 1) & ~1; // padding for next even address

                       label = (int)Model.sliceNumber(plain, pos + 2, 1);

                       //System.out.println(pos + ": type=" + type + ", len" + len);

                       switch(label) {
                               case 1: account = Model.sliceString(plain, pos + 4, len); break;
                               case 2: password = Model.sliceString(plain, pos + 4, len); break;
                               case 3: datetype = Model.sliceBytes(plain, pos + 4, 2); break;
                               case 255: notes = Model.sliceString(plain, pos + 4, len); break;
                       }

                       pos = pos + reallen + 4;

                       if(pos < (plain.length - 2))
                               len = (int)Model.sliceNumber(plain, pos, 2);
                       else
                               len = 0xffff;
               }

               if(fieldName.equals("account")) return (Object)account;
               if(fieldName.equals("password")) return (Object)password;
               if(fieldName.equals("notes")) return (Object)notes;
               if(fieldName.equals("datetype")) return (Object)datetype;

               return null; // no fieldname specified
       }

       /**
        * This method decrypts a ciphertext in database format 4.
        *
        * @param encrypted Text to decrypt
        * @param fieldName Field to return (account, password, notes, datetype)
        *
        * @return String with decrypted text or null if no fieldName was specified
        */
       public Object decrypt_4(byte[] cipherText, String fieldName) {
           int posPlain = 0;
               int nextItem = 0;
               int i, j;
               int len = cipherText.length;
               int rest;
               byte[] plainText = new byte[len];
               byte[] buffer;
               byte[] buffer2;

               try {
                       cipher.init(Cipher.DECRYPT_MODE, key);

                       // ECB: 8 byte blocks
                       for(i=0; i<(len / KDESBLOCKSIZE); i++) {
                               buffer = cipher.update(cipherText, i * KDESBLOCKSIZE, KDESBLOCKSIZE);
                               for(j=0; j<buffer.length; j++) {
                                       plainText[posPlain++] = buffer[j];
                               }
                       }

                       rest = len % KDESBLOCKSIZE;

                       buffer = new byte[KDESBLOCKSIZE];
                       for(i=0; i<KDESBLOCKSIZE; i++) {
                               buffer[i] = (i < rest) ? cipherText[len - rest + i] : 0;
                               // zero padding to get 8 bytes
                       }

                       buffer2 = cipher.doFinal(buffer);
                       for(j=0; j<rest; j++) {
                               plainText[posPlain++] = buffer2[j];
                       }
               }
               catch(Exception e) {
                       return "Could not decrypt data.";
               }

               //Model.printHexByteArray("decrypt", plainText);

               // get account, password & notes
               String account = Model.sliceString(plainText, nextItem, -1);
               nextItem += account.length() + 1;

               String password = Model.sliceString(plainText, nextItem, -1);
               nextItem += password.length() + 1;

               String notes = Model.sliceString(plainText, nextItem, -1);
               nextItem += notes.length() + 1;

               byte[] datetype = Model.sliceBytes(plainText, nextItem, 2);

               if(fieldName.equals("account")) return (Object)account;
               if(fieldName.equals("password")) return (Object)password;
               if(fieldName.equals("notes")) return (Object)notes;
               if(fieldName.equals("datetype")) return (Object)datetype;

               return (Object)plainText; // no fieldname specified
       }

       // encrypt ----------------------------------------------------
       /**
        * This method calls the encrypt method according to database version.
        *
        * @param plainText Text to encrypt
        *
        * @return Encrypted text
        */
       public byte[] encrypt(byte[] plainText) throws Exception {
               byte[] temp = null;

               switch(version) {
                       case 4: temp = encrypt_des_aes(plainText, 8); break;
                       case 5:
                               switch(type) {
                                       case 1: temp = encrypt_des_aes(plainText, 8); break;
                                       case 2: temp = encrypt_des_aes(plainText, 16); break;
                                       case 3: temp = encrypt_des_aes(plainText, 16); break;
                               }
                               break;
               }

               return temp;
       }

       /**
        * This method encrypts a text.
        *
        * @param plainText Text to encrypt
        * @param blocksize AES 16 byte, TripleDES 8 byte
        *
        * @return 16 byte IV + Encrypted text
        */
       public byte[] encrypt_des_aes(byte[] plainText, int blocksize) throws Exception {
               int clen;
               int i, j;
               int cpos = 0;
               int rest;
               byte[] buffer;
               byte[] buffer2;
               byte[] cipherText;
               byte[] iv;

               int plen = plainText.length;

               // blocksize byte blocks
               // TripleDES 8 byte
               // AES 16 byte
               if(plen % blocksize != 0) {
                       clen = plen + (blocksize - (plen % blocksize));
               }
               else {
                       clen = plen;
               }

               cipherText = new byte[clen];

               // initialize cipher
               cipher.init(Cipher.ENCRYPT_MODE, key);

               iv = cipher.getIV();
               //if(iv != null) {
               //      System.out.println("iv: " + iv.length);
               //}

               // encrypt data in length of blocksize
               for(i=0; i<(plen / blocksize); i++) {
                       buffer = cipher.update(plainText, i * blocksize, blocksize);
                       for(j=0; j<buffer.length; j++) {
                               cipherText[cpos++] = buffer[j];
                       }
               }

               // last block of data does not have full blocksize
               rest = plen % blocksize;
               if(rest != 0) {
                       buffer = new byte[blocksize];
                       for(i=0; i<blocksize; i++) {
                               buffer[i] = (i < rest) ? plainText[plen - rest + i] : 0;
                       }

                       buffer2 = cipher.doFinal(buffer);

                       for(j=0; j<blocksize; j++) {
                               cipherText[cpos] = buffer2[j];
                               cpos++;
                       }
               }

               // return: 16 byte iv + cipherText
               byte[] temp = new byte[16 + cipherText.length];
               Arrays.fill(temp, (byte)0);

               // Keyring database format 5
               if(iv != null) {
                       System.arraycopy(iv, 0, temp, 0, iv.length);
               }

               System.arraycopy(cipherText, 0, temp, 16, cipherText.length);
               return temp;
       }
}