/** * * 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 static edu.mit.media.funf.util.AsyncSharedPrefs.async; import java.io.File; import java.security.GeneralSecurityException; import java.util.HashMap; import java.util.Map; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.DESKeySpec; import javax.crypto.spec.PBEKeySpec; import android.content.Context; import android.content.SharedPreferences; 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; /** * 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. */ public class DefaultArchive implements FileArchive { private static final String ENCRYPTION_PREFS = "edu.mit.media.funf.configured.ConfiguredEncryption"; private static final String ENCRYPTION_KEY = "ENCRYPTION_KEY"; private static final String DES_ENCRYPTION = "DES"; private static final char[] DEFAULT_PASSWORD = "changeme".toCharArray(); 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 protected final String databaseName; protected final Context context; protected final SharedPreferences preferences; private DefaultArchive(Context context, String databaseName) { this.context = context.getApplicationContext(); this.databaseName = databaseName; this.preferences = async(this.context.getSharedPreferences(ENCRYPTION_PREFS, Context.MODE_PRIVATE)); } private static final Map<String,DefaultArchive> instances = new HashMap<String, DefaultArchive>(); /** * Get an instance of the default archive for the specified database name * @param context * @param databaseName * @return */ public synchronized static DefaultArchive getArchive(Context context, String databaseName) { DefaultArchive archive = instances.get(databaseName); if (archive == null) { archive = new DefaultArchive(context, databaseName); instances.put(databaseName, archive); } return archive; } /** * 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 PBEKeySpec keySpec = new PBEKeySpec(encryptionPassword, SALT, ITERATION_COUNT); try { SecretKeyFactory factory = SecretKeyFactory.getInstance("PBEWithMD5AndDES"); SecretKey secretKey = factory.generateSecret(keySpec); saveKey(secretKey); } catch (GeneralSecurityException e) { throw new RuntimeException("Unable to encrypt data files.", e); } } public void setEncryptionKey(byte[] encryptionKey) { 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); } } protected SecretKey getKey() { String encodedDesKey = preferences.getString(ENCRYPTION_KEY, null); if (encodedDesKey == null) { // Use default password if key set setEncryptionPassword(DEFAULT_PASSWORD); encodedDesKey = preferences.getString(ENCRYPTION_KEY, null); } assert encodedDesKey != null; try { DESKeySpec des = new DESKeySpec(Base64Coder.decode(encodedDesKey.toCharArray())); return SecretKeyFactory.getInstance(DES_ENCRYPTION).generateSecret(des); } catch (GeneralSecurityException e) { throw new RuntimeException("Unable to build key for encryption", e); } } private void saveKey(SecretKey secretKey) { SharedPreferences.Editor edit = preferences.edit(); edit.putString(ENCRYPTION_KEY, new String(Base64Coder.encode(secretKey.getEncoded()))); edit.commit(); // Reset delegate archive, to reinitialize key delegateArchive = null; getDelegateArchive(); } ///////////////////// // Delegate public String getPathOnSDCard() { return FileUtil.getSdCardPath(context) + databaseName + "/"; } private FileArchive delegateArchive; // Cache protected FileArchive getDelegateArchive() { if (delegateArchive == null) { synchronized (this) { if (delegateArchive == null) { SecretKey key = getKey(); 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_" + databaseName + "_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); } }