OSDN Git Service

cmfm: secure storage and other improvements
[android-x86/packages-apps-CMFileManager.git] / src / com / cyanogenmod / filemanager / console / secure / SecureConsole.java
1 /*
2  * Copyright (C) 2014 The CyanogenMod Project
3  *
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
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17
18 package com.cyanogenmod.filemanager.console.secure;
19
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;
29
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;
55
56 import org.apache.http.auth.AuthenticationException;
57
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;
67
68 import java.io.File;
69 import java.io.FilenameFilter;
70 import java.io.IOException;
71 import java.net.URI;
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;
77
78 /**
79  * A secure implementation of a {@link VirtualMountPointConsole} that uses a
80  * secure filesystem backend
81  */
82 public class SecureConsole extends VirtualMountPointConsole {
83
84     public static final String TAG = "SecureConsole";
85
86     /** The singleton TArchiveDetector which enclosure this driver **/
87     public static final TArchiveDetector DETECTOR = new TArchiveDetector(
88             SecureStorageDriverProvider.SINGLETON, SecureStorageDriverProvider.SINGLETON.get());
89
90     public static String getSecureStorageName() {
91         return String.format("storage.%s.%s",
92                 String.valueOf(UserHandle.myUserId()),
93                 SecureStorageDriverProvider.SECURE_STORAGE_SCHEME);
94     }
95
96     public static TFile getSecureStorageRoot() {
97         return new TFile(FileManagerApplication.getInstance().getExternalFilesDir(null),
98                 getSecureStorageName(), DETECTOR);
99     }
100
101     public static URI getSecureStorageRootUri() {
102         return new File(FileManagerApplication.getInstance().getExternalFilesDir(null),
103                 getSecureStorageName()).toURI();
104     }
105
106     private static SecureConsole sConsole = null;
107
108     public final Handler mSyncHandler;
109
110     private boolean mIsMounted;
111     private boolean mRequiresSync;
112
113     private final int mBufferSize;
114
115     private static final long SYNC_WAIT = 10000L;
116
117     private static final int MSG_SYNC_FS = 0;
118
119     private final ExecutorService mExecutorService = Executors.newFixedThreadPool(1);
120
121     private final Callback mSyncCallback = new Callback() {
122         @Override
123         public boolean handleMessage(Message msg) {
124             switch (msg.what) {
125                 case MSG_SYNC_FS:
126                     mExecutorService.execute(new Runnable() {
127                         @Override
128                         public void run() {
129                             sync();
130                         }
131                     });
132                     break;
133
134                 default:
135                     break;
136             }
137             return true;
138         }
139     };
140
141     /**
142      * Return an instance of the current console
143      * @return
144      */
145     public static synchronized SecureConsole getInstance(Context ctx, int bufferSize) {
146         if (sConsole == null) {
147             sConsole = new SecureConsole(ctx, bufferSize);
148         }
149         return sConsole;
150     }
151
152     private final TFile mStorageRoot;
153     private final String mStorageName;
154
155     /**
156      * Constructor of <code>SecureConsole</code>
157      *
158      * @param ctx The current context
159      */
160     private SecureConsole(Context ctx, int bufferSize) {
161         super(ctx);
162         mIsMounted = false;
163         mBufferSize = bufferSize;
164         mSyncHandler = new Handler(mSyncCallback);
165         mStorageRoot = getSecureStorageRoot();
166         mStorageName = getSecureStorageName();
167
168         // Save a copy of the console. This has a unique instance for all the app
169         if (sConsole != null) {
170             sConsole = this;
171         }
172     }
173
174     @Override
175     public void dealloc() {
176         super.dealloc();
177
178         // Synchronize the underlaying storage
179         mSyncHandler.removeMessages(MSG_SYNC_FS);
180         sync();
181     }
182
183     /**
184      * {@inheritDoc}
185      */
186     @Override
187     public String getName() {
188         return "Secure";
189     }
190
191     /**
192      * {@inheritDoc}
193      */
194     @Override
195     public boolean isSecure() {
196         return true;
197     }
198
199     /**
200      * {@inheritDoc}
201      */
202     @Override
203     public boolean isMounted() {
204         return mIsMounted;
205     }
206
207     /**
208      * {@inheritDoc}
209      */
210     @Override
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));
217         return mountPoints;
218     }
219
220     /**
221      * {@inheritDoc}
222      */
223     @Override
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(),
230                 mp.getTotalSpace(),
231                 mp.length(),
232                 mp.getTotalSpace() - mp.length()));
233         return diskUsage;
234     }
235
236     /**
237      * Method that returns if the path belongs to the secure storage
238      *
239      * @param path The path to check
240      * @return
241      */
242     public boolean isSecureStorageResource(String path) {
243         return FileHelper.belongsToDirectory(new File(path), getVirtualMountPoint());
244     }
245
246     /**
247      * {@inheritDoc}
248      */
249     @Override
250     public DiskUsage getDiskUsage(String path) {
251         if (isSecureStorageResource(path)) {
252             return getDiskUsage().get(0);
253         }
254         return null;
255     }
256
257     /**
258      * {@inheritDoc}
259      */
260     @Override
261     public String getMountPointName() {
262         return "secure";
263     }
264
265
266     /**
267      * {@inheritDoc}
268      */
269     @Override
270     public boolean isRemote() {
271         return false;
272     }
273
274     /**
275      * {@inheritDoc}
276      */
277     @Override
278     public ExecutableFactory getExecutableFactory() {
279         return new SecureExecutableFactory(this);
280     }
281
282     /**
283      * Method that request a reset of the current password
284      */
285     public void requestReset(final Context ctx) {
286         AsyncTask<Void, Void, Boolean> task = new AsyncTask<Void, Void, Boolean>() {
287             @Override
288             protected Boolean doInBackground(Void... params) {
289                 boolean result = false;
290
291                 // Unmount the filesystem
292                 if (mIsMounted) {
293                     unmount();
294                 }
295                 try {
296                     SecureStorageKeyManagerProvider.SINGLETON.reset();
297
298                     // Mount with the new key
299                     mount(ctx);
300
301                     // In order to claim a write, we need to be sure that an operation is
302                     // done to disk before unmount the device.
303                     try {
304                         String testName = UUID.randomUUID().toString();
305                         TFile test = new TFile(getSecureStorageRoot(), testName);
306                         test.createNewFile();
307                         test.rm();
308                         result = true;
309                     } catch (IOException ex) {
310                         ExceptionUtil.translateException(ctx, ex);
311                     }
312
313                 } catch (Exception ex) {
314                     ExceptionUtil.translateException(ctx, ex);
315                 } finally {
316                     unmount();
317                 }
318
319                 return result;
320             }
321
322             @Override
323             protected void onPostExecute(Boolean result) {
324                 if (result) {
325                     // Success
326                     DialogHelper.showToast(ctx, R.string.msgs_success, Toast.LENGTH_SHORT);
327                 }
328             }
329
330         };
331         task.execute();
332     }
333
334     /**
335      * Method that request a delete of the current password
336      */
337     @SuppressWarnings("deprecation")
338     public void requestDelete(final Context ctx) {
339         AsyncTask<Void, Void, Boolean> task = new AsyncTask<Void, Void, Boolean>() {
340             @Override
341             protected Boolean doInBackground(Void... params) {
342                 boolean result = false;
343
344                 // Unmount the filesystem
345                 if (mIsMounted) {
346                     unmount();
347                 }
348                 try {
349                     SecureStorageKeyManagerProvider.SINGLETON.delete();
350
351                     // Test mount/unmount
352                     mount(ctx);
353                     unmount();
354
355                     // Password is valid. Delete the storage
356                     mStorageRoot.getFile().delete();
357
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);
364
365                     result = true;
366
367                 } catch (Exception ex) {
368                     ExceptionUtil.translateException(ctx, ex);
369                 }
370
371                 return result;
372             }
373
374             @Override
375             protected void onPostExecute(Boolean result) {
376                 if (result) {
377                     // Success
378                     DialogHelper.showToast(ctx, R.string.msgs_success, Toast.LENGTH_SHORT);
379                 }
380             }
381
382         };
383         task.execute();
384     }
385
386     /**
387      * {@inheritDoc}
388      */
389     public boolean unmount() {
390         // Unmount the filesystem and cancel the cached key
391         mRequiresSync = true;
392         boolean ret = sync();
393         if (ret) {
394             SecureStorageKeyManagerProvider.SINGLETON.unmount();
395         }
396         mIsMounted = false;
397
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);
404
405         return mIsMounted;
406     }
407
408     /**
409      * Method that verifies if the current storage is open and mount it
410      *
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
415      */
416     @SuppressWarnings("deprecation")
417     public synchronized void mount(Context ctx)
418             throws CancelledOperationException, AuthenticationFailedException,
419             NoSuchFileOrDirectory {
420         if (!mIsMounted) {
421             File root = mStorageRoot.getFile();
422             try {
423                 boolean newStorage = !root.exists();
424                 mStorageRoot.mount();
425                 if (newStorage) {
426                     // Force a synchronization
427                     mRequiresSync = true;
428                     sync();
429                 } else {
430                     // Remove any previous cache files (if not sync invoked)
431                     clearCache(ctx);
432                 }
433
434                 // The device is mounted
435                 mIsMounted = true;
436
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);
443
444             } catch (IOException ex) {
445                 if (ex.getCause() != null && ex.getCause() instanceof CancelledOperation) {
446                     throw new CancelledOperationException();
447                 }
448                 if (ex.getCause() != null && ex.getCause() instanceof RaesAuthenticationException) {
449                     throw new AuthenticationFailedException(ctx.getString(
450                             R.string.secure_storage_unlock_failed));
451                 }
452                 Log.e(TAG, String.format("Failed to open secure storage: %s", root, ex));
453                 throw new NoSuchFileOrDirectory();
454             }
455         }
456     }
457
458     /**
459      * Method that returns if the path is the real secure storage file
460      *
461      * @param path The path to check
462      * @return boolean If the path is the secure storage
463      */
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));
468         }
469         return false;
470     }
471
472     /**
473      * Method that returns if the path is the real secure storage file
474      *
475      * @param path The path to check
476      * @return boolean If the path is the secure storage
477      */
478     public static boolean isSecureStorageDir(TFile path) {
479         return getSecureStorageRoot().equals(path);
480     }
481
482     /**
483      * Method that build a real file from a virtual path
484      *
485      * @param path The path from build the real file
486      * @return TFile The real file
487      */
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);
493     }
494
495     /**
496      * Method that build a virtual file from a real path
497      *
498      * @param path The path from build the virtual file
499      * @return TFile The virtual file
500      */
501     public String buildVirtualPath(TFile path) {
502         String real = mStorageRoot.toString();
503         String virtual = getVirtualMountPoint().toString();
504         String dst = path.toString().replace(real, virtual);
505         return dst;
506     }
507
508     /**
509      * {@inheritDoc}
510      */
511     @Override
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
518         try {
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$
525         }
526
527         //Auditing program execution
528         if (isTrace()) {
529             Log.v(TAG, String.format("Executing program: %s", //$NON-NLS-1$
530                     executable.getClass().toString()));
531         }
532
533
534         final Program program = (Program) executable;
535
536         // Open storage encryption (if required)
537         if (program.requiresOpen()) {
538             mount(ctx);
539         }
540
541         // Execute the program
542         program.setTrace(isTrace());
543         if (program.isAsynchronous()) {
544             // Execute in a thread
545             Thread t = new Thread() {
546                 @Override
547                 public void run() {
548                     try {
549                         program.execute();
550                         requestSync(program);
551                     } catch (Exception e) {
552                         // Program must use onException to communicate exceptions
553                         Log.v(TAG,
554                                 String.format("Async execute failed program: %s", //$NON-NLS-1$
555                                 program.getClass().toString()));
556                     }
557                 }
558             };
559             t.start();
560
561         } else {
562             // Synchronous execution
563             program.execute();
564             requestSync(program);
565         }
566     }
567
568     /**
569      * Request a synchronization of the underlying filesystem
570      *
571      * @param program The last called program
572      */
573     private void requestSync(Program program) {
574         if (program.requiresSync()) {
575             mRequiresSync = true;
576         }
577
578         // There is some changes to synchronize?
579         if (mRequiresSync) {
580             Boolean defaultValue = ((Boolean)FileManagerSettings.
581                     SETTINGS_SECURE_STORAGE_DELAYED_SYNC.getDefaultValue());
582             Boolean delayedSync =
583                     Boolean.valueOf(
584                             Preferences.getSharedPreferences().getBoolean(
585                                 FileManagerSettings.SETTINGS_SECURE_STORAGE_DELAYED_SYNC.getId(),
586                                 defaultValue.booleanValue()));
587             mSyncHandler.removeMessages(MSG_SYNC_FS);
588             if (delayedSync) {
589                 // Request a sync in 30 seconds, if users is not doing any operation
590                 mSyncHandler.sendEmptyMessageDelayed(MSG_SYNC_FS, SYNC_WAIT);
591             } else {
592                 // Do the synchronization now
593                 mSyncHandler.sendEmptyMessage(MSG_SYNC_FS);
594             }
595         }
596     }
597
598     /**
599      * Synchronize the underlying filesystem
600      *
601      * @retun boolean If the unmount success
602      */
603     public synchronized boolean sync() {
604         if (mRequiresSync) {
605             Log.i(TAG, "Syncing underlaying storage");
606             mRequiresSync = false;
607             // Sync the underlying storage
608             try {
609                 TVFS.sync(mStorageRoot,
610                         BitField.of(CLEAR_CACHE)
611                                 .set(FORCE_CLOSE_INPUT, true)
612                                 .set(FORCE_CLOSE_OUTPUT, true));
613                 return true;
614             } catch (IOException e) {
615                 Log.e(TAG, String.format("Failed to sync secure storage: %s", mStorageRoot, e));
616                 return false;
617             }
618         }
619         return true;
620     }
621
622     /**
623      * Method that clear the cache
624      *
625      * @param ctx The current context
626      */
627     private void clearCache(Context ctx) {
628         File filesDir = ctx.getExternalFilesDir(null);
629         File[] cacheFiles = filesDir.listFiles(new FilenameFilter() {
630             @Override
631             public boolean accept(File dir, String filename) {
632                 return filename.startsWith(mStorageName)
633                         && filename.endsWith(".tmp");
634             }
635         });
636         for (File cacheFile : cacheFiles) {
637             cacheFile.delete();
638         }
639     }
640 }