package freenet.pluginmanager;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import freenet.config.SubConfig;
import freenet.crypt.AEADCryptBucket;
import freenet.node.FSParseException;
import freenet.node.Node;
import freenet.node.NodeInitException;
import freenet.node.ProgramDirectory;
import freenet.support.IllegalBase64Exception;
import freenet.support.SimpleFieldSet;
import freenet.support.api.Bucket;
import freenet.support.io.FileBucket;
import freenet.support.io.FileUtil;
import freenet.support.io.PaddedBucket;
public class PluginStores {
final Node node;
private final ProgramDirectory pluginStoresDir;
public PluginStores(Node node, SubConfig installConfig) throws NodeInitException {
this.node = node;
pluginStoresDir = node.setupProgramDir(installConfig, "pluginStoresDir", "plugin-data",
"NodeClientCore.pluginStoresDir", "NodeClientCore.pluginStoresDir", null, null);
File dir = pluginStoresDir.dir();
if(!(dir.mkdirs() || (dir.exists() && dir.isDirectory() && dir.canRead() && dir.canWrite()))) {
System.err.println("Unable to create folder for plugin data: "+pluginStoresDir.dir());
}
}
private void writePluginStoreInner(String storeIdentifier, PluginStore pluginStore, boolean isEncrypted, boolean backup) throws IOException {
Bucket bucket = makePluginStoreBucket(storeIdentifier, isEncrypted, backup);
OutputStream os = bucket.getOutputStream();
try {
if(pluginStore != null) {
pluginStore.exportStoreAsSFS().writeTo(os);
}
} finally {
os.close();
}
}
private File getPluginStoreFile(String storeIdentifier, boolean encrypted, boolean backup) {
String filename = storeIdentifier;
filename += ".data";
if(backup)
filename += ".bak";
if(encrypted)
filename += ".crypt";
return pluginStoresDir.file(filename);
}
private Bucket makePluginStoreBucket(String storeIdentifier, boolean isEncrypted, boolean backup)
throws FileNotFoundException {
File f = getPluginStoreFile(storeIdentifier, isEncrypted, backup);
Bucket bucket = new FileBucket(f, false, true, false, false);
if(isEncrypted) {
byte[] key = node.getPluginStoreKey(storeIdentifier);
if(key != null) {
// We pad then encrypt, which is wasteful, but we have no way to persist the size.
// Unfortunately AEADCryptBucket needs to know the real termination point.
bucket = new AEADCryptBucket(bucket, key);
bucket = new PaddedBucket(bucket);
}
}
return bucket;
}
private Bucket findPluginStoreBucket(String storeIdentifier, boolean isEncrypted, boolean backup)
throws FileNotFoundException {
File f = getPluginStoreFile(storeIdentifier, isEncrypted, backup);
if(!f.exists()) return null;
Bucket bucket = new FileBucket(f, false, false, false, false);
if(isEncrypted) {
byte[] key = node.getPluginStoreKey(storeIdentifier);
if(key != null) {
// We pad then encrypt, which is wasteful, but we have no way to persist the size.
// Unfortunately AEADCryptBucket needs to know the real termination point.
bucket = new AEADCryptBucket(bucket, key);
bucket = new PaddedBucket(bucket, bucket.size());
}
}
return bucket;
}
public PluginStore loadPluginStore(String storeIdentifier) {
boolean isEncrypted = node.wantEncryptedDatabase();
PluginStore store = loadPluginStore(storeIdentifier, isEncrypted, false);
if(store != null) return store;
store = loadPluginStore(storeIdentifier, isEncrypted, true);
if(store != null) return store;
isEncrypted = !isEncrypted;
store = loadPluginStore(storeIdentifier, isEncrypted, false);
if(store != null) return store;
store = loadPluginStore(storeIdentifier, isEncrypted, true);
return store;
}
private PluginStore loadPluginStore(String storeIdentifier, boolean isEncrypted, boolean backup) {
Bucket bucket;
try {
bucket = findPluginStoreBucket(storeIdentifier, isEncrypted, backup);
if(bucket == null) return null;
} catch (FileNotFoundException e) {
return null;
}
InputStream is = null;
try {
try {
is = bucket.getInputStream();
SimpleFieldSet fs = SimpleFieldSet.readFrom(is, false, false, true, true);
return new PluginStore(fs);
} finally {
// Do NOT use Closer.close().
// We use authenticated encryption, which will throw at close() time if the file is corrupt,
// or has been modified while the node was offline etc.
if(is != null) is.close();
}
} catch (IOException e) {
// Hence, if close() throws, we DO need to catch it here.
System.err.println("Unable to load plugin data for "+storeIdentifier+" : "+e);
System.err.println("This could be caused by data corruption or bugs in Freenet.");
// FIXME crypto - possible it's caused by attack while offline.
return null;
} catch (IllegalBase64Exception e) {
// Hence, if close() throws, we DO need to catch it here.
System.err.println("Unable to load plugin data for "+storeIdentifier+" : "+e);
System.err.println("This could be caused by data corruption or bugs in Freenet.");
// FIXME crypto - possible it's caused by attack while offline.
return null;
} catch (FSParseException e) {
// Hence, if close() throws, we DO need to catch it here.
System.err.println("Unable to load plugin data for "+storeIdentifier+" : "+e);
System.err.println("This could be caused by data corruption or bugs in Freenet.");
// FIXME crypto - possible it's caused by attack while offline.
return null;
}
}
public void writePluginStore(String storeIdentifier, PluginStore store) throws IOException {
boolean isEncrypted = node.wantEncryptedDatabase();
File backup = getPluginStoreFile(storeIdentifier, isEncrypted, true);
File main = getPluginStoreFile(storeIdentifier, isEncrypted, false);
if(backup.exists() && main.exists()) {
FileUtil.secureDelete(backup);
}
if(main.exists()) {
if(!main.renameTo(backup))
System.err.println("Unable to rename "+main+" to "+backup+" when writing pluginstore for "+storeIdentifier);
}
writePluginStoreInner(storeIdentifier, store, isEncrypted, false);
File f = getPluginStoreFile(storeIdentifier, !isEncrypted, true);
if(f.exists()) {
FileUtil.secureDelete(f);
}
f = getPluginStoreFile(storeIdentifier, !isEncrypted, false);
if(f.exists()) {
FileUtil.secureDelete(f);
}
}
}