2 * Copyright (C) 2014 The CyanogenMod Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
18 package com.cyanogenmod.filemanager.console.secure;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.os.AsyncTask;
23 import android.os.Handler;
24 import android.os.Message;
25 import android.os.UserHandle;
26 import android.os.Handler.Callback;
27 import android.util.Log;
28 import android.widget.Toast;
30 import com.cyanogenmod.filemanager.FileManagerApplication;
31 import com.cyanogenmod.filemanager.R;
32 import com.cyanogenmod.filemanager.commands.Executable;
33 import com.cyanogenmod.filemanager.commands.ExecutableFactory;
34 import com.cyanogenmod.filemanager.commands.MountExecutable;
35 import com.cyanogenmod.filemanager.commands.secure.Program;
36 import com.cyanogenmod.filemanager.commands.secure.SecureExecutableFactory;
37 import com.cyanogenmod.filemanager.console.AuthenticationFailedException;
38 import com.cyanogenmod.filemanager.console.CancelledOperationException;
39 import com.cyanogenmod.filemanager.console.CommandNotFoundException;
40 import com.cyanogenmod.filemanager.console.Console;
41 import com.cyanogenmod.filemanager.console.ConsoleAllocException;
42 import com.cyanogenmod.filemanager.console.ExecutionException;
43 import com.cyanogenmod.filemanager.console.InsufficientPermissionsException;
44 import com.cyanogenmod.filemanager.console.NoSuchFileOrDirectory;
45 import com.cyanogenmod.filemanager.console.OperationTimeoutException;
46 import com.cyanogenmod.filemanager.console.ReadOnlyFilesystemException;
47 import com.cyanogenmod.filemanager.console.VirtualMountPointConsole;
48 import com.cyanogenmod.filemanager.model.DiskUsage;
49 import com.cyanogenmod.filemanager.model.MountPoint;
50 import com.cyanogenmod.filemanager.preferences.FileManagerSettings;
51 import com.cyanogenmod.filemanager.preferences.Preferences;
52 import com.cyanogenmod.filemanager.util.DialogHelper;
53 import com.cyanogenmod.filemanager.util.ExceptionUtil;
54 import com.cyanogenmod.filemanager.util.FileHelper;
56 import org.apache.http.auth.AuthenticationException;
58 import de.schlichtherle.truezip.crypto.raes.RaesAuthenticationException;
59 import de.schlichtherle.truezip.file.TArchiveDetector;
60 import de.schlichtherle.truezip.file.TFile;
61 import de.schlichtherle.truezip.file.TVFS;
62 import de.schlichtherle.truezip.key.CancelledOperation;
63 import static de.schlichtherle.truezip.fs.FsSyncOption.CLEAR_CACHE;
64 import static de.schlichtherle.truezip.fs.FsSyncOption.FORCE_CLOSE_INPUT;
65 import static de.schlichtherle.truezip.fs.FsSyncOption.FORCE_CLOSE_OUTPUT;
66 import de.schlichtherle.truezip.util.BitField;
69 import java.io.FilenameFilter;
70 import java.io.IOException;
72 import java.util.ArrayList;
73 import java.util.List;
74 import java.util.UUID;
75 import java.util.concurrent.ExecutorService;
76 import java.util.concurrent.Executors;
79 * A secure implementation of a {@link VirtualMountPointConsole} that uses a
80 * secure filesystem backend
82 public class SecureConsole extends VirtualMountPointConsole {
84 public static final String TAG = "SecureConsole";
86 /** The singleton TArchiveDetector which enclosure this driver **/
87 public static final TArchiveDetector DETECTOR = new TArchiveDetector(
88 SecureStorageDriverProvider.SINGLETON, SecureStorageDriverProvider.SINGLETON.get());
90 public static String getSecureStorageName() {
91 return String.format("storage.%s.%s",
92 String.valueOf(UserHandle.myUserId()),
93 SecureStorageDriverProvider.SECURE_STORAGE_SCHEME);
96 public static TFile getSecureStorageRoot() {
97 return new TFile(FileManagerApplication.getInstance().getExternalFilesDir(null),
98 getSecureStorageName(), DETECTOR);
101 public static URI getSecureStorageRootUri() {
102 return new File(FileManagerApplication.getInstance().getExternalFilesDir(null),
103 getSecureStorageName()).toURI();
106 private static SecureConsole sConsole = null;
108 public final Handler mSyncHandler;
110 private boolean mIsMounted;
111 private boolean mRequiresSync;
113 private final int mBufferSize;
115 private static final long SYNC_WAIT = 10000L;
117 private static final int MSG_SYNC_FS = 0;
119 private final ExecutorService mExecutorService = Executors.newFixedThreadPool(1);
121 private final Callback mSyncCallback = new Callback() {
123 public boolean handleMessage(Message msg) {
126 mExecutorService.execute(new Runnable() {
142 * Return an instance of the current console
145 public static synchronized SecureConsole getInstance(Context ctx, int bufferSize) {
146 if (sConsole == null) {
147 sConsole = new SecureConsole(ctx, bufferSize);
152 private final TFile mStorageRoot;
153 private final String mStorageName;
156 * Constructor of <code>SecureConsole</code>
158 * @param ctx The current context
160 private SecureConsole(Context ctx, int bufferSize) {
163 mBufferSize = bufferSize;
164 mSyncHandler = new Handler(mSyncCallback);
165 mStorageRoot = getSecureStorageRoot();
166 mStorageName = getSecureStorageName();
168 // Save a copy of the console. This has a unique instance for all the app
169 if (sConsole != null) {
175 public void dealloc() {
178 // Synchronize the underlaying storage
179 mSyncHandler.removeMessages(MSG_SYNC_FS);
187 public String getName() {
195 public boolean isSecure() {
203 public boolean isMounted() {
211 public List<MountPoint> getMountPoints() {
212 // This console only has one mountpoint
213 List<MountPoint> mountPoints = new ArrayList<MountPoint>();
214 String status = mIsMounted ? MountExecutable.READWRITE : MountExecutable.READONLY;
215 mountPoints.add(new MountPoint(getVirtualMountPoint().getAbsolutePath(),
216 "securestorage", "securestoragefs", status, 0, 0, true, false));
224 @SuppressWarnings("deprecation")
225 public List<DiskUsage> getDiskUsage() {
226 // This console only has one mountpoint, and is fully usage
227 List<DiskUsage> diskUsage = new ArrayList<DiskUsage>();
228 File mp = mStorageRoot.getFile();
229 diskUsage.add(new DiskUsage(mp.getAbsolutePath(),
232 mp.getTotalSpace() - mp.length()));
237 * Method that returns if the path belongs to the secure storage
239 * @param path The path to check
242 public boolean isSecureStorageResource(String path) {
243 return FileHelper.belongsToDirectory(new File(path), getVirtualMountPoint());
250 public DiskUsage getDiskUsage(String path) {
251 if (isSecureStorageResource(path)) {
252 return getDiskUsage().get(0);
261 public String getMountPointName() {
270 public boolean isRemote() {
278 public ExecutableFactory getExecutableFactory() {
279 return new SecureExecutableFactory(this);
283 * Method that request a reset of the current password
285 public void requestReset(final Context ctx) {
286 AsyncTask<Void, Void, Boolean> task = new AsyncTask<Void, Void, Boolean>() {
288 protected Boolean doInBackground(Void... params) {
289 boolean result = false;
291 // Unmount the filesystem
296 SecureStorageKeyManagerProvider.SINGLETON.reset();
298 // Mount with the new key
301 // In order to claim a write, we need to be sure that an operation is
302 // done to disk before unmount the device.
304 String testName = UUID.randomUUID().toString();
305 TFile test = new TFile(getSecureStorageRoot(), testName);
306 test.createNewFile();
309 } catch (IOException ex) {
310 ExceptionUtil.translateException(ctx, ex);
313 } catch (Exception ex) {
314 ExceptionUtil.translateException(ctx, ex);
323 protected void onPostExecute(Boolean result) {
326 DialogHelper.showToast(ctx, R.string.msgs_success, Toast.LENGTH_SHORT);
335 * Method that request a delete of the current password
337 @SuppressWarnings("deprecation")
338 public void requestDelete(final Context ctx) {
339 AsyncTask<Void, Void, Boolean> task = new AsyncTask<Void, Void, Boolean>() {
341 protected Boolean doInBackground(Void... params) {
342 boolean result = false;
344 // Unmount the filesystem
349 SecureStorageKeyManagerProvider.SINGLETON.delete();
351 // Test mount/unmount
355 // Password is valid. Delete the storage
356 mStorageRoot.getFile().delete();
358 // Send an broadcast to notify that the mount state of this filesystem changed
359 Intent intent = new Intent(FileManagerSettings.INTENT_MOUNT_STATUS_CHANGED);
360 intent.putExtra(FileManagerSettings.EXTRA_MOUNTPOINT,
361 getVirtualMountPoint().toString());
362 intent.putExtra(FileManagerSettings.EXTRA_STATUS, MountExecutable.READONLY);
363 getCtx().sendBroadcast(intent);
367 } catch (Exception ex) {
368 ExceptionUtil.translateException(ctx, ex);
375 protected void onPostExecute(Boolean result) {
378 DialogHelper.showToast(ctx, R.string.msgs_success, Toast.LENGTH_SHORT);
389 public boolean unmount() {
390 // Unmount the filesystem and cancel the cached key
391 mRequiresSync = true;
392 boolean ret = sync();
394 SecureStorageKeyManagerProvider.SINGLETON.unmount();
398 // Send an broadcast to notify that the mount state of this filesystem changed
399 Intent intent = new Intent(FileManagerSettings.INTENT_MOUNT_STATUS_CHANGED);
400 intent.putExtra(FileManagerSettings.EXTRA_MOUNTPOINT,
401 getVirtualMountPoint().toString());
402 intent.putExtra(FileManagerSettings.EXTRA_STATUS, MountExecutable.READONLY);
403 getCtx().sendBroadcast(intent);
409 * Method that verifies if the current storage is open and mount it
411 * @param ctx The current context
412 * @throws CancelledOperationException If the operation was cancelled (by the user)
413 * @throws AuthenticationException If the secure storage isn't unlocked
414 * @throws NoSuchFileOrDirectory If the secure storage isn't accessible
416 @SuppressWarnings("deprecation")
417 public synchronized void mount(Context ctx)
418 throws CancelledOperationException, AuthenticationFailedException,
419 NoSuchFileOrDirectory {
421 File root = mStorageRoot.getFile();
423 boolean newStorage = !root.exists();
424 mStorageRoot.mount();
426 // Force a synchronization
427 mRequiresSync = true;
430 // Remove any previous cache files (if not sync invoked)
434 // The device is mounted
437 // Send an broadcast to notify that the mount state of this filesystem changed
438 Intent intent = new Intent(FileManagerSettings.INTENT_MOUNT_STATUS_CHANGED);
439 intent.putExtra(FileManagerSettings.EXTRA_MOUNTPOINT,
440 getVirtualMountPoint().toString());
441 intent.putExtra(FileManagerSettings.EXTRA_STATUS, MountExecutable.READWRITE);
442 getCtx().sendBroadcast(intent);
444 } catch (IOException ex) {
445 if (ex.getCause() != null && ex.getCause() instanceof CancelledOperation) {
446 throw new CancelledOperationException();
448 if (ex.getCause() != null && ex.getCause() instanceof RaesAuthenticationException) {
449 throw new AuthenticationFailedException(ctx.getString(
450 R.string.secure_storage_unlock_failed));
452 Log.e(TAG, String.format("Failed to open secure storage: %s", root, ex));
453 throw new NoSuchFileOrDirectory();
459 * Method that returns if the path is the real secure storage file
461 * @param path The path to check
462 * @return boolean If the path is the secure storage
464 public static boolean isSecureStorageDir(String path) {
465 Console vc = getVirtualConsoleForPath(path);
466 if (vc != null && vc instanceof SecureConsole) {
467 return isSecureStorageDir(((SecureConsole) vc).buildRealFile(path));
473 * Method that returns if the path is the real secure storage file
475 * @param path The path to check
476 * @return boolean If the path is the secure storage
478 public static boolean isSecureStorageDir(TFile path) {
479 return getSecureStorageRoot().equals(path);
483 * Method that build a real file from a virtual path
485 * @param path The path from build the real file
486 * @return TFile The real file
488 public TFile buildRealFile(String path) {
489 String real = mStorageRoot.toString();
490 String virtual = getVirtualMountPoint().toString();
491 String src = path.replace(virtual, real);
492 return new TFile(src, DETECTOR);
496 * Method that build a virtual file from a real path
498 * @param path The path from build the virtual file
499 * @return TFile The virtual file
501 public String buildVirtualPath(TFile path) {
502 String real = mStorageRoot.toString();
503 String virtual = getVirtualMountPoint().toString();
504 String dst = path.toString().replace(real, virtual);
512 public synchronized void execute(Executable executable, Context ctx)
513 throws ConsoleAllocException, InsufficientPermissionsException, NoSuchFileOrDirectory,
514 OperationTimeoutException, ExecutionException, CommandNotFoundException,
515 ReadOnlyFilesystemException, CancelledOperationException,
516 AuthenticationFailedException {
517 // Check that the program is a secure program
519 Program p = (Program) executable;
520 p.setBufferSize(mBufferSize);
521 } catch (Throwable e) {
522 Log.e(TAG, String.format("Failed to resolve program: %s", //$NON-NLS-1$
523 executable.getClass().toString()), e);
524 throw new CommandNotFoundException("executable is not a program", e); //$NON-NLS-1$
527 //Auditing program execution
529 Log.v(TAG, String.format("Executing program: %s", //$NON-NLS-1$
530 executable.getClass().toString()));
534 final Program program = (Program) executable;
536 // Open storage encryption (if required)
537 if (program.requiresOpen()) {
541 // Execute the program
542 program.setTrace(isTrace());
543 if (program.isAsynchronous()) {
544 // Execute in a thread
545 Thread t = new Thread() {
550 requestSync(program);
551 } catch (Exception e) {
552 // Program must use onException to communicate exceptions
554 String.format("Async execute failed program: %s", //$NON-NLS-1$
555 program.getClass().toString()));
562 // Synchronous execution
564 requestSync(program);
569 * Request a synchronization of the underlying filesystem
571 * @param program The last called program
573 private void requestSync(Program program) {
574 if (program.requiresSync()) {
575 mRequiresSync = true;
578 // There is some changes to synchronize?
580 Boolean defaultValue = ((Boolean)FileManagerSettings.
581 SETTINGS_SECURE_STORAGE_DELAYED_SYNC.getDefaultValue());
582 Boolean delayedSync =
584 Preferences.getSharedPreferences().getBoolean(
585 FileManagerSettings.SETTINGS_SECURE_STORAGE_DELAYED_SYNC.getId(),
586 defaultValue.booleanValue()));
587 mSyncHandler.removeMessages(MSG_SYNC_FS);
589 // Request a sync in 30 seconds, if users is not doing any operation
590 mSyncHandler.sendEmptyMessageDelayed(MSG_SYNC_FS, SYNC_WAIT);
592 // Do the synchronization now
593 mSyncHandler.sendEmptyMessage(MSG_SYNC_FS);
599 * Synchronize the underlying filesystem
601 * @retun boolean If the unmount success
603 public synchronized boolean sync() {
605 Log.i(TAG, "Syncing underlaying storage");
606 mRequiresSync = false;
607 // Sync the underlying storage
609 TVFS.sync(mStorageRoot,
610 BitField.of(CLEAR_CACHE)
611 .set(FORCE_CLOSE_INPUT, true)
612 .set(FORCE_CLOSE_OUTPUT, true));
614 } catch (IOException e) {
615 Log.e(TAG, String.format("Failed to sync secure storage: %s", mStorageRoot, e));
623 * Method that clear the cache
625 * @param ctx The current context
627 private void clearCache(Context ctx) {
628 File filesDir = ctx.getExternalFilesDir(null);
629 File[] cacheFiles = filesDir.listFiles(new FilenameFilter() {
631 public boolean accept(File dir, String filename) {
632 return filename.startsWith(mStorageName)
633 && filename.endsWith(".tmp");
636 for (File cacheFile : cacheFiles) {