org.panbox.core.crypto.Obfuscator.java Source code

Java tutorial

Introduction

Here is the source code for org.panbox.core.crypto.Obfuscator.java

Source

/*
 * 
 *               Panbox - encryption for cloud storage 
 *      Copyright (C) 2014-2015 by Fraunhofer SIT and Sirrix AG 
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it 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.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * Additonally, third party code may be provided with notices and open source
 * licenses from communities and third parties that govern the use of those
 * portions, and any licenses granted hereunder do not alter any rights and
 * obligations you may have under such open source licenses, however, the
 * disclaimer of warranty and limitation of liability provisions of the GPLv3 
 * will apply to all the product.
 * 
 */
package org.panbox.core.crypto;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
import java.util.zip.Inflater;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.log4j.Logger;
import org.bouncycastle.util.Arrays;
import org.panbox.OS;
import org.panbox.OS.OperatingSystem;
import org.panbox.PanboxConstants;
import org.panbox.core.LimitedHashMap;
import org.panbox.core.Utils;
import org.panbox.core.exception.MissingIVException;
import org.panbox.core.exception.ObfuscationException;

public class Obfuscator {

    private static Cipher encryptCipher;
    private static boolean zip = false;
    private static final Logger logger = Logger.getLogger("org.panbox.core");

    public static final String IV_POOL_PATH = PanboxConstants.PANBOX_SHARE_METADATA_DIRECTORY + File.separator
            + "IVPool";

    private String sharePath;
    private String shareName;
    private AbstractObfuscatorIVPool ivPoolImpl;
    private OperatingSystem os;

    public Obfuscator(String sharePath, AbstractObfuscatorIVPool ivPoolImpl, String shareName)
            throws ObfuscationException {
        this.sharePath = sharePath;
        this.shareName = shareName;
        this.ivPoolImpl = ivPoolImpl;
        this.os = OS.getOperatingSystem();

        try {
            encryptCipher = Cipher.getInstance(KeyConstants.OBFUSCATOR_ALG);
            this.lookupvalueDigest = MessageDigest.getInstance(KeyConstants.IV_LOOKUP_HASH_ALG);
            this.ivDigest = MessageDigest.getInstance(KeyConstants.IV_LOOKUP_HASH_ALG);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
            encryptCipher = null;
            logger.fatal("Could not initialize cipher or digest - Fatal error!", e);
            throw new ObfuscationException("Unable to create obfuscator - Could not initialize cipher or digest!",
                    e);
        }

        // init IV pool sub directories
        initIVPoolSubDirs();
    }

    private void initIVPoolSubDirs() {
        for (int i = 0; i < Utils.hexArray.length; i++) {
            char cur = Utils.hexArray[i];
            File dirs = new File(this.sharePath + File.separator + IV_POOL_PATH + File.separator + cur);
            dirs.mkdirs();
        }
    }

    private synchronized File getIVPoolSubDir(char firstChar) {
        return new File(this.sharePath + File.separator + IV_POOL_PATH + File.separator + firstChar);
    }

    // realFile -> obfuscatedFile
    private LimitedHashMap<String, String> cachedObfuscatedNames = new LimitedHashMap<String, String>(1024 * 10);

    // obfuscatedFile -> realFile
    private LimitedHashMap<String, String> cachedDeObfuscatedNames = new LimitedHashMap<String, String>(1024 * 10);

    private MessageDigest lookupvalueDigest;

    private synchronized byte[] createLookupHash(String obfuscatedFileName, SecretKey obKey)
            throws ObfuscationException {
        try {
            lookupvalueDigest.update(obfuscatedFileName.getBytes(PanboxConstants.STANDARD_CHARSET));
            lookupvalueDigest.update(obKey.getEncoded());
            return lookupvalueDigest.digest();

        } catch (UnsupportedEncodingException e) {
            logger.error("Unsupported encoding", e);
            throw new ObfuscationException("Error creating IV lookup value due to unsupported encoding!", e);
        }
    }

    /**
     * Deobfuscates a string with the given key and encodes it with Base64
     * 
     * @param str
     *            - string to deobfuscate
     * @param key
     *            - symmetric key used to decrypt the string
     * @return - deobfuscated string
     * @throws MissingIVException
     *             if for a given filename, no IV sidecar file could be found.
     *             base64LookupHash will be set in this exception
     * @throws ObfuscationException
     */

    public synchronized String deObfuscate(String str, SecretKey key)
            throws MissingIVException, ObfuscationException {

        logger.debug("Obfuscator(" + this.shareName + ") DeObfuscate(String, key): " + str);

        // lookup in cache
        if (cachedDeObfuscatedNames.containsKey(str)) {
            logger.debug("DeObfuscate(String, key) - return cached value for: " + str);
            return cachedDeObfuscatedNames.get(str);
        }

        // if not in cache, do processing
        byte[] lookupHash = createLookupHash(str, key);
        String sLookupHash = Utils.bytesToHex(lookupHash);

        byte[] iv = ivPoolImpl.getCachedIV(sLookupHash, this.shareName);

        if (null == iv) {
            // re-read IVs from file-system or server
            logger.debug("Obfuscator: RE-fetch IV Pool");
            ivPoolImpl.fetchIVPool(this.sharePath, this.shareName);
            iv = ivPoolImpl.getCachedIV(sLookupHash, this.shareName);

            // // "direct" IV lookup
            // char firstChar = base64LookupHash.toUpperCase().charAt(0);
            // File path = getIVPoolSubDir(firstChar);
            // File[] res = path.listFiles((FileFilter) new PrefixFileFilter(
            // base64LookupHash));
            // iv = Utils.hexToBytes(res[0].getName().substring(41));

        }

        if (iv == null) {
            // this may be due to the CSP trying to resolve a conflict by
            // renaming two conflicting copies of a file. in this case, the
            // obfuscator's filename conflict resolution handler need to be
            // called to try to recover this situation. however, this needs to
            // be done *outside* of this method as we need full context (i.e.
            // absolute filename etc)

            // error cannot decode
            logger.error("Could not find IV for hash: " + sLookupHash + " generated from obfuscate file: " + str);
            throw new MissingIVException(
                    "Could not find IV for hash: " + sLookupHash + " generated from obfuscate file: " + str,
                    sLookupHash);
        }

        String originalName = null;
        try {
            originalName = decryptFileName(str, key.getEncoded(), iv, false, EncodingType.BASE64);
        } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException
                | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException
                | DataFormatException | IOException e) {
            logger.error("Could not deobfuscate string: " + str, e);
            throw new ObfuscationException("Could not decrypt string: " + str, e);
        }

        // do caching
        cachedDeObfuscatedNames.put(str, originalName);

        logger.debug("Obfuscator(" + this.shareName + ") DeObfuscate(String, key)-return: " + originalName);
        return originalName;

    }

    private MessageDigest ivDigest;

    private synchronized byte[] createIV(String originalFileName, SecretKey obKey) throws ObfuscationException {
        try {
            ivDigest.update(originalFileName.getBytes(PanboxConstants.STANDARD_CHARSET));
            ivDigest.update(obKey.getEncoded());
            byte[] hash = ivDigest.digest();

            // truncate to IV size
            return Arrays.copyOf(hash, KeyConstants.SYMMETRIC_BLOCK_SIZE);

        } catch (UnsupportedEncodingException e) {
            logger.error("Unsupported encoding", e);
            throw new ObfuscationException("Error creating IV for filename due to unsupported encoding!", e);
        }
    }

    private synchronized void createIVFile(byte[] iv, String encryptedName, SecretKey key)
            throws ObfuscationException {
        // store IV in pool
        String ivhex = Utils.bytesToHex(iv);
        byte[] lookupHashBytes = createLookupHash(encryptedName, key);
        String lookupHashStr = Utils.bytesToHex(lookupHashBytes);

        char firstChar = lookupHashStr.toUpperCase().charAt(0);

        File dirs = getIVPoolSubDir(firstChar);

        // java based file creation
        File ivFile = new File(dirs.getAbsolutePath() + File.separator + lookupHashStr + ivhex);
        try {
            ivFile.createNewFile();
        } catch (IOException e) {
            logger.error("Unable to create IV sidecar file!", e);
            throw new ObfuscationException("Unable to create IV sidecar file!", e);
        }
    }

    /**
     * Obfuscates a string with the given key and encodes it with a fixed
     * encoding
     * 
     * @param str
     *            - string to obfuscate
     * @param key
     *            - Symmetric key to encrypt string with
     * @param createiv
     *            - if set to <code>true</code>, the corresponding sidecar file
     *            within the IV pool will be created during obfuscation
     * @return - A string encoded in Base64, representing the obfuscated string
     * @throws ObfuscationException
     *             if obfuscation failed for some reason
     */
    public synchronized String obfuscate(String str, SecretKey key, boolean createiv) throws ObfuscationException {

        logger.debug("Obfuscator(" + this.shareName + ") Obfuscate(String, key): " + str + " with createiv=? "
                + createiv);
        String encryptedName = null;
        if (cachedObfuscatedNames.containsKey(str)) {
            encryptedName = cachedObfuscatedNames.get(str);

            // logger.debug("obfuscate() return cached value " + encryptedName +
            // " for plain str: " + str);

            // Remove to fix bug with lookup value pointing to different IVs
            // if (createiv) {
            // createIVFile(createIV(str, key), encryptedName, key);
            // }
        } else {

            // logger.debug("obfuscate() do new obfuscation for plain str: " +
            // str);

            // calculate IV
            byte[] iv = createIV(str, key);

            try {
                encryptedName = encryptFileName(str, key, iv, zip, EncodingType.BASE64);
            } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException
                    | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException
                    | IOException e) {
                logger.error("Could not obfuscate string: " + str, e);
                throw new ObfuscationException("Could not obfuscate string: " + str, e);
            }

            if (createiv) {
                createIVFile(iv, encryptedName, key);
                // do caching, but only if the IV-file was created
                cachedObfuscatedNames.put(str, encryptedName);
            }

        }
        logger.debug("Obfuscator(" + this.shareName + ") Obfuscate(String, key)-return: " + encryptedName);
        return encryptedName;
    }

    private String encryptFileName(String filename, SecretKey key, byte[] iv, boolean zip, EncodingType encoding)
            throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
            InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, IOException {
        // System.out.println("Original String ("+filename.length()+" chars):\t"
        // + filename);

        IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
        encryptCipher.init(Cipher.ENCRYPT_MODE, key, ivParameterSpec);

        byte[] encryptedBytes;
        if (zip) {
            // test zip filename
            byte[] zippedFileName = zipFileName(filename);

            // System.out.println("Zip ("
            // + ((float) zippedFileName.length / (float) filename
            // .getBytes().length) * 100 + "%) OrigName bytes: "
            // + filename.getBytes().length + " Zipped bytes: "
            // + zippedFileName.length);
            encryptedBytes = encryptCipher.doFinal(zippedFileName);
        } else {
            // if we do not have UTF-8, convert the filename to UTF-8
            byte[] bytes = filename.getBytes(PanboxConstants.STANDARD_CHARSET);
            // encryptedBytes = encryptCipher.doFinal(filename.getBytes());
            encryptedBytes = encryptCipher.doFinal(bytes);
        }

        // String text = Base64.encodeBase64URLSafeString(encryptedBytes);
        // System.out.println("Base64-Url String ("+base64Text.length()+" chars):\t"
        // + base64Text);
        String text = EncodingHelper.encodeByte(encryptedBytes, encoding);

        return text;
    }

    private String decryptFileName(String filename, byte[] key, byte[] iv, boolean zip, EncodingType encoding)
            throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
            InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, DataFormatException,
            IOException {
        // byte[] encFileName = Base64.decodeBase64(filename);
        byte[] encFileName = EncodingHelper.decodeString(filename, encoding);

        Cipher decryptCipher = encryptCipher;

        IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
        SecretKeySpec aesKeySpec = new SecretKeySpec(key, "AES");

        decryptCipher.init(Cipher.DECRYPT_MODE, aesKeySpec, ivParameterSpec);

        byte[] decryptedBytes = decryptCipher.doFinal(encFileName);

        String decoded;

        if (zip) {
            decoded = unzipFileName(decryptedBytes);
        } else {
            // decoded = new String(decryptedBytes);

            // since we now force the conversion to UTF-8, we expect the
            // decrypted filename to be in UTF-8
            decoded = new String(decryptedBytes, PanboxConstants.STANDARD_CHARSET);
        }

        // System.out.println("Decrypted String:\t" + decoded);

        return decoded;
    }

    /**
     * Obfuscates a full path with the given key and encodes it with a fixed
     * encoding
     * 
     * @param path
     *            - path to obfuscate
     * @param key
     *            - Symmetric key to encrypt string with
     * @param createiv
     *            - if set to <code>true</code>, the corresponding sidecar files
     *            within the IV pool will be created during obfuscation
     * @return - obfuscated path
     * @throws ObfuscationException
     *             if obfuscation failed for some reason
     */
    public String obfuscatePath(String path, SecretKey key, boolean createivs) throws ObfuscationException {

        logger.debug("ObfuscatePath: " + path);

        String[] dirs = {};
        String newPath = "";

        if (!path.equals(File.separator)) {
            String splitter = File.separator.toString();
            if (os.isWindows()) {
                // WINx
                splitter += File.separator;
            }
            if (path.startsWith(File.separator)) {
                dirs = path.substring(1).split(splitter);
            } else {
                dirs = path.split(splitter);
            }
            for (String dir : dirs) {
                newPath += File.separator + obfuscate(dir, key, createivs);
            }
        } else {
            logger.debug("ObfuscatePath-return: " + path);
            return path;
        }
        logger.debug("ObfuscatePath-return: " + newPath);
        return newPath;
    }

    public String deObfuscatePath(String path, SecretKey obfuscationKey)
            throws MissingIVException, ObfuscationException {
        logger.debug("DeObfuscatePath: " + path);

        String[] dirs = {};
        StringBuffer newPath = new StringBuffer(path);

        if (!newPath.toString().equals(File.separator)) {
            if (os.isWindows()) {
                // WINx
                dirs = newPath.substring(1).split(File.separator + File.separator);
            } else {
                // LINUX
                dirs = newPath.substring(1).split(File.separator);
            }
            newPath.delete(0, newPath.length());
            for (String dir : dirs) {
                newPath.append(File.separator);
                newPath.append(deObfuscate(dir, obfuscationKey));
            }
        }
        logger.debug("DeObfuscatePath-return: " + newPath.toString());
        return newPath.toString();
    }

    /**
     * Method for handling obfuscated filename conflicts. Checks, if the last
     * part of the given absolute filename may have been marked as a conflicting
     * copy by the CSP, tries to resolve and deobfuscate the original filename,
     * and, if successful generates a proposal for a new obfuscated filename
     * which incorporates the conflict-indicating suffix.
     * 
     * @param path
     *            obfuscated absolute path
     * @param obfuscationKey
     * @return proposed new obfuscated filename, or <code>null</code>, if
     *         conflict resolution was not successful
     */
    public synchronized String resolveConflictCandidate(String path, SecretKey key) throws ObfuscationException {
        logger.debug("Checking potential conflict candidate " + path);
        String[] dirs = {};
        StringBuffer newPath = new StringBuffer(path);

        if (!path.equals(File.separator)) {
            if (os.isWindows()) {
                // WINx
                dirs = newPath.substring(1).split(File.separator + File.separator);
            } else {
                // LINUX
                dirs = newPath.substring(1).split(File.separator);
            }

            String conflictingName = dirs[dirs.length - 1];
            // lookup-value should be at the beginning of the filename,
            // conflict-indicating suffix is usually appended after some
            // whitespace
            String[] conflictParts = conflictingName.split("\\s+");
            String obfResolvedName = null;
            if (conflictParts.length > 1) {
                // check if we can find an IV for the conflict prefix value
                try {
                    String deobfConflict = deObfuscate(conflictParts[0], key);
                    // we were able to deobfuscate the prefix, now resolve
                    // conflict
                    logger.debug("Identified conflict for filename " + conflictingName);

                    // new filename is obfuscate(deobfuscatedprefix +
                    // conflict-indicating suffix)
                    // TODO: regex interpretation of typical conflict sufixes
                    String conflictIndicatorSuffix = conflictingName.substring(conflictParts[0].length());
                    String resolvedName = deobfConflict + conflictIndicatorSuffix;
                    // create iv sidecar file in advance
                    obfResolvedName = obfuscate(resolvedName, key, true);

                    logger.debug("Resolved conflicting filename to " + resolvedName + ", created obfuscated value: "
                            + obfResolvedName);
                } catch (MissingIVException e) {
                    logger.error("Unable to resolve conflict for " + path, e);
                    // ObfuscationException causing this problem already has
                    // been thrown before, just return null at this point.
                    return null;
                }

            } else {
                logger.error("Unable to determine conflict-indicating suffix in filename " + path);
                return null;
            }

            newPath.delete(0, newPath.length());
            for (int i = 0; i < dirs.length - 1; i++) {
                newPath.append(File.separator);
                newPath.append(dirs[i]);
            }

            // append obfuscated resolved filename
            newPath.append(File.separator);
            newPath.append(obfResolvedName);

            // return proposed new filename
            logger.debug("Return proposed obfuscated conflict resolution filename " + newPath.toString());
            return newPath.toString();
        } else {
            return null;
        }
    }

    // old stuff

    // @Deprecated
    // public static synchronized String obfuscateWithZip(String filename,
    // SecretKey key, byte[] iv, EncodingType encoding) {
    // try {
    // return encryptFileName(filename, key, iv, true, encoding);
    // } catch (InvalidKeyException | NoSuchAlgorithmException
    // | NoSuchPaddingException | InvalidAlgorithmParameterException
    // | IllegalBlockSizeException | BadPaddingException | IOException e) {
    // logger.error("Could not obfuscate and zip string: " + filename, e);
    // }
    // return null;
    // }
    //
    // @Deprecated
    // public static synchronized String deObfuscateWithZip(String filename,
    // byte[] key, byte[] iv, EncodingType encoding) {
    // try {
    // return decryptFileName(filename, key, iv, true, encoding);
    // } catch (InvalidKeyException | NoSuchAlgorithmException
    // | NoSuchPaddingException | InvalidAlgorithmParameterException
    // | IllegalBlockSizeException | BadPaddingException
    // | DataFormatException | IOException e) {
    // logger.error("Could not deobfuscate zipped string: " + filename, e);
    // }
    // return null;
    // }

    @Deprecated
    private static synchronized byte[] zipFileName(String fileName) throws IOException {
        Deflater deflater = new Deflater();

        byte[] bytes = fileName.getBytes();
        deflater.setInput(bytes);

        deflater.finish();

        ByteArrayOutputStream bos = new ByteArrayOutputStream(bytes.length);
        byte[] buffer = new byte[1024];

        while (!deflater.finished()) {
            int bytesCompressed = deflater.deflate(buffer);
            bos.write(buffer, 0, bytesCompressed);
        }
        bos.close();

        int count = 0;
        for (int i = 0; i < buffer.length; i++) {
            if (buffer[i] != 0)
                count++;
        }

        byte[] result = new byte[count];
        for (int i = 0; i < result.length; i++) {
            result[i] = buffer[i];
        }

        return result;
    }

    @Deprecated
    private static synchronized String unzipFileName(byte[] zippedFileName)
            throws DataFormatException, IOException {
        Inflater inflater = new Inflater();
        inflater.setInput(zippedFileName);

        byte[] buffer = new byte[1024];
        inflater.inflate(buffer);
        inflater.end();

        int count = 0;
        for (int i = 0; i < buffer.length; i++) {
            if (buffer[i] != 0)
                count++;
        }

        byte[] result = new byte[count];
        for (int i = 0; i < result.length; i++) {
            result[i] = buffer[i];
        }
        return new String(result, PanboxConstants.STANDARD_CHARSET);

    }

}