/**
*
* Funf: Open Sensing Framework
* Copyright (C) 2010-2011 Nadav Aharony, Wei Pan, Alex Pentland.
* Acknowledgments: Alan Gardner
* Contact: nadav@media.mit.edu
*
* This file is part of Funf.
*
* Funf is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* Funf 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with Funf. If not, see <http://www.gnu.org/licenses/>.
*
*/
package edu.mit.media.funf.storage;
import java.io.File;
import java.security.GeneralSecurityException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import javax.crypto.spec.PBEKeySpec;
import android.content.Context;
import edu.mit.media.funf.Schedule.DefaultSchedule;
import edu.mit.media.funf.config.Configurable;
import edu.mit.media.funf.security.Base64Coder;
import edu.mit.media.funf.util.FileUtil;
import edu.mit.media.funf.util.NameGenerator;
import edu.mit.media.funf.util.NameGenerator.CompositeNameGenerator;
import edu.mit.media.funf.util.NameGenerator.RequiredSuffixNameGenerator;
import edu.mit.media.funf.util.NameGenerator.SystemUniqueTimestampNameGenerator;
import edu.mit.media.funf.util.StringUtil;
/**
* A default implementation of a file archive, which should be good enough for most cases.
*
* This archive provides internal memory and SD card redundancy, managed backups, as well as file encryption.
* Archives are singletons by database name.
*/
@DefaultSchedule(interval=3600)
public class DefaultArchive implements FileArchive {
private static final String DES_ENCRYPTION = "DES";
private final static byte[] SALT = {
(byte)0xa6, (byte)0xab, (byte)0x09, (byte)0x93,
(byte)0xf4, (byte)0xcc, (byte)0xee, (byte)0x10
};
private final static int ITERATION_COUNT = 135; // # of times password is hashed
@Configurable
protected String name = "default";
@Configurable
protected String password;
@Configurable
protected String key;
protected Context context;
public DefaultArchive() {
}
public DefaultArchive(Context ctx, String name) {
this.context = ctx;
this.name = name;
}
public void setContext(Context context) {
this.context = context;
}
public void setName(String name) {
this.name = name;
}
/**
* Set the encryption key using a password.
* Does not store the password, but instead uses it to derive a DES key to encrypt files.
* @param encryptionPassword
*/
public void setEncryptionPassword(char[] encryptionPassword) { // Uses char[] instead of String to prevent caching
if (encryptionPassword == null || encryptionPassword.length == 0) {
setEncryptionKey(null);
} else {
PBEKeySpec keySpec = new PBEKeySpec(encryptionPassword, SALT, ITERATION_COUNT);
try {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBEWithMD5AndDES");
SecretKey secretKey = factory.generateSecret(keySpec);
setEncryptionKey(secretKey.getEncoded());
} catch (GeneralSecurityException e) {
throw new RuntimeException("Unable to encrypt data files.", e);
}
}
}
public void setEncryptionKey(byte[] encryptionKey) {
if (encryptionKey == null || encryptionKey.length == 0) {
saveKey(null);
} else {
try {
DESKeySpec des = new DESKeySpec(encryptionKey);
SecretKey key = SecretKeyFactory.getInstance(DES_ENCRYPTION).generateSecret(des);
saveKey(key);
} catch (GeneralSecurityException e) {
throw new RuntimeException("Unable to build key for encryption", e);
}
}
}
private SecretKey keyCache = null;
public SecretKey getSecretKey() {
if (keyCache == null) {
if (key != null) {
setEncryptionKey(Base64Coder.decode(key.toCharArray()));
} else if (password != null) {
setEncryptionPassword(password.toCharArray());
}
}
return keyCache;
}
private void saveKey(SecretKey secretKey) {
keyCache = secretKey;
// Reset delegate archive, to reinitialize key
delegateArchive = null;
getDelegateArchive();
}
/////////////////////
// Delegate
private String getCleanedName() {
return StringUtil.simpleFilesafe(name);
}
public String getPathOnSDCard() {
return FileUtil.getSdCardPath(context) + getCleanedName() + "/";
}
private FileArchive delegateArchive; // Cache
protected FileArchive getDelegateArchive() {
if (delegateArchive == null) {
synchronized (this) {
if (delegateArchive == null) {
SecretKey key = getSecretKey();
String rootSdCardPath = getPathOnSDCard();
FileArchive backupArchive = FileDirectoryArchive.getRollingFileArchive(new File(rootSdCardPath + "backup"));
FileArchive mainArchive = new CompositeFileArchive(
getTimestampedDbFileArchive(new File(rootSdCardPath + "archive"), context, key),
getTimestampedDbFileArchive(context.getDir("funf_" + getCleanedName() + "_archive", Context.MODE_PRIVATE), context, key)
);
delegateArchive = new BackedUpArchive(mainArchive, backupArchive);
}
}
}
return delegateArchive;
}
static FileDirectoryArchive getTimestampedDbFileArchive(File archiveDir, Context context, SecretKey encryptionKey) {
NameGenerator nameGenerator = new CompositeNameGenerator(new SystemUniqueTimestampNameGenerator(context), new RequiredSuffixNameGenerator(".db"));
FileCopier copier = (encryptionKey == null) ? new FileCopier.SimpleFileCopier() : new FileCopier.EncryptedFileCopier(encryptionKey, DES_ENCRYPTION);
return new FileDirectoryArchive(archiveDir, nameGenerator, copier, new DirectoryCleaner.KeepAll());
}
@Override
public boolean add(File item) {
return getDelegateArchive().add(item);
}
@Override
public boolean contains(File item) {
return getDelegateArchive().contains(item);
}
@Override
public File[] getAll() {
return getDelegateArchive().getAll();
}
@Override
public boolean remove(File item) {
return getDelegateArchive().remove(item);
}
}