OSDN Git Service

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