/* * Copyright (C) 2010-2016 Martin Goellnitz * * 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 2 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, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA, 02110-1301, USA */ package jfs.sync.encryption; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.OutputStream; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Map; import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; import jfs.conf.JFSConfig; import jfs.conf.JFSLog; import jfs.conf.JFSText; import jfs.sync.JFSFile; import jfs.sync.JFSProgress; import jfs.sync.util.SecurityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * File descriptor for encrypted files to hide encrypted names, size differences and the like. */ public class JFSEncryptedFile extends JFSFile { private static final Logger LOG = LoggerFactory.getLogger(JFSEncryptedFile.class); /** * duplication to avoid constant casts */ private final AbstractFileProducer fileProducer; private FileInfo fileInfo; /** * If the file is a directory this points to all files (and directories) within the directory. Null for all non * directories. */ private JFSFile[] list = null; /** The last input stream opened for this file. */ private InputStream in = null; /** The last output stream opened for this file. */ private OutputStream out = null; /** * Creates a new local JFS file object. * * @param fileProducer The assigned file producer. * @param cipherSpec JCE cipher specification. * @param relativePath The relative path of the JFS file starting from the root JFS file. */ JFSEncryptedFile(AbstractFileProducer fileProducer, String relativePath) { super(fileProducer, relativePath); // super has a somewhat buggy normalization of filename only dealing with local file separator definitions this.relativePath = this.relativePath.replace('\\', '/'); this.relativePath = this.relativePath.replace("/", fileProducer.getSeparator()); this.fileProducer = fileProducer; fileInfo = fileProducer.getFileInfo(getRelativePath()); LOG.debug("('{}') name={}", getRelativePath(), fileInfo.getPath()); }// JFSEncryptedFile() private Cipher getCipher(int cipherMode) { try { String cipherSpec = ((AbstractFileProducer) getFileProducer()).storageAccess.getCipherSpec(); byte[] credentials = ((AbstractFileProducer) getFileProducer()).storageAccess.getFileCredentials(getRelativePath()); return SecurityUtils.getCipher(cipherSpec, cipherMode, credentials); } catch (NoSuchAlgorithmException nsae) { LOG.error("getCipher() No Such Algorhithm"); } catch (NoSuchPaddingException nspe) { LOG.error("getCipher() No Such Padding"); } catch (InvalidKeyException ike) { LOG.error("getCipher() Invalid Key {}", ike.getLocalizedMessage()); } // try/catch return null; } // getCipher() /** * @see JFSFile#getName() */ @Override public final String getName() { LOG.debug("getName('{}') {}", getRelativePath(), fileInfo.getName()); return fileInfo.getName(); } /** * @see JFSFile#getPath() */ @Override public final String getPath() { return fileProducer.getRootPath()+getRelativePath(); } /** * @see JFSFile#isDirectory() */ @Override public final boolean isDirectory() { return fileInfo.isDirectory(); } /** * @see JFSFile#canRead() */ @Override public final boolean canRead() { return fileInfo.isCanRead(); } /** * @see JFSFile#canWrite() */ @Override public final boolean canWrite() { LOG.debug("canWrite() {}", fileInfo.isCanWrite()); return fileInfo.isCanWrite(); // return true; } /** * @see JFSFile#getLength() */ @Override public final long getLength() { if (fileInfo.getSize()<0) { try { // TODO: move this to storage layer? InputStream fis = fileProducer.getInputStream(getRelativePath()); ObjectInputStream ois = new ObjectInputStream(fis); JFSEncryptedStream.readMarker(ois); fileInfo.setSize(JFSEncryptedStream.readLength(ois)); LOG.debug("getLength({}) detected plain text length {}", getRelativePath(), fileInfo.getSize()); ois.close(); } catch (Exception e) { // TODO: what to do now?!?!?! LOG.error("getLength() could not detect plain text length for "+getPath(), e); } // try/catch } // if return fileInfo.getSize(); } // getLength() /** * @see JFSFile#getLastModified() */ @Override public final long getLastModified() { LOG.debug("lastModified('{}') {}", getRelativePath(), fileInfo.getModificationDate()); // strange enough, directories need to be 0 in all cases return isDirectory() ? 0 : fileInfo.getModificationDate(); } /** * @see JFSFile#getList() */ @Override public final JFSFile[] getList() { if (list==null) { String[] files = fileProducer.list(getRelativePath()); if (files!=null) { list = new JFSFile[files.length]; for (int i = 0; i<files.length; i++) { // asFolder parameter doesn't to anything LOG.debug("getList({}) {} {}", getName(), i, files[i]); list[i] = fileProducer.getJfsFile(getRelativePath()+fileProducer.getSeparator()+files[i], false); } // for } else { list = new JFSFile[0]; } } LOG.debug("getList() {}", list.length); return list; } /** * @see JFSFile#exists() */ @Override public final boolean exists() { LOG.debug("exists('{}') {}", getRelativePath(), fileInfo.isExists()); return fileInfo.isExists(); } /** * @see JFSFile#mkdir() */ @Override public final boolean mkdir() { boolean success = fileProducer.createDirectory(getRelativePath()); if (success) { fileInfo.setDirectory(true); } // if return success; } // mkdir() /** * @see JFSFile#setLastModified(long) */ @Override public final boolean setLastModified(long time) { boolean success = fileProducer.setLastModified(getRelativePath(), time); LOG.debug("setLastModified('{}') setting modification date: {}", getRelativePath(), success); if (success) { fileInfo.setModificationDate(time); } // if return success; } /** * @see JFSFile#setReadOnly() */ @Override public final boolean setReadOnly() { boolean success = true; if (JFSConfig.getInstance().isSetCanWrite()) { success = fileProducer.setReadOnly(getRelativePath()); if (success) { fileInfo.setCanWrite(false); } // if } // if return success; } // setReadOnly() /** * @see JFSFile#delete() */ @Override public final boolean delete() { return fileProducer.delete(getRelativePath()); } /** * @see JFSFile#getInputStream() */ @Override protected InputStream getInputStream() { try { InputStream stream = fileProducer.getInputStream(getRelativePath()); return JFSEncryptedStream.createInputStream(stream, getLength(), getCipher(Cipher.DECRYPT_MODE)); } catch (IOException ioe) { LOG.error("getInputStream() I/O Exception "+ioe.getLocalizedMessage()); return null; } // try/catch } // getInputStream() /** * @see JFSFile#getOutputStream() */ @Override protected OutputStream getOutputStream() { String p = getRelativePath(); out = null; long l = getLength(); LOG.debug("getOutputStream() opening '{}' with length {}", p, l); try { int idx = p.lastIndexOf('.'); long compressionLimit = Long.MAX_VALUE; if (idx>0) { idx++; String extension = p.substring(idx).toLowerCase(); Map<String, Long> compressedExtensions = ((JFSEncryptedFileProducer) fileProducer).getCompressionLevels(); if (compressedExtensions.containsKey(extension)) { compressionLimit = compressedExtensions.get(extension); LOG.info("getOutputStream() compression limit {} set for {}", compressionLimit, getName()); } // if } // if out = JFSEncryptedStream.createOutputStream(compressionLimit, fileProducer.getOutputStream(p), l, getCipher(Cipher.ENCRYPT_MODE)); } catch (IOException e) { LOG.error("getOutputStream()", e); } // try/catch return out; } // getOutputStream() /** * @see JFSFile#closeInputStream() */ @Override protected void closeInputStream() { JFSText t = JFSText.getInstance(); try { if (in!=null) { in.close(); in = null; } // if } catch (IOException e) { JFSLog.getErr().getStream().println(t.get("error.io")+" "+e); } // try/catch } // closeInputStream() /** * @see JFSFile#closeOutputStream() */ @Override protected void closeOutputStream() { JFSText t = JFSText.getInstance(); try { if (out!=null) { LOG.debug("closeOutputStream() closing {}", getPath()); out.flush(); out.close(); out = null; } // if } catch (IOException e) { JFSLog.getErr().getStream().println(t.get("error.io")+" "+e); } // try/catch } // closeOutputStream() /** * @see JFSFile#preCopyTgt(JFSFile) */ @Override protected boolean preCopyTgt(JFSFile srcFile) { fileInfo.setSize(srcFile.getLength()); return true; } /** * @see JFSFile#preCopySrc(JFSFile) */ @Override protected boolean preCopySrc(JFSFile tgtFile) { return true; } /** * @see JFSFile#postCopyTgt(JFSFile) */ @Override protected boolean postCopyTgt(JFSFile srcFile) { boolean success = true; // Set last modified and read-only only when file is no directory: if (!JFSProgress.getInstance().isCanceled()&&!srcFile.isDirectory()) { // Just to work on the same file info fileInfo = fileProducer.getFileInfo(relativePath); fileInfo.setExists(true); fileInfo.setSize(srcFile.getLength()); success = success&&setLastModified(srcFile.getLastModified()); // set last modified has to implicitly if (!success) { fileProducer.flush(fileInfo); } // if if (!srcFile.canWrite()) { success = success&&setReadOnly(); } // if } // if return success; } /** * @see JFSFile#postCopySrc(JFSFile) */ @Override protected boolean postCopySrc(JFSFile tgtFile) { LOG.info("postCopySrc() free memory {}", Runtime.getRuntime().freeMemory()); return true; } // postCopySrc() /** * @see JFSFile#flush() */ @Override public boolean flush() { return true; } @Override protected void finalize() throws Throwable { LOG.info("finalize() free memory {}", Runtime.getRuntime().freeMemory()); super.finalize(); } // finalize() } // JFSEncryptedFile()