/*
*
* 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.desktop.common.vfs;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import javax.crypto.SecretKey;
import org.apache.commons.io.FilenameUtils;
import org.apache.log4j.Logger;
import org.panbox.core.crypto.Obfuscator;
import org.panbox.core.exception.ObfuscationException;
import org.panbox.core.keymgmt.ShareKey;
import org.panbox.core.vfs.backend.VirtualFile;
import org.panbox.desktop.common.ex.PanboxCreateFailedException;
import org.panbox.desktop.common.ex.PanboxDeleteFailedException;
import org.panbox.desktop.common.ex.PanboxEncryptionException;
import org.panbox.desktop.common.ex.PanboxHandleException;
import org.panbox.desktop.common.ex.PanboxRenameFailedException;
import org.panbox.desktop.common.vfs.backend.VirtualRandomAccessFile;
import org.panbox.desktop.common.vfs.backend.exceptions.SecretKeyNotFoundException;
/**
* @author palige
*
* Linux-specific implementation of Panbox VFS interface.
*/
public class PanboxFSLinux extends PanboxFS {
private static final Logger logger = Logger
.getLogger("org.panbox.desktop.common");
private final static HashMap<Long, VirtualFileAccessSession> fileInstanceTable = new HashMap<Long, VirtualFileAccessSession>();
public PanboxFSLinux(PanboxFSAdapter backend) {
super(backend);
}
private class VirtualFileAccessSession {
final VirtualRandomAccessFile file;
final boolean readonly;
/**
* @param file
* @param readonly
*/
public VirtualFileAccessSession(VirtualRandomAccessFile file,
boolean readonly) {
this.file = file;
this.readonly = readonly;
}
}
public synchronized void create(final String path, final long handle,
final boolean readonly) throws ObfuscationException, IOException,
PanboxEncryptionException {
logger.debug("create : " + path + ", handle: " + handle);
ShareKey shareKey = backingStorage.getLatestShareKeyForFile(path);
VirtualRandomAccessFile virt = (VirtualRandomAccessFile) getVirtualFileForFileName(
path, true);
virt.create(shareKey.version, shareKey.key);
fileInstanceTable.put(handle, new VirtualFileAccessSession(virt,
readonly));
logger.debug("create : VirtualFile(" + path
+ ").create() was successful.");
}
public synchronized void flush(final String path, final long handle)
throws PanboxHandleException, IOException {
logger.debug("flush : " + path + ", handle: " + handle);
VirtualFileAccessSession session = fileInstanceTable.get(handle);
if (session != null) {
// note: flush() will also be called upon readonly files
session.file.flush();
logger.debug("flush : VirtualFile(" + path
+ ").flush() was successful.");
} else {
throw new PanboxHandleException(
"No available instance for given handle nr. " + handle);
}
}
public synchronized AbstractFileInfo getattr(final String path,
boolean alreadyObfuscated, boolean outputObfuscated)
throws IOException {
logger.debug("getattr : " + path);
return getFileInfo(path, alreadyObfuscated, outputObfuscated);
}
public synchronized void mkdir(final String path)
throws PanboxCreateFailedException, SecretKeyNotFoundException,
FileNotFoundException, ObfuscationException {
logger.debug("mkdir : " + path);
if (!getVirtualFileForFileName(path, true).createNewDirectory()) {
throw new PanboxCreateFailedException(
"Failed to create directory '" + path + "'.");
} else {
logger.debug("mkdir : VirtualFile(" + path
+ ").createNewDirectory() was successful.");
}
}
public synchronized void open(final String path, final long handle,
boolean readonly) throws ObfuscationException,
PanboxEncryptionException, IOException {
logger.debug("open : " + path + ", handle: " + handle + ", readonly: "
+ readonly);
VirtualRandomAccessFile virt = (VirtualRandomAccessFile) getVirtualFileForFileName(path);
virt.open();
virt.initWithShareKey(backingStorage.getShareKeyForFile(path,
virt.getShareKeyVersion()));
fileInstanceTable.put(handle, new VirtualFileAccessSession(virt,
readonly));
}
public synchronized int read(final String path, final ByteBuffer buffer,
final long offset, final long size, final long handle)
throws PanboxHandleException, PanboxEncryptionException,
IOException {
VirtualFileAccessSession session = fileInstanceTable.get(handle);
if (session != null) {
logger.debug("read, File : " + path + ", Obf. path: "
+ session.file + ", buffersize: " + buffer.remaining()
+ ", offset: " + offset);
return session.file.read(offset, buffer);
} else {
throw new PanboxHandleException(
"No available instance for given handle nr. " + handle);
}
}
public synchronized int read(final String path, final byte[] buffer,
final long offset, final long size, final long handle)
throws PanboxHandleException, PanboxEncryptionException,
IOException {
VirtualFileAccessSession session = fileInstanceTable.get(handle);
if (session != null) {
logger.debug("read, File : " + path + ", Obf. path: "
+ session.file + ", buffersize: " + buffer.length
+ ", offset: " + offset);
return session.file.read(offset, buffer);
} else {
throw new PanboxHandleException(
"No available instance for given handle nr. " + handle);
}
}
public synchronized Collection<String> readdir(final String path)
throws FileNotFoundException {
logger.debug("readdir : " + path);
VirtualFile[] files = null;
try {
if (path.equals(File.separator)) {
files = backingStorage.getFile(path).list();
} else {
files = getVirtualFileForFileName(path).list();
}
} catch (IOException | ObfuscationException e) {
files = new VirtualFile[0];
}
final ArrayList<String> list = new ArrayList<String>();
for (final VirtualFile s : files) {
// This code is for making files and folders staring with '.'
// invisible!
// This is mainly used for the .panbox Folder
String nameOfFile = s.getFileName().substring(
s.getFileName().lastIndexOf(File.separator) + 1);
logger.trace("listDirectory : nameOfFile: " + nameOfFile);
if (nameOfFile.startsWith(".")) {
// ignore file, it started with .
logger.trace("listDirectory : Ignoring file starting with '.': "
+ nameOfFile + " (" + path + ")");
continue;
}
list.add(backingStorage.getRelativePathForFile(s));
}
return list;
}
public synchronized void release(final String path, final long handle)
throws PanboxHandleException, IOException {
logger.debug("release : " + path + ", handle: " + handle);
VirtualFileAccessSession session = fileInstanceTable.get(handle);
if (session != null) {
session.file.close();
fileInstanceTable.remove(handle);
} else {
throw new PanboxHandleException(
"No available instance for given handle nr. " + handle);
}
}
public synchronized void setLastAccessTime(final String path,
final long ac_nsec) throws IOException {
logger.debug("setLastAccessTime : " + path);
try {
VirtualFile backed = getVirtualFileForFileName(path);
backed.setLastAccessTime(ac_nsec);
} catch (ObfuscationException e) {
throw new IOException("Obfuscation failed!");
}
}
public synchronized void setModifiedTime(String path, long mod_nsec)
throws IOException, ObfuscationException {
logger.debug("setModifiedTime : " + path);
VirtualFile backed = getVirtualFileForFileName(path);
backed.setModifiedTime(mod_nsec);
}
public synchronized void rename(final String oldpath, final String newpath)
throws PanboxRenameFailedException, ObfuscationException,
IOException {
logger.debug("rename : " + oldpath + " -> " + newpath);
VirtualFile newFile = getVirtualFileForFileName(newpath, true);
VirtualFile oldFile = getVirtualFileForFileName(oldpath);
// VirtualFileAccessSession sessionold, sessionnew;
// sessionold = getSessionforVirtualFile(oldFile);
// sessionnew = getSessionforVirtualFile(newFile);
//
// if ((sessionnew != null) || (sessionold) != null) {
// throw new IOException(
// "rename of files in active session not supported!");
// }
// oldFile = (sessionold != null) ? sessionold.file : oldFile;
// newFile = (sessionnew != null) ? sessionnew.file : newFile;
if (newFile.exists() && !newFile.canWrite()) {
throw new IOException("Renaming of readonly files is not allowed!");
} else {
if (!oldFile.renameTo(newFile)) {
throw new PanboxRenameFailedException("Operation " + oldpath
+ ").renameTo(" + newpath + ") was not successful");
} else {
logger.debug("rename : VirtualFile(" + oldpath + ").renameTo("
+ newpath + ") was successful.");
}
}
}
/**
* Convenience method which returns {@link VirtualFile}-instance for given
* filename without creating IV sidear file during obfuscation.
*
*
* @param filename
* @return
* @throws SecretKeyNotFoundException
* @throws FileNotFoundException
* @throws ObfuscationException
*/
protected synchronized VirtualFile getVirtualFileForFileName(String filename)
throws SecretKeyNotFoundException, FileNotFoundException,
ObfuscationException {
return getVirtualFileForFileName(filename, false);
}
@Override
protected synchronized VirtualFile getVirtualFileForFileName(
String fileName, boolean createIV)
throws SecretKeyNotFoundException, FileNotFoundException,
ObfuscationException {
// check if file currently is already referenced within the context of a
// file access session and, if so, use this instance instead.
// Reasoning: If operations like rename or truncate are conducted upon
// a file whose contents are currently also being accessed, having
// multiple instances may cause inconsistencies
VirtualFile file = super.getVirtualFileForFileName(fileName, createIV);
VirtualFileAccessSession session = getSessionforVirtualFile(file);
return (session != null) ? session.file : file;
}
public synchronized void rmdir(final String path) throws IOException,
ObfuscationException {
logger.debug("rmdir : " + path);
if (!getVirtualFileForFileName(path).delete()) {
throw new IOException("Deletion failed!");
} else {
logger.debug("deleteDirectory : VirtualFile(" + path
+ ").deleteDirectory() was successful.");
}
}
private synchronized VirtualFileAccessSession getSessionforVirtualFile(
VirtualFile file) {
if (file instanceof VirtualRandomAccessFile) {
VirtualFileAccessSession session = null;
Collection<VirtualFileAccessSession> col = fileInstanceTable
.values();
for (Iterator<VirtualFileAccessSession> iterator = col.iterator(); iterator
.hasNext();) {
VirtualFileAccessSession virtualFileAccessSession = (VirtualFileAccessSession) iterator
.next();
if (virtualFileAccessSession.file.equals(file)) {
session = virtualFileAccessSession;
break;
}
}
return session;
} else
return null;
}
public synchronized void truncate(final String path, final long length)
throws IOException, SecretKeyNotFoundException,
FileNotFoundException, ObfuscationException,
PanboxEncryptionException {
logger.debug("truncate : " + path + ", length: " + length);
VirtualFile vFile = getVirtualFileForFileName(path);
VirtualFileAccessSession session = getSessionforVirtualFile(vFile);
if (session == null) {
VirtualRandomAccessFile virt = (VirtualRandomAccessFile) getVirtualFileForFileName(path);
virt.open();
virt.initWithShareKey(backingStorage.getShareKeyForFile(path,
virt.getShareKeyVersion()));
virt.setLength(length);
virt.flush();
virt.close();
} else {
// already opened
if (session.readonly) {
throw new IOException("Illegal operation: File " + session.file
+ " was opened readonly!");
} else {
session.file.setLength(length);
}
}
}
public enum AccessMode {
EXISTS, READ, WRITE, EXECUTE
};
public synchronized boolean access(final String path, final AccessMode mode)
throws SecretKeyNotFoundException, FileNotFoundException,
ObfuscationException {
logger.debug("access : " + path);
VirtualFile virt = getVirtualFileForFileName(path);
if (virt instanceof VirtualRandomAccessFile) {
switch (mode) {
case READ:
return virt.canRead();
case WRITE:
return virt.canWrite();
case EXECUTE:
return virt.canExecute();
case EXISTS:
return virt.exists();
default:
return true;
}
} else {
return true;
}
}
public synchronized void unlink(final String path)
throws PanboxDeleteFailedException, SecretKeyNotFoundException,
FileNotFoundException, ObfuscationException {
logger.debug("unlink : " + path);
if (!getVirtualFileForFileName(path).delete()) {
throw new PanboxDeleteFailedException("Deletion failed!");
} else {
logger.debug("deleteFile : VirtualFile(" + path
+ ").deleteFile() was successful.");
}
}
public synchronized int write(final String path, final ByteBuffer buffer,
final long offset, final long size, final long handle)
throws PanboxHandleException, IOException,
PanboxEncryptionException {
VirtualFileAccessSession session = fileInstanceTable.get(handle);
if (session != null) {
if (session.readonly) {
throw new IOException("Illegal operation: File " + session.file
+ " was opened readonly!");
} else {
logger.debug("write, File : " + path + ", Obf. path: "
+ session.file + ", buffersize: " + buffer.remaining()
+ ", offset: " + offset);
return session.file.write(offset, buffer);
}
} else {
throw new PanboxHandleException(
"No available instance for given handle nr. " + handle);
}
}
public synchronized int write(final String path, final byte[] buffer,
final long offset, final long size, final long handle)
throws PanboxHandleException, IOException,
PanboxEncryptionException {
VirtualFileAccessSession session = fileInstanceTable.get(handle);
if (session != null) {
if (session.readonly) {
throw new IOException("Illegal operation: File " + session.file
+ " was opened readonly!");
} else {
logger.debug("write, File : " + path + ", Obf. path: "
+ session.file + ", buffersize: " + buffer.length
+ ", offset: " + offset);
return session.file.write(offset, buffer);
}
} else {
throw new PanboxHandleException(
"No available instance for given handle nr. " + handle);
}
}
public synchronized void chmod(final String path, final long attr)
throws IOException, ObfuscationException {
logger.debug("chmod: path=" + path + ", attr=" + attr);
VirtualFile virt = getVirtualFileForFileName(path);
// NOTE: Calling chmod() on files which are currently in the process of
// being encrypted & written should not affect the corresponding file
// access session, but only come into effect after the fh has been close
// in the backend
virt.setAttr(attr);
}
public synchronized void symlink(final String target, final String link)
throws IOException {
try {
// TODO: Here, we parse 3 times for the Share that manages the File
VirtualFile vlink = getVirtualFileForFileName(link, true);
String fullpath = FilenameUtils.getFullPath(link);
String sharePath = FilenameUtils.normalize(fullpath.substring(0,
fullpath.indexOf('/', 1) + 1));
String shareloc = FilenameUtils.concat(fullpath, target);
if (FilenameUtils.equals(sharePath, shareloc)
|| FilenameUtils.directoryContains(sharePath, shareloc)) {
SecretKey sk = backingStorage
.getObfuscationKeyForFile(shareloc);
Obfuscator obfuscator = backingStorage.getObfuscator(shareloc);
// create obfuscated symlink target
String[] targetparts = target.split("/");
StringBuffer res = new StringBuffer();
if (target.startsWith(File.separator)) {
res.append(File.separator);
}
for (int i = 0; i < targetparts.length; i++) {
String cur = targetparts[i];
if (cur.equals(".") || cur.equals("..")) {
res.append(cur);
} else {
// append obfuscated part of path
res.append(obfuscator.obfuscate(cur, sk, true));
}
// append intermediary separators
if (i < targetparts.length - 1) {
res.append(File.separator);
}
}
if (target.endsWith(File.separator)) {
res.append(File.separator);
}
String obfuscatedTarget = res.toString();
Path ptarget = Paths.get(obfuscatedTarget);
Path plink = Paths.get(vlink.getFileName());
Files.createSymbolicLink(plink, ptarget);
logger.debug("symlink, Target : " + obfuscatedTarget
+ ", Link: " + vlink.getFileName());
} else {
throw new IOException(
"Symlinks outside of shares are not supported.");
}
} catch (ObfuscationException e) {
// logger.error("Could not obfuscate symlink target!", e);
throw new IOException("Could not obfuscate symlink target!", e);
}
}
public synchronized void readlink(final String path,
final ByteBuffer buffer, final long size) throws IOException {
try {
// TODO: Here, we parse 3 times for the Share that manages the File
String fullpath = FilenameUtils.getFullPath(path);
String sharePath = FilenameUtils.normalize(fullpath.substring(0,
fullpath.indexOf('/', 1) + 1));
VirtualFile vpath = getVirtualFileForFileName(path);
String target = Files.readSymbolicLink(vpath.getFile().toPath())
.toString();
String shareloc = FilenameUtils.concat(fullpath, target.toString());
if (FilenameUtils.directoryContains(sharePath, shareloc)
|| FilenameUtils.equals(sharePath, shareloc)) {
SecretKey sk = backingStorage
.getObfuscationKeyForFile(shareloc);
Obfuscator obfuscator = backingStorage.getObfuscator(shareloc);
// create deobfuscated symlink target
String[] targetparts = target.split("/");
StringBuffer res = new StringBuffer();
if (target.startsWith(File.separator)) {
res.append(File.separator);
}
for (int i = 0; i < targetparts.length; i++) {
String cur = targetparts[i];
if (cur.equals(".") || cur.equals("..")) {
res.append(cur);
} else {
// append obfuscated part of path
res.append(obfuscator.deObfuscate(cur, sk));
}
// append intermediary separators
if (i < targetparts.length - 1) {
res.append(File.separator);
}
}
if (target.endsWith(File.separator)) {
res.append(File.separator);
}
byte[] ret = res.toString().getBytes();
int realsize = Math.min(ret.length, (int) size);
buffer.put(ret, 0, realsize);
logger.debug("readline, Link : " + path + ", Target: "
+ res.toString());
} else {
throw new IOException(
"Symlinks outside of shares are not supported.");
}
} catch (ObfuscationException e) {
throw new IOException("Error deobfuscating symlink contents!", e);
}
}
/**
* indicates if there currently exist any ongoing file access sessions
*
* @return <code>true</code>, if there exist one or more file access
* sessions
*/
public boolean openFileAccessSessions() {
return (fileInstanceTable.size() > 0);
}
public void beforeUnmount(File mountPoint) {
logger.warn("Panbox is about to shutdown - flush and close all open AES* instances...");
Collection<VirtualFileAccessSession> coll = fileInstanceTable.values();
for (Iterator<VirtualFileAccessSession> it = coll.iterator(); it
.hasNext();) {
VirtualFileAccessSession session = (VirtualFileAccessSession) it
.next();
try {
session.file.flush();
session.file.close();
logger.info("Successfully closed VirtualFile instance for file "
+ session.file.getFileName());
} catch (Exception e) {
logger.error("Error on closing VirtualFile instance for file "
+ session.file.getFileName());
}
}
}
}