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 de.schlichtherle.truezip.crypto.raes.RaesAuthenticationException;
57 import de.schlichtherle.truezip.file.TArchiveDetector;
58 import de.schlichtherle.truezip.file.TFile;
59 import de.schlichtherle.truezip.file.TVFS;
60 import de.schlichtherle.truezip.key.CancelledOperation;
61 import static de.schlichtherle.truezip.fs.FsSyncOption.CLEAR_CACHE;
62 import static de.schlichtherle.truezip.fs.FsSyncOption.FORCE_CLOSE_INPUT;
63 import static de.schlichtherle.truezip.fs.FsSyncOption.FORCE_CLOSE_OUTPUT;
64 import de.schlichtherle.truezip.util.BitField;
67 import java.io.FilenameFilter;
68 import java.io.IOException;
70 import java.util.ArrayList;
71 import java.util.List;
72 import java.util.UUID;
73 import java.util.concurrent.ExecutorService;
74 import java.util.concurrent.Executors;
77 * A secure implementation of a {@link VirtualMountPointConsole} that uses a
78 * secure filesystem backend
80 public class SecureConsole extends VirtualMountPointConsole {
82 public static final String TAG = "SecureConsole";
84 /** The singleton TArchiveDetector which enclosure this driver **/
85 public static final TArchiveDetector DETECTOR = new TArchiveDetector(
86 SecureStorageDriverProvider.SINGLETON, SecureStorageDriverProvider.SINGLETON.get());
88 public static String getSecureStorageName() {
89 return String.format("storage.%s.%s",
90 String.valueOf(UserHandle.myUserId()),
91 SecureStorageDriverProvider.SECURE_STORAGE_SCHEME);
94 public static TFile getSecureStorageRoot() {
95 return new TFile(FileManagerApplication.getInstance().getExternalFilesDir(null),
96 getSecureStorageName(), DETECTOR);
99 public static URI getSecureStorageRootUri() {
100 return new File(FileManagerApplication.getInstance().getExternalFilesDir(null),
101 getSecureStorageName()).toURI();
104 private static SecureConsole sConsole = null;
106 public final Handler mSyncHandler;
108 private boolean mIsMounted;
109 private boolean mRequiresSync;
111 private final int mBufferSize;
113 private static final long SYNC_WAIT = 10000L;
115 private static final int MSG_SYNC_FS = 0;
117 private final ExecutorService mExecutorService = Executors.newFixedThreadPool(1);
119 private final Callback mSyncCallback = new Callback() {
121 public boolean handleMessage(Message msg) {
124 mExecutorService.execute(new Runnable() {
140 * Return an instance of the current console
143 public static synchronized SecureConsole getInstance(Context ctx, int bufferSize) {
144 if (sConsole == null) {
145 sConsole = new SecureConsole(ctx, bufferSize);
150 private final TFile mStorageRoot;
151 private final String mStorageName;
154 * Constructor of <code>SecureConsole</code>
156 * @param ctx The current context
158 private SecureConsole(Context ctx, int bufferSize) {
161 mBufferSize = bufferSize;
162 mSyncHandler = new Handler(mSyncCallback);
163 mStorageRoot = getSecureStorageRoot();
164 mStorageName = getSecureStorageName();
166 // Save a copy of the console. This has a unique instance for all the app
167 if (sConsole != null) {
173 public void dealloc() {
176 // Synchronize the underlaying storage
177 mSyncHandler.removeMessages(MSG_SYNC_FS);
185 public String getName() {
193 public boolean isSecure() {
201 public boolean isMounted() {
209 public List<MountPoint> getMountPoints() {
210 // This console only has one mountpoint
211 List<MountPoint> mountPoints = new ArrayList<MountPoint>();
212 String status = mIsMounted ? MountExecutable.READWRITE : MountExecutable.READONLY;
213 mountPoints.add(new MountPoint(getVirtualMountPoint().getAbsolutePath(),
214 "securestorage", "securestoragefs", status, 0, 0, true, false));
222 @SuppressWarnings("deprecation")
223 public List<DiskUsage> getDiskUsage() {
224 // This console only has one mountpoint, and is fully usage
225 List<DiskUsage> diskUsage = new ArrayList<DiskUsage>();
226 File mp = mStorageRoot.getFile();
227 diskUsage.add(new DiskUsage(mp.getAbsolutePath(),
230 mp.getTotalSpace() - mp.length()));
235 * Method that returns if the path belongs to the secure storage
237 * @param path The path to check
240 public boolean isSecureStorageResource(String path) {
241 return FileHelper.belongsToDirectory(new File(path), getVirtualMountPoint());
248 public DiskUsage getDiskUsage(String path) {
249 if (isSecureStorageResource(path)) {
250 return getDiskUsage().get(0);
259 public String getMountPointName() {
268 public boolean isRemote() {
276 public ExecutableFactory getExecutableFactory() {
277 return new SecureExecutableFactory(this);
281 * Method that request a reset of the current password
283 public void requestReset(final Context ctx) {
284 AsyncTask<Void, Void, Boolean> task = new AsyncTask<Void, Void, Boolean>() {
286 protected Boolean doInBackground(Void... params) {
287 boolean result = false;
289 // Unmount the filesystem
294 SecureStorageKeyManagerProvider.SINGLETON.reset();
296 // Mount with the new key
299 // In order to claim a write, we need to be sure that an operation is
300 // done to disk before unmount the device.
302 String testName = UUID.randomUUID().toString();
303 TFile test = new TFile(getSecureStorageRoot(), testName);
304 test.createNewFile();
307 } catch (IOException ex) {
308 ExceptionUtil.translateException(ctx, ex);
311 } catch (Exception ex) {
312 ExceptionUtil.translateException(ctx, ex);
321 protected void onPostExecute(Boolean result) {
324 DialogHelper.showToast(ctx, R.string.msgs_success, Toast.LENGTH_SHORT);
333 * Method that request a delete of the current password
335 @SuppressWarnings("deprecation")
336 public void requestDelete(final Context ctx) {
337 AsyncTask<Void, Void, Boolean> task = new AsyncTask<Void, Void, Boolean>() {
339 protected Boolean doInBackground(Void... params) {
340 boolean result = false;
342 // Unmount the filesystem
347 SecureStorageKeyManagerProvider.SINGLETON.delete();
349 // Test mount/unmount
353 // Password is valid. Delete the storage
354 mStorageRoot.getFile().delete();
356 // Send an broadcast to notify that the mount state of this filesystem changed
357 Intent intent = new Intent(FileManagerSettings.INTENT_MOUNT_STATUS_CHANGED);
358 intent.putExtra(FileManagerSettings.EXTRA_MOUNTPOINT,
359 getVirtualMountPoint().toString());
360 intent.putExtra(FileManagerSettings.EXTRA_STATUS, MountExecutable.READONLY);
361 getCtx().sendBroadcast(intent);
365 } catch (Exception ex) {
366 ExceptionUtil.translateException(ctx, ex);
373 protected void onPostExecute(Boolean result) {
376 DialogHelper.showToast(ctx, R.string.msgs_success, Toast.LENGTH_SHORT);
387 public boolean unmount() {
388 // Unmount the filesystem and cancel the cached key
389 mRequiresSync = true;
390 boolean ret = sync();
392 SecureStorageKeyManagerProvider.SINGLETON.unmount();
396 // Send an broadcast to notify that the mount state of this filesystem changed
397 Intent intent = new Intent(FileManagerSettings.INTENT_MOUNT_STATUS_CHANGED);
398 intent.putExtra(FileManagerSettings.EXTRA_MOUNTPOINT,
399 getVirtualMountPoint().toString());
400 intent.putExtra(FileManagerSettings.EXTRA_STATUS, MountExecutable.READONLY);
401 getCtx().sendBroadcast(intent);
407 * Method that verifies if the current storage is open and mount it
409 * @param ctx The current context
410 * @throws CancelledOperationException If the operation was cancelled (by the user)
411 * @throws AuthenticationException If the secure storage isn't unlocked
412 * @throws NoSuchFileOrDirectory If the secure storage isn't accessible
414 @SuppressWarnings("deprecation")
415 public synchronized void mount(Context ctx)
416 throws CancelledOperationException, AuthenticationFailedException,
417 NoSuchFileOrDirectory {
419 File root = mStorageRoot.getFile();
421 boolean newStorage = !root.exists();
422 mStorageRoot.mount();
424 // Force a synchronization
425 mRequiresSync = true;
428 // Remove any previous cache files (if not sync invoked)
432 // The device is mounted
435 // Send an broadcast to notify that the mount state of this filesystem changed
436 Intent intent = new Intent(FileManagerSettings.INTENT_MOUNT_STATUS_CHANGED);
437 intent.putExtra(FileManagerSettings.EXTRA_MOUNTPOINT,
438 getVirtualMountPoint().toString());
439 intent.putExtra(FileManagerSettings.EXTRA_STATUS, MountExecutable.READWRITE);
440 getCtx().sendBroadcast(intent);
442 } catch (IOException ex) {
443 if (ex.getCause() != null && ex.getCause() instanceof CancelledOperation) {
444 throw new CancelledOperationException();
446 if (ex.getCause() != null && ex.getCause() instanceof RaesAuthenticationException) {
447 throw new AuthenticationFailedException(ctx.getString(
448 R.string.secure_storage_unlock_failed));
450 Log.e(TAG, String.format("Failed to open secure storage: %s", root, ex));
451 throw new NoSuchFileOrDirectory();
457 * Method that returns if the path is the real secure storage file
459 * @param path The path to check
460 * @return boolean If the path is the secure storage
462 public static boolean isSecureStorageDir(String path) {
463 Console vc = getVirtualConsoleForPath(path);
464 if (vc != null && vc instanceof SecureConsole) {
465 return isSecureStorageDir(((SecureConsole) vc).buildRealFile(path));
471 * Method that returns if the path is the real secure storage file
473 * @param path The path to check
474 * @return boolean If the path is the secure storage
476 public static boolean isSecureStorageDir(TFile path) {
477 return getSecureStorageRoot().equals(path);
481 * Method that build a real file from a virtual path
483 * @param path The path from build the real file
484 * @return TFile The real file
486 public TFile buildRealFile(String path) {
487 String real = mStorageRoot.toString();
488 String virtual = getVirtualMountPoint().toString();
489 String src = path.replace(virtual, real);
490 return new TFile(src, DETECTOR);
494 * Method that build a virtual file from a real path
496 * @param path The path from build the virtual file
497 * @return TFile The virtual file
499 public String buildVirtualPath(TFile path) {
500 String real = mStorageRoot.toString();
501 String virtual = getVirtualMountPoint().toString();
502 String dst = path.toString().replace(real, virtual);
510 public synchronized void execute(Executable executable, Context ctx)
511 throws ConsoleAllocException, InsufficientPermissionsException, NoSuchFileOrDirectory,
512 OperationTimeoutException, ExecutionException, CommandNotFoundException,
513 ReadOnlyFilesystemException, CancelledOperationException,
514 AuthenticationFailedException {
515 // Check that the program is a secure program
517 Program p = (Program) executable;
518 p.setBufferSize(mBufferSize);
519 } catch (Throwable e) {
520 Log.e(TAG, String.format("Failed to resolve program: %s", //$NON-NLS-1$
521 executable.getClass().toString()), e);
522 throw new CommandNotFoundException("executable is not a program", e); //$NON-NLS-1$
525 //Auditing program execution
527 Log.v(TAG, String.format("Executing program: %s", //$NON-NLS-1$
528 executable.getClass().toString()));
532 final Program program = (Program) executable;
534 // Open storage encryption (if required)
535 if (program.requiresOpen()) {
539 // Execute the program
540 program.setTrace(isTrace());
541 if (program.isAsynchronous()) {
542 // Execute in a thread
543 Thread t = new Thread() {
548 requestSync(program);
549 } catch (Exception e) {
550 // Program must use onException to communicate exceptions
552 String.format("Async execute failed program: %s", //$NON-NLS-1$
553 program.getClass().toString()));
560 // Synchronous execution
562 requestSync(program);
567 * Request a synchronization of the underlying filesystem
569 * @param program The last called program
571 private void requestSync(Program program) {
572 if (program.requiresSync()) {
573 mRequiresSync = true;
576 // There is some changes to synchronize?
578 Boolean defaultValue = ((Boolean)FileManagerSettings.
579 SETTINGS_SECURE_STORAGE_DELAYED_SYNC.getDefaultValue());
580 Boolean delayedSync =
582 Preferences.getSharedPreferences().getBoolean(
583 FileManagerSettings.SETTINGS_SECURE_STORAGE_DELAYED_SYNC.getId(),
584 defaultValue.booleanValue()));
585 mSyncHandler.removeMessages(MSG_SYNC_FS);
587 // Request a sync in 30 seconds, if users is not doing any operation
588 mSyncHandler.sendEmptyMessageDelayed(MSG_SYNC_FS, SYNC_WAIT);
590 // Do the synchronization now
591 mSyncHandler.sendEmptyMessage(MSG_SYNC_FS);
597 * Synchronize the underlying filesystem
599 * @retun boolean If the unmount success
601 public synchronized boolean sync() {
603 Log.i(TAG, "Syncing underlaying storage");
604 mRequiresSync = false;
605 // Sync the underlying storage
607 TVFS.sync(mStorageRoot,
608 BitField.of(CLEAR_CACHE)
609 .set(FORCE_CLOSE_INPUT, true)
610 .set(FORCE_CLOSE_OUTPUT, true));
612 } catch (IOException e) {
613 Log.e(TAG, String.format("Failed to sync secure storage: %s", mStorageRoot, e));
621 * Method that clear the cache
623 * @param ctx The current context
625 private void clearCache(Context ctx) {
626 File filesDir = ctx.getExternalFilesDir(null);
627 File[] cacheFiles = filesDir.listFiles(new FilenameFilter() {
629 public boolean accept(File dir, String filename) {
630 return filename.startsWith(mStorageName)
631 && filename.endsWith(".tmp");
634 for (File cacheFile : cacheFiles) {