diff options
author | Jorge Ruesga <jorge@ruesga.com> | 2014-10-28 03:26:42 +0100 |
---|---|---|
committer | Jorge Ruesga <jorge@ruesga.com> | 2014-11-10 23:16:25 +0000 |
commit | 877d4660622ebcaa992f05396237169c289470c4 (patch) | |
tree | b254cb04803096aa9695f42e1bcc914ba09e12fd /src/com/cyanogenmod/filemanager/console | |
parent | fcb4908c2c949f55ec966e09a0a91210dff2ca3f (diff) | |
download | android_packages_apps_CMFileManager-877d4660622ebcaa992f05396237169c289470c4.tar.gz android_packages_apps_CMFileManager-877d4660622ebcaa992f05396237169c289470c4.tar.bz2 android_packages_apps_CMFileManager-877d4660622ebcaa992f05396237169c289470c4.zip |
cmfm: secure storage and other improvements
This patch adds support for virtual filesystems and implements a SecureStorage
filesystem (a password protected area) mounted in /storage or /sdcard/storage
(in chrooted environments).
Also includes a better print support and a cleanup of the code and design of
the menu drawer.
Bump version to 2.0.0
Required: https://github.com/jruesga/android_external_libtruezip located
in external/libtruezip
Patchset 4: Fix selection of unmounted virtual storages.
Fix actions on virtual mount points folders.
Fix strings and typos. Change drop for delete secure storage.
Patchset 5: Move actionbar buttons to navigation drawer
Remove history position
Patchset 6: Update theme preview images
Fix filesystem status image on theme change
Patchset 7: Fix binary file detection in editor (including unicode files)
Patchset 8: Fix unsafe operations in virtual mountpoint logic
Patchset 9: Rebase
Change-Id: I65511352ca649dcbf238c8b07cf8c22465296e8e
Signed-off-by: Jorge Ruesga <jorge@ruesga.com>
Diffstat (limited to 'src/com/cyanogenmod/filemanager/console')
13 files changed, 1920 insertions, 122 deletions
diff --git a/src/com/cyanogenmod/filemanager/console/AuthenticationFailedException.java b/src/com/cyanogenmod/filemanager/console/AuthenticationFailedException.java new file mode 100644 index 00000000..795111e9 --- /dev/null +++ b/src/com/cyanogenmod/filemanager/console/AuthenticationFailedException.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2012 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cyanogenmod.filemanager.console; + +import java.io.IOException; + +/** + * An exception that indicates that the operation failed because an authentication failure + */ +public class AuthenticationFailedException extends IOException { + private static final long serialVersionUID = -2199496556437722726L; + + /** + * Constructor of <code>AuthenticationFailedException</code>. + * + * @param msg The associated message + */ + public AuthenticationFailedException(String msg) { + super(msg); + } + +} diff --git a/src/com/cyanogenmod/filemanager/console/CancelledOperationException.java b/src/com/cyanogenmod/filemanager/console/CancelledOperationException.java new file mode 100644 index 00000000..e19d0dc3 --- /dev/null +++ b/src/com/cyanogenmod/filemanager/console/CancelledOperationException.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2012 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cyanogenmod.filemanager.console; + +import java.io.IOException; + +/** + * An exception that indicates that the operation was cancelled + */ +public class CancelledOperationException extends IOException { + private static final long serialVersionUID = 2999554355110192173L; + + /** + * Constructor of <code>CancelledOperationException</code>. + */ + public CancelledOperationException() { + super(); + } + +} diff --git a/src/com/cyanogenmod/filemanager/console/Console.java b/src/com/cyanogenmod/filemanager/console/Console.java index ba28db55..6404431f 100644 --- a/src/com/cyanogenmod/filemanager/console/Console.java +++ b/src/com/cyanogenmod/filemanager/console/Console.java @@ -15,6 +15,8 @@ */ package com.cyanogenmod.filemanager.console; +import android.content.Context; + import com.cyanogenmod.filemanager.commands.AsyncResultExecutable; import com.cyanogenmod.filemanager.commands.Executable; import com.cyanogenmod.filemanager.commands.ExecutableFactory; @@ -46,7 +48,7 @@ public abstract class Console * * @return boolean If the console has to trace */ - public boolean isTrace() { + public final boolean isTrace() { return this.mTrace; } @@ -111,6 +113,7 @@ public abstract class Console * Method for execute a command in the operating system layer. * * @param executable The executable command to be executed + * @param ctx The current context * @throws ConsoleAllocException If the console is not allocated * @throws InsufficientPermissionsException If an operation requires elevated permissions * @throws NoSuchFileOrDirectory If the file or directory was not found @@ -118,10 +121,14 @@ public abstract class Console * @throws CommandNotFoundException If the executable program was not found * @throws ExecutionException If the operation returns a invalid exit code * @throws ReadOnlyFilesystemException If the operation writes in a read-only filesystem + * @throws CancelledOperationException If the operation was cancelled + * @throws AuthenticationFailedException If the operation failed because an + * authentication failure + * @throws AuthenticationFailedException */ - public abstract void execute(final Executable executable) + public abstract void execute(final Executable executable, final Context ctx) throws ConsoleAllocException, InsufficientPermissionsException, NoSuchFileOrDirectory, OperationTimeoutException, ExecutionException, CommandNotFoundException, - ReadOnlyFilesystemException; + ReadOnlyFilesystemException, CancelledOperationException, AuthenticationFailedException; } diff --git a/src/com/cyanogenmod/filemanager/console/VirtualConsole.java b/src/com/cyanogenmod/filemanager/console/VirtualConsole.java new file mode 100644 index 00000000..8512bc77 --- /dev/null +++ b/src/com/cyanogenmod/filemanager/console/VirtualConsole.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 20124 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cyanogenmod.filemanager.console; + +import android.content.Context; +import android.util.Log; + +import com.cyanogenmod.filemanager.commands.SIGNAL; +import com.cyanogenmod.filemanager.console.Console; +import com.cyanogenmod.filemanager.console.ConsoleAllocException; +import com.cyanogenmod.filemanager.model.Identity; +import com.cyanogenmod.filemanager.util.AIDHelper; + +/** + * An abstract base class for all the virtual {@link Console}. + */ +public abstract class VirtualConsole extends Console { + + public static final String TAG = "VirtualConsole"; + + private boolean mActive; + private final Context mCtx; + private final Identity mIdentity; + + /** + * Constructor of <code>VirtualConsole</code> + * + * @param ctx The current context + */ + public VirtualConsole(Context ctx) { + super(); + mCtx = ctx; + mIdentity = AIDHelper.createVirtualIdentity(); + } + + public abstract String getName(); + + /** + * {@inheritDoc} + */ + @Override + public void alloc() throws ConsoleAllocException { + try { + if (isTrace()) { + Log.v(TAG, "Allocating " + getName() + " console"); + } + mActive = true; + } catch (Exception e) { + Log.e(TAG, "Failed to allocate " + getName() + " console", e); + throw new ConsoleAllocException("failed to build console", e); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void dealloc() { + if (isTrace()) { + Log.v(TAG, "Deallocating Java console"); + } + mActive = true; + } + + /** + * {@inheritDoc} + */ + @Override + public void realloc() throws ConsoleAllocException { + dealloc(); + alloc(); + } + + /** + * {@inheritDoc} + */ + @Override + public Identity getIdentity() { + return mIdentity; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isPrivileged() { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isActive() { + return mActive; + } + + /** + * Method that returns the current context + * + * @return Context The current context + */ + public Context getCtx() { + return mCtx; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean onCancel() { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean onSendSignal(SIGNAL signal) { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean onEnd() { + return false; + } +} diff --git a/src/com/cyanogenmod/filemanager/console/VirtualMountPointConsole.java b/src/com/cyanogenmod/filemanager/console/VirtualMountPointConsole.java new file mode 100644 index 00000000..ba730606 --- /dev/null +++ b/src/com/cyanogenmod/filemanager/console/VirtualMountPointConsole.java @@ -0,0 +1,292 @@ +/* + * Copyright (C) 20124 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cyanogenmod.filemanager.console; + +import android.content.Context; +import android.os.Environment; +import android.os.SystemClock; + +import com.cyanogenmod.filemanager.FileManagerApplication; +import com.cyanogenmod.filemanager.R; +import com.cyanogenmod.filemanager.console.secure.SecureConsole; +import com.cyanogenmod.filemanager.model.Directory; +import com.cyanogenmod.filemanager.model.DiskUsage; +import com.cyanogenmod.filemanager.model.Identity; +import com.cyanogenmod.filemanager.model.MountPoint; +import com.cyanogenmod.filemanager.model.Permissions; +import com.cyanogenmod.filemanager.preferences.AccessMode; +import com.cyanogenmod.filemanager.util.AIDHelper; +import com.cyanogenmod.filemanager.util.FileHelper; + +import java.io.File; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * An abstract base class for of a {@link VirtualConsole} that has a virtual mount point + * in the filesystem. + */ +public abstract class VirtualMountPointConsole extends VirtualConsole { + + private static final String DEFAULT_STORAGE_NAME = "storage"; + +// private static File sVirtualStorageDir; + + private static List<VirtualMountPointConsole> sVirtualConsoles; + private static Identity sVirtualIdentity; + private static Permissions sVirtualFolderPermissions; + + public VirtualMountPointConsole(Context ctx) { + super(ctx); + } + + /** + * Should return the name of the mount point name + * + * @return String The name of the mount point name of this console. + */ + public abstract String getMountPointName(); + + /** + * Method that returns if the console is secure + * + * @return boolean If the console is secure + */ + public abstract boolean isSecure(); + + /** + * Method that returns if the console is remote + * + * @return boolean If the console is remote + */ + public abstract boolean isRemote(); + + /** + * Method that returns if the console is mounted + * + * @return boolean If the console is mounted + */ + public abstract boolean isMounted(); + + /** + * Method that unmounts the filesystem + * + * @return boolean If the filesystem was unmounted + */ + public abstract boolean unmount(); + + /** + * Returns the mountpoints for the console + * + * @return List<MountPoint> The list of mountpoints handled by the console + */ + public abstract List<MountPoint> getMountPoints(); + + /** + * Returns the disk usage of every mountpoint for the console + * + * @return List<DiskUsage> The list of disk usage of the mountpoints handled by the console + */ + public abstract List<DiskUsage> getDiskUsage(); + + /** + * Returns the disk usage of the path + * + * @param path The path to check + * @return DiskUsage The disk usage for the passed path + */ + public abstract DiskUsage getDiskUsage(String path); + + /** + * Method that register all the implemented virtual consoles. This method should + * be called only once on the application instantiation. + * + * @param context The current context + */ + public static void registerVirtualConsoles(Context context) { + if (sVirtualConsoles != null) return; + sVirtualConsoles = new ArrayList<VirtualMountPointConsole>(); + sVirtualIdentity = AIDHelper.createVirtualIdentity(); + sVirtualFolderPermissions = Permissions.createDefaultFolderPermissions(); + + int bufferSize = context.getResources().getInteger(R.integer.buffer_size); + + // Register every known virtual mountable console + sVirtualConsoles.add(SecureConsole.getInstance(context, bufferSize)); + // TODO Add remote consoles. Not ready for now. + // sVirtualConsoles.add(new RemoteConsole(context)); + } + + /** + * Method that returns the virtual storage directory + * @return + */ + private static File getVirtualStorageDir() { + final Context context = FileManagerApplication.getInstance().getApplicationContext(); + File dir = new File(context.getString(R.string.virtual_storage_dir)); + AccessMode mode = FileManagerApplication.getAccessMode(); + if (mode.equals(AccessMode.SAFE) || !dir.isDirectory()) { + // Chroot environment (create a folder inside the external storage) + return getChrootedVirtualStorageDir(); + } + return dir; + } + + /** + * Method that returns the chrooted virtual storage directory + * + * @return File The Virtual storage directory + */ + private static File getChrootedVirtualStorageDir() { + File root = new File(Environment.getExternalStorageDirectory(), DEFAULT_STORAGE_NAME); + root.mkdir(); + return root; + } + + /** + * Method that list all the virtual directories + * + * @return List<Directory> The list of virtual directories + */ + public static List<Directory> getVirtualMountableDirectories() { + final Date date = new Date(System.currentTimeMillis() - SystemClock.elapsedRealtime()); + List<Directory> directories = new ArrayList<Directory>(); + for (VirtualMountPointConsole console : sVirtualConsoles) { + File dir = null; + do { + dir = console.getVirtualMountPoint(); + } while (dir.getParentFile() != null && !isVirtualStorageDir(dir.getParent())); + + if (dir != null) { + Directory directory = new Directory( + dir.getName(), + getVirtualStorageDir().getAbsolutePath(), + sVirtualIdentity.getUser(), + sVirtualIdentity.getGroup(), + sVirtualFolderPermissions, + date, date, date); + directory.setSecure(console.isSecure()); + directory.setRemote(console.isRemote()); + + if (!directories.contains(directory)) { + directories.add(directory); + } + } + } + return directories; + } + + /** + * Method that returns the virtual mountpoints of every register console + * @return + */ + public static List<MountPoint> getVirtualMountPoints() { + List<MountPoint> mountPoints = new ArrayList<MountPoint>(); + for (VirtualMountPointConsole console : sVirtualConsoles) { + mountPoints.addAll(console.getMountPoints()); + } + return mountPoints; + } + + /** + * Method that returns the virtual disk usage of the mountpoints of every register console + * @return + */ + public static List<DiskUsage> getVirtualDiskUsage() { + List<DiskUsage> diskUsage = new ArrayList<DiskUsage>(); + for (VirtualMountPointConsole console : sVirtualConsoles) { + diskUsage.addAll(console.getDiskUsage()); + } + return diskUsage; + } + + /** + * Returns if the passed directory is the current virtual storage directory + * + * @param directory The directory to check + * @return boolean If is the current virtual storage directory + */ + public static boolean isVirtualStorageDir(String directory) { + return getVirtualStorageDir().equals(new File(directory)); + } + + /** + * Returns if the passed resource belongs to a virtual filesystem + * + * @param path The path to check + * @return boolean If is the resource belongs to a virtual filesystem + */ + public static boolean isVirtualStorageResource(String path) { + for (VirtualMountPointConsole console : sVirtualConsoles) { + if (FileHelper.belongsToDirectory(new File(path), console.getVirtualMountPoint())) { + return true; + } + } + return false; + } + + /** + * Method that returns the virtual console for the path or null if the path + * is not a virtual filesystem + * + * @param path the path to check + * @return VirtualMountPointConsole The found console + */ + public static VirtualMountPointConsole getVirtualConsoleForPath(String path) { + File file = new File(path); + for (VirtualMountPointConsole console : sVirtualConsoles) { + if (FileHelper.belongsToDirectory(file, console.getVirtualMountPoint())) { + return console; + } + } + return null; + } + + public static List<Console> getVirtualConsoleForSearchPath(String path) { + List<Console> consoles = new ArrayList<Console>(); + File dir = new File(path); + for (VirtualMountPointConsole console : sVirtualConsoles) { + if (FileHelper.belongsToDirectory(console.getVirtualMountPoint(), dir)) { + // Only mount consoles can participate in the search + if (console.isMounted()) { + consoles.add(console); + } + } + } + return consoles; + } + + /** + * Returns if the passed directory is the virtual mountpoint directory of the virtual console + * + * @param directory The directory to check + * @return boolean If is the virtual mountpoint directory of the virtual console + */ + public boolean isVirtualMountPointDir(String directory) { + return getVirtualMountPoint().equals(new File(directory)); + } + + /** + * Method that returns the virtual mount point for this console + * + * @return String The virtual mount point + */ + public final File getVirtualMountPoint() { + return new File(getVirtualStorageDir(), getMountPointName()); + } +} diff --git a/src/com/cyanogenmod/filemanager/console/java/JavaConsole.java b/src/com/cyanogenmod/filemanager/console/java/JavaConsole.java index daf30525..f59f7461 100644 --- a/src/com/cyanogenmod/filemanager/console/java/JavaConsole.java +++ b/src/com/cyanogenmod/filemanager/console/java/JavaConsole.java @@ -17,43 +17,31 @@ package com.cyanogenmod.filemanager.console.java; import android.content.Context; -import android.os.Process; import android.util.Log; import com.cyanogenmod.filemanager.commands.Executable; import com.cyanogenmod.filemanager.commands.ExecutableFactory; -import com.cyanogenmod.filemanager.commands.SIGNAL; import com.cyanogenmod.filemanager.commands.java.JavaExecutableFactory; import com.cyanogenmod.filemanager.commands.java.Program; import com.cyanogenmod.filemanager.console.CommandNotFoundException; -import com.cyanogenmod.filemanager.console.Console; import com.cyanogenmod.filemanager.console.ConsoleAllocException; import com.cyanogenmod.filemanager.console.ExecutionException; import com.cyanogenmod.filemanager.console.InsufficientPermissionsException; import com.cyanogenmod.filemanager.console.NoSuchFileOrDirectory; import com.cyanogenmod.filemanager.console.OperationTimeoutException; import com.cyanogenmod.filemanager.console.ReadOnlyFilesystemException; -import com.cyanogenmod.filemanager.model.AID; -import com.cyanogenmod.filemanager.model.Group; -import com.cyanogenmod.filemanager.model.Identity; -import com.cyanogenmod.filemanager.model.User; -import com.cyanogenmod.filemanager.util.AIDHelper; - -import java.util.ArrayList; +import com.cyanogenmod.filemanager.console.VirtualConsole; /** - * An implementation of a {@link Console} based on a java implementation.<br/> + * An implementation of a {@link VirtualConsole} based on a java implementation.<br/> * <br/> * This console is a non-privileged console an many of the functionality is not implemented * because can't be obtain from java api. */ -public final class JavaConsole extends Console { +public final class JavaConsole extends VirtualConsole { private static final String TAG = "JavaConsole"; //$NON-NLS-1$ - private boolean mActive; - - private final Context mCtx; private final int mBufferSize; /** @@ -63,45 +51,17 @@ public final class JavaConsole extends Console { * @param bufferSize The buffer size */ public JavaConsole(Context ctx, int bufferSize) { - super(); - this.mCtx = ctx; + super(ctx); this.mBufferSize = bufferSize; } - /** - * {@inheritDoc} - */ - @Override - public void alloc() throws ConsoleAllocException { - try { - if (isTrace()) { - Log.v(TAG, "Allocating Java console"); //$NON-NLS-1$ - } - this.mActive = true; - } catch (Exception e) { - Log.e(TAG, "Failed to allocate Java console", e); //$NON-NLS-1$ - throw new ConsoleAllocException("failed to build console", e); //$NON-NLS-1$ - } - } /** * {@inheritDoc} */ @Override - public void dealloc() { - if (isTrace()) { - Log.v(TAG, "Deallocating Java console"); //$NON-NLS-1$ - } - this.mActive = true; - } - - /** - * {@inheritDoc} - */ - @Override - public void realloc() throws ConsoleAllocException { - dealloc(); - alloc(); + public String getName() { + return "Java"; } /** @@ -116,48 +76,10 @@ public final class JavaConsole extends Console { * {@inheritDoc} */ @Override - public Identity getIdentity() { - AID aid = AIDHelper.getAID(Process.myUid()); - if (aid == null) return null; - return new Identity( - new User(aid.getId(), aid.getName()), - new Group(aid.getId(), aid.getName()), - new ArrayList<Group>()); - } - - /** - * {@inheritDoc} - */ - @Override - public boolean isPrivileged() { - return false; - } - - /** - * {@inheritDoc} - */ - @Override - public boolean isActive() { - return this.mActive; - } - - /** - * Method that returns the current context - * - * @return Context The current context - */ - public Context getCtx() { - return this.mCtx; - } - - /** - * {@inheritDoc} - */ - @Override - public synchronized void execute(Executable executable) throws ConsoleAllocException, - InsufficientPermissionsException, NoSuchFileOrDirectory, - OperationTimeoutException, ExecutionException, - CommandNotFoundException, ReadOnlyFilesystemException { + public synchronized void execute(Executable executable, Context ctx) + throws ConsoleAllocException, InsufficientPermissionsException, NoSuchFileOrDirectory, + OperationTimeoutException, ExecutionException, CommandNotFoundException, + ReadOnlyFilesystemException { // Check that the program is a java program try { Program p = (Program)executable; @@ -201,28 +123,4 @@ public final class JavaConsole extends Console { } } - /** - * {@inheritDoc} - */ - @Override - public boolean onCancel() { - return false; - } - - /** - * {@inheritDoc} - */ - @Override - public boolean onSendSignal(SIGNAL signal) { - return false; - } - - /** - * {@inheritDoc} - */ - @Override - public boolean onEnd() { - return false; - } - }
\ No newline at end of file diff --git a/src/com/cyanogenmod/filemanager/console/remote/RemoteConsole.java b/src/com/cyanogenmod/filemanager/console/remote/RemoteConsole.java new file mode 100644 index 00000000..a57b5d51 --- /dev/null +++ b/src/com/cyanogenmod/filemanager/console/remote/RemoteConsole.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.cyanogenmod.filemanager.console.remote; + +import android.content.Context; + +import com.cyanogenmod.filemanager.commands.Executable; +import com.cyanogenmod.filemanager.commands.ExecutableFactory; +import com.cyanogenmod.filemanager.console.CommandNotFoundException; +import com.cyanogenmod.filemanager.console.ConsoleAllocException; +import com.cyanogenmod.filemanager.console.ExecutionException; +import com.cyanogenmod.filemanager.console.InsufficientPermissionsException; +import com.cyanogenmod.filemanager.console.NoSuchFileOrDirectory; +import com.cyanogenmod.filemanager.console.OperationTimeoutException; +import com.cyanogenmod.filemanager.console.ReadOnlyFilesystemException; +import com.cyanogenmod.filemanager.console.VirtualMountPointConsole; +import com.cyanogenmod.filemanager.model.DiskUsage; +import com.cyanogenmod.filemanager.model.MountPoint; + +import java.util.ArrayList; +import java.util.List; + +/** + * An implementation of a {@link VirtualMountPointConsole} for remote filesystems + */ +public class RemoteConsole extends VirtualMountPointConsole { + + /** + * Constructor of <code>RemoteConsole</code> + * + * @param ctx The current context + */ + public RemoteConsole(Context ctx) { + super(ctx); + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return "Remote"; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isSecure() { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isRemote() { + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isMounted() { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean unmount() { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public List<MountPoint> getMountPoints() { + List<MountPoint> mountPoints = new ArrayList<MountPoint>(); + return mountPoints; + } + + /** + * {@inheritDoc} + */ + @Override + public List<DiskUsage> getDiskUsage() { + List<DiskUsage> diskUsage = new ArrayList<DiskUsage>(); + return diskUsage; + } + + /** + * {@inheritDoc} + */ + @Override + public DiskUsage getDiskUsage(String path) { + // TODO Fix when remote console will be implemented + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public String getMountPointName() { + return "remote"; + } + + /** + * {@inheritDoc} + */ + @Override + public ExecutableFactory getExecutableFactory() { + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public synchronized void execute(Executable executable, Context ctx) + throws ConsoleAllocException, InsufficientPermissionsException, NoSuchFileOrDirectory, + OperationTimeoutException, ExecutionException, CommandNotFoundException, + ReadOnlyFilesystemException { + + } +} diff --git a/src/com/cyanogenmod/filemanager/console/secure/SecureConsole.java b/src/com/cyanogenmod/filemanager/console/secure/SecureConsole.java new file mode 100644 index 00000000..be019a66 --- /dev/null +++ b/src/com/cyanogenmod/filemanager/console/secure/SecureConsole.java @@ -0,0 +1,640 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.cyanogenmod.filemanager.console.secure; + +import android.content.Context; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Handler; +import android.os.Message; +import android.os.UserHandle; +import android.os.Handler.Callback; +import android.util.Log; +import android.widget.Toast; + +import com.cyanogenmod.filemanager.FileManagerApplication; +import com.cyanogenmod.filemanager.R; +import com.cyanogenmod.filemanager.commands.Executable; +import com.cyanogenmod.filemanager.commands.ExecutableFactory; +import com.cyanogenmod.filemanager.commands.MountExecutable; +import com.cyanogenmod.filemanager.commands.secure.Program; +import com.cyanogenmod.filemanager.commands.secure.SecureExecutableFactory; +import com.cyanogenmod.filemanager.console.AuthenticationFailedException; +import com.cyanogenmod.filemanager.console.CancelledOperationException; +import com.cyanogenmod.filemanager.console.CommandNotFoundException; +import com.cyanogenmod.filemanager.console.Console; +import com.cyanogenmod.filemanager.console.ConsoleAllocException; +import com.cyanogenmod.filemanager.console.ExecutionException; +import com.cyanogenmod.filemanager.console.InsufficientPermissionsException; +import com.cyanogenmod.filemanager.console.NoSuchFileOrDirectory; +import com.cyanogenmod.filemanager.console.OperationTimeoutException; +import com.cyanogenmod.filemanager.console.ReadOnlyFilesystemException; +import com.cyanogenmod.filemanager.console.VirtualMountPointConsole; +import com.cyanogenmod.filemanager.model.DiskUsage; +import com.cyanogenmod.filemanager.model.MountPoint; +import com.cyanogenmod.filemanager.preferences.FileManagerSettings; +import com.cyanogenmod.filemanager.preferences.Preferences; +import com.cyanogenmod.filemanager.util.DialogHelper; +import com.cyanogenmod.filemanager.util.ExceptionUtil; +import com.cyanogenmod.filemanager.util.FileHelper; + +import org.apache.http.auth.AuthenticationException; + +import de.schlichtherle.truezip.crypto.raes.RaesAuthenticationException; +import de.schlichtherle.truezip.file.TArchiveDetector; +import de.schlichtherle.truezip.file.TFile; +import de.schlichtherle.truezip.file.TVFS; +import de.schlichtherle.truezip.key.CancelledOperation; +import static de.schlichtherle.truezip.fs.FsSyncOption.CLEAR_CACHE; +import static de.schlichtherle.truezip.fs.FsSyncOption.FORCE_CLOSE_INPUT; +import static de.schlichtherle.truezip.fs.FsSyncOption.FORCE_CLOSE_OUTPUT; +import de.schlichtherle.truezip.util.BitField; + +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * A secure implementation of a {@link VirtualMountPointConsole} that uses a + * secure filesystem backend + */ +public class SecureConsole extends VirtualMountPointConsole { + + public static final String TAG = "SecureConsole"; + + /** The singleton TArchiveDetector which enclosure this driver **/ + public static final TArchiveDetector DETECTOR = new TArchiveDetector( + SecureStorageDriverProvider.SINGLETON, SecureStorageDriverProvider.SINGLETON.get()); + + public static String getSecureStorageName() { + return String.format("storage.%s.%s", + String.valueOf(UserHandle.myUserId()), + SecureStorageDriverProvider.SECURE_STORAGE_SCHEME); + } + + public static TFile getSecureStorageRoot() { + return new TFile(FileManagerApplication.getInstance().getExternalFilesDir(null), + getSecureStorageName(), DETECTOR); + } + + public static URI getSecureStorageRootUri() { + return new File(FileManagerApplication.getInstance().getExternalFilesDir(null), + getSecureStorageName()).toURI(); + } + + private static SecureConsole sConsole = null; + + public final Handler mSyncHandler; + + private boolean mIsMounted; + private boolean mRequiresSync; + + private final int mBufferSize; + + private static final long SYNC_WAIT = 10000L; + + private static final int MSG_SYNC_FS = 0; + + private final ExecutorService mExecutorService = Executors.newFixedThreadPool(1); + + private final Callback mSyncCallback = new Callback() { + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_SYNC_FS: + mExecutorService.execute(new Runnable() { + @Override + public void run() { + sync(); + } + }); + break; + + default: + break; + } + return true; + } + }; + + /** + * Return an instance of the current console + * @return + */ + public static synchronized SecureConsole getInstance(Context ctx, int bufferSize) { + if (sConsole == null) { + sConsole = new SecureConsole(ctx, bufferSize); + } + return sConsole; + } + + private final TFile mStorageRoot; + private final String mStorageName; + + /** + * Constructor of <code>SecureConsole</code> + * + * @param ctx The current context + */ + private SecureConsole(Context ctx, int bufferSize) { + super(ctx); + mIsMounted = false; + mBufferSize = bufferSize; + mSyncHandler = new Handler(mSyncCallback); + mStorageRoot = getSecureStorageRoot(); + mStorageName = getSecureStorageName(); + + // Save a copy of the console. This has a unique instance for all the app + if (sConsole != null) { + sConsole = this; + } + } + + @Override + public void dealloc() { + super.dealloc(); + + // Synchronize the underlaying storage + mSyncHandler.removeMessages(MSG_SYNC_FS); + sync(); + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return "Secure"; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isSecure() { + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isMounted() { + return mIsMounted; + } + + /** + * {@inheritDoc} + */ + @Override + public List<MountPoint> getMountPoints() { + // This console only has one mountpoint + List<MountPoint> mountPoints = new ArrayList<MountPoint>(); + String status = mIsMounted ? MountExecutable.READWRITE : MountExecutable.READONLY; + mountPoints.add(new MountPoint(getVirtualMountPoint().getAbsolutePath(), + "securestorage", "securestoragefs", status, 0, 0, true, false)); + return mountPoints; + } + + /** + * {@inheritDoc} + */ + @Override + @SuppressWarnings("deprecation") + public List<DiskUsage> getDiskUsage() { + // This console only has one mountpoint, and is fully usage + List<DiskUsage> diskUsage = new ArrayList<DiskUsage>(); + File mp = mStorageRoot.getFile(); + diskUsage.add(new DiskUsage(mp.getAbsolutePath(), + mp.getTotalSpace(), + mp.length(), + mp.getTotalSpace() - mp.length())); + return diskUsage; + } + + /** + * Method that returns if the path belongs to the secure storage + * + * @param path The path to check + * @return + */ + public boolean isSecureStorageResource(String path) { + return FileHelper.belongsToDirectory(new File(path), getVirtualMountPoint()); + } + + /** + * {@inheritDoc} + */ + @Override + public DiskUsage getDiskUsage(String path) { + if (isSecureStorageResource(path)) { + return getDiskUsage().get(0); + } + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public String getMountPointName() { + return "secure"; + } + + + /** + * {@inheritDoc} + */ + @Override + public boolean isRemote() { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public ExecutableFactory getExecutableFactory() { + return new SecureExecutableFactory(this); + } + + /** + * Method that request a reset of the current password + */ + public void requestReset(final Context ctx) { + AsyncTask<Void, Void, Boolean> task = new AsyncTask<Void, Void, Boolean>() { + @Override + protected Boolean doInBackground(Void... params) { + boolean result = false; + + // Unmount the filesystem + if (mIsMounted) { + unmount(); + } + try { + SecureStorageKeyManagerProvider.SINGLETON.reset(); + + // Mount with the new key + mount(ctx); + + // In order to claim a write, we need to be sure that an operation is + // done to disk before unmount the device. + try { + String testName = UUID.randomUUID().toString(); + TFile test = new TFile(getSecureStorageRoot(), testName); + test.createNewFile(); + test.rm(); + result = true; + } catch (IOException ex) { + ExceptionUtil.translateException(ctx, ex); + } + + } catch (Exception ex) { + ExceptionUtil.translateException(ctx, ex); + } finally { + unmount(); + } + + return result; + } + + @Override + protected void onPostExecute(Boolean result) { + if (result) { + // Success + DialogHelper.showToast(ctx, R.string.msgs_success, Toast.LENGTH_SHORT); + } + } + + }; + task.execute(); + } + + /** + * Method that request a delete of the current password + */ + @SuppressWarnings("deprecation") + public void requestDelete(final Context ctx) { + AsyncTask<Void, Void, Boolean> task = new AsyncTask<Void, Void, Boolean>() { + @Override + protected Boolean doInBackground(Void... params) { + boolean result = false; + + // Unmount the filesystem + if (mIsMounted) { + unmount(); + } + try { + SecureStorageKeyManagerProvider.SINGLETON.delete(); + + // Test mount/unmount + mount(ctx); + unmount(); + + // Password is valid. Delete the storage + mStorageRoot.getFile().delete(); + + // Send an broadcast to notify that the mount state of this filesystem changed + Intent intent = new Intent(FileManagerSettings.INTENT_MOUNT_STATUS_CHANGED); + intent.putExtra(FileManagerSettings.EXTRA_MOUNTPOINT, + getVirtualMountPoint().toString()); + intent.putExtra(FileManagerSettings.EXTRA_STATUS, MountExecutable.READONLY); + getCtx().sendBroadcast(intent); + + result = true; + + } catch (Exception ex) { + ExceptionUtil.translateException(ctx, ex); + } + + return result; + } + + @Override + protected void onPostExecute(Boolean result) { + if (result) { + // Success + DialogHelper.showToast(ctx, R.string.msgs_success, Toast.LENGTH_SHORT); + } + } + + }; + task.execute(); + } + + /** + * {@inheritDoc} + */ + public boolean unmount() { + // Unmount the filesystem and cancel the cached key + mRequiresSync = true; + boolean ret = sync(); + if (ret) { + SecureStorageKeyManagerProvider.SINGLETON.unmount(); + } + mIsMounted = false; + + // Send an broadcast to notify that the mount state of this filesystem changed + Intent intent = new Intent(FileManagerSettings.INTENT_MOUNT_STATUS_CHANGED); + intent.putExtra(FileManagerSettings.EXTRA_MOUNTPOINT, + getVirtualMountPoint().toString()); + intent.putExtra(FileManagerSettings.EXTRA_STATUS, MountExecutable.READONLY); + getCtx().sendBroadcast(intent); + + return mIsMounted; + } + + /** + * Method that verifies if the current storage is open and mount it + * + * @param ctx The current context + * @throws CancelledOperationException If the operation was cancelled (by the user) + * @throws AuthenticationException If the secure storage isn't unlocked + * @throws NoSuchFileOrDirectory If the secure storage isn't accessible + */ + @SuppressWarnings("deprecation") + public synchronized void mount(Context ctx) + throws CancelledOperationException, AuthenticationFailedException, + NoSuchFileOrDirectory { + if (!mIsMounted) { + File root = mStorageRoot.getFile(); + try { + boolean newStorage = !root.exists(); + mStorageRoot.mount(); + if (newStorage) { + // Force a synchronization + mRequiresSync = true; + sync(); + } else { + // Remove any previous cache files (if not sync invoked) + clearCache(ctx); + } + + // The device is mounted + mIsMounted = true; + + // Send an broadcast to notify that the mount state of this filesystem changed + Intent intent = new Intent(FileManagerSettings.INTENT_MOUNT_STATUS_CHANGED); + intent.putExtra(FileManagerSettings.EXTRA_MOUNTPOINT, + getVirtualMountPoint().toString()); + intent.putExtra(FileManagerSettings.EXTRA_STATUS, MountExecutable.READWRITE); + getCtx().sendBroadcast(intent); + + } catch (IOException ex) { + if (ex.getCause() != null && ex.getCause() instanceof CancelledOperation) { + throw new CancelledOperationException(); + } + if (ex.getCause() != null && ex.getCause() instanceof RaesAuthenticationException) { + throw new AuthenticationFailedException(ctx.getString( + R.string.secure_storage_unlock_failed)); + } + Log.e(TAG, String.format("Failed to open secure storage: %s", root, ex)); + throw new NoSuchFileOrDirectory(); + } + } + } + + /** + * Method that returns if the path is the real secure storage file + * + * @param path The path to check + * @return boolean If the path is the secure storage + */ + public static boolean isSecureStorageDir(String path) { + Console vc = getVirtualConsoleForPath(path); + if (vc != null && vc instanceof SecureConsole) { + return isSecureStorageDir(((SecureConsole) vc).buildRealFile(path)); + } + return false; + } + + /** + * Method that returns if the path is the real secure storage file + * + * @param path The path to check + * @return boolean If the path is the secure storage + */ + public static boolean isSecureStorageDir(TFile path) { + return getSecureStorageRoot().equals(path); + } + + /** + * Method that build a real file from a virtual path + * + * @param path The path from build the real file + * @return TFile The real file + */ + public TFile buildRealFile(String path) { + String real = mStorageRoot.toString(); + String virtual = getVirtualMountPoint().toString(); + String src = path.replace(virtual, real); + return new TFile(src, DETECTOR); + } + + /** + * Method that build a virtual file from a real path + * + * @param path The path from build the virtual file + * @return TFile The virtual file + */ + public String buildVirtualPath(TFile path) { + String real = mStorageRoot.toString(); + String virtual = getVirtualMountPoint().toString(); + String dst = path.toString().replace(real, virtual); + return dst; + } + + /** + * {@inheritDoc} + */ + @Override + public synchronized void execute(Executable executable, Context ctx) + throws ConsoleAllocException, InsufficientPermissionsException, NoSuchFileOrDirectory, + OperationTimeoutException, ExecutionException, CommandNotFoundException, + ReadOnlyFilesystemException, CancelledOperationException, + AuthenticationFailedException { + // Check that the program is a secure program + try { + Program p = (Program) executable; + p.setBufferSize(mBufferSize); + } catch (Throwable e) { + Log.e(TAG, String.format("Failed to resolve program: %s", //$NON-NLS-1$ + executable.getClass().toString()), e); + throw new CommandNotFoundException("executable is not a program", e); //$NON-NLS-1$ + } + + //Auditing program execution + if (isTrace()) { + Log.v(TAG, String.format("Executing program: %s", //$NON-NLS-1$ + executable.getClass().toString())); + } + + + final Program program = (Program) executable; + + // Open storage encryption (if required) + if (program.requiresOpen()) { + mount(ctx); + } + + // Execute the program + program.setTrace(isTrace()); + if (program.isAsynchronous()) { + // Execute in a thread + Thread t = new Thread() { + @Override + public void run() { + try { + program.execute(); + requestSync(program); + } catch (Exception e) { + // Program must use onException to communicate exceptions + Log.v(TAG, + String.format("Async execute failed program: %s", //$NON-NLS-1$ + program.getClass().toString())); + } + } + }; + t.start(); + + } else { + // Synchronous execution + program.execute(); + requestSync(program); + } + } + + /** + * Request a synchronization of the underlying filesystem + * + * @param program The last called program + */ + private void requestSync(Program program) { + if (program.requiresSync()) { + mRequiresSync = true; + } + + // There is some changes to synchronize? + if (mRequiresSync) { + Boolean defaultValue = ((Boolean)FileManagerSettings. + SETTINGS_SECURE_STORAGE_DELAYED_SYNC.getDefaultValue()); + Boolean delayedSync = + Boolean.valueOf( + Preferences.getSharedPreferences().getBoolean( + FileManagerSettings.SETTINGS_SECURE_STORAGE_DELAYED_SYNC.getId(), + defaultValue.booleanValue())); + mSyncHandler.removeMessages(MSG_SYNC_FS); + if (delayedSync) { + // Request a sync in 30 seconds, if users is not doing any operation + mSyncHandler.sendEmptyMessageDelayed(MSG_SYNC_FS, SYNC_WAIT); + } else { + // Do the synchronization now + mSyncHandler.sendEmptyMessage(MSG_SYNC_FS); + } + } + } + + /** + * Synchronize the underlying filesystem + * + * @retun boolean If the unmount success + */ + public synchronized boolean sync() { + if (mRequiresSync) { + Log.i(TAG, "Syncing underlaying storage"); + mRequiresSync = false; + // Sync the underlying storage + try { + TVFS.sync(mStorageRoot, + BitField.of(CLEAR_CACHE) + .set(FORCE_CLOSE_INPUT, true) + .set(FORCE_CLOSE_OUTPUT, true)); + return true; + } catch (IOException e) { + Log.e(TAG, String.format("Failed to sync secure storage: %s", mStorageRoot, e)); + return false; + } + } + return true; + } + + /** + * Method that clear the cache + * + * @param ctx The current context + */ + private void clearCache(Context ctx) { + File filesDir = ctx.getExternalFilesDir(null); + File[] cacheFiles = filesDir.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String filename) { + return filename.startsWith(mStorageName) + && filename.endsWith(".tmp"); + } + }); + for (File cacheFile : cacheFiles) { + cacheFile.delete(); + } + } +} diff --git a/src/com/cyanogenmod/filemanager/console/secure/SecureStorageDriver.java b/src/com/cyanogenmod/filemanager/console/secure/SecureStorageDriver.java new file mode 100644 index 00000000..df2e4822 --- /dev/null +++ b/src/com/cyanogenmod/filemanager/console/secure/SecureStorageDriver.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cyanogenmod.filemanager.console.secure; + +import de.schlichtherle.truezip.fs.archive.zip.raes.SafeZipRaesDriver; +import de.schlichtherle.truezip.socket.sl.IOPoolLocator; + +/** + * Custom implementation of {@code SafeZipRaesDriver} + */ +public class SecureStorageDriver extends SafeZipRaesDriver { + + // The singleton FsDriver reference + static final SecureStorageDriver SINGLETON = new SecureStorageDriver(); + + /** + * Constructor of {@code SecureStorageDriver} + */ + private SecureStorageDriver() { + super(IOPoolLocator.SINGLETON, SecureStorageKeyManagerProvider.SINGLETON); + } + +} diff --git a/src/com/cyanogenmod/filemanager/console/secure/SecureStorageDriverProvider.java b/src/com/cyanogenmod/filemanager/console/secure/SecureStorageDriverProvider.java new file mode 100644 index 00000000..17555286 --- /dev/null +++ b/src/com/cyanogenmod/filemanager/console/secure/SecureStorageDriverProvider.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cyanogenmod.filemanager.console.secure; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import de.schlichtherle.truezip.fs.FsDriver; +import de.schlichtherle.truezip.fs.FsDriverProvider; +import de.schlichtherle.truezip.fs.FsScheme; +import de.schlichtherle.truezip.fs.file.FileDriver; + +/** + * The SecureStorage driver provider which handles {@code "secure"} data schemes + */ +public class SecureStorageDriverProvider implements FsDriverProvider { + + /** File scheme **/ + public static final String FILE_SCHEME = "file"; + + /** SecureStorage scheme **/ + public static final String SECURE_STORAGE_SCHEME = "secure"; + + /** The singleton instance of this class. */ + static final SecureStorageDriverProvider SINGLETON = new SecureStorageDriverProvider(); + + /** You cannot instantiate this class. */ + private SecureStorageDriverProvider() { + } + + /** + * {@inheritDoc} + */ + @Override + public Map<FsScheme, FsDriver> get() { + return Boot.DRIVERS; + } + + /** A static data utility class used for lazy initialization. */ + private static final class Boot { + static final Map<FsScheme, FsDriver> DRIVERS; + static { + final Map<FsScheme, FsDriver> fast = new LinkedHashMap<FsScheme, FsDriver>(); + fast.put(FsScheme.create(FILE_SCHEME), new FileDriver()); + fast.put(FsScheme.create(SECURE_STORAGE_SCHEME), SecureStorageDriver.SINGLETON); + DRIVERS = Collections.unmodifiableMap(fast); + } + } // Boot +} diff --git a/src/com/cyanogenmod/filemanager/console/secure/SecureStorageKeyManagerProvider.java b/src/com/cyanogenmod/filemanager/console/secure/SecureStorageKeyManagerProvider.java new file mode 100644 index 00000000..810b94dd --- /dev/null +++ b/src/com/cyanogenmod/filemanager/console/secure/SecureStorageKeyManagerProvider.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cyanogenmod.filemanager.console.secure; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import de.schlichtherle.truezip.crypto.raes.param.AesCipherParameters; +import de.schlichtherle.truezip.key.AbstractKeyManagerProvider; +import de.schlichtherle.truezip.key.KeyManager; +import de.schlichtherle.truezip.key.PromptingKeyManager; +import de.schlichtherle.truezip.key.PromptingKeyProvider; + +/** + * The SecureStorage KeyManager provider + */ +public class SecureStorageKeyManagerProvider extends AbstractKeyManagerProvider { + + /** The singleton instance of this class. */ + static final SecureStorageKeyManagerProvider SINGLETON = + new SecureStorageKeyManagerProvider(); + + private final static SecureStorageKeyPromptDialog PROMPT_DIALOG = + new SecureStorageKeyPromptDialog(); + + /** You cannot instantiate this class. */ + private SecureStorageKeyManagerProvider() { + } + + /** + * {@inheritDoc} + */ + @Override + public Map<Class<?>, KeyManager<?>> get() { + return Boot.MANAGERS; + } + + /** + * @hide + */ + void unmount() { + PROMPT_DIALOG.umount(); + getKeyProvider().setKey(null); + } + + /** + * @hide + */ + void reset() { + PROMPT_DIALOG.reset(); + getKeyProvider().setKey(null); + } + + /** + * @hide + */ + void delete() { + PROMPT_DIALOG.delete(); + getKeyProvider().setKey(null); + } + + @SuppressWarnings("unchecked") + private static PromptingKeyProvider<AesCipherParameters> getKeyProvider() { + PromptingKeyManager<AesCipherParameters> keyManager = + (PromptingKeyManager<AesCipherParameters>) Boot.MANAGERS.get( + AesCipherParameters.class); + return (PromptingKeyProvider<AesCipherParameters>) keyManager.getKeyProvider( + SecureConsole.getSecureStorageRootUri()); + } + + /** A static data utility class used for lazy initialization. */ + private static final class Boot { + static final Map<Class<?>, KeyManager<?>> MANAGERS; + static { + final PromptingKeyManager<AesCipherParameters> promptKeyManager = + new PromptingKeyManager<AesCipherParameters>(PROMPT_DIALOG); + final Map<Class<?>, KeyManager<?>> fast = new LinkedHashMap<Class<?>, KeyManager<?>>(); + fast.put(AesCipherParameters.class, promptKeyManager); + MANAGERS = Collections.unmodifiableMap(fast); + + // We need that the provider ask always for a password + getKeyProvider().setAskAlwaysForWriteKey(true); + } + } // class Boot + +} diff --git a/src/com/cyanogenmod/filemanager/console/secure/SecureStorageKeyPromptDialog.java b/src/com/cyanogenmod/filemanager/console/secure/SecureStorageKeyPromptDialog.java new file mode 100644 index 00000000..2db530c4 --- /dev/null +++ b/src/com/cyanogenmod/filemanager/console/secure/SecureStorageKeyPromptDialog.java @@ -0,0 +1,399 @@ +/* + * Copyright (C) 2014 The CyanogenMod Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cyanogenmod.filemanager.console.secure; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnCancelListener; +import android.content.DialogInterface.OnClickListener; +import android.content.DialogInterface.OnDismissListener; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; + +import com.cyanogenmod.filemanager.FileManagerApplication; +import com.cyanogenmod.filemanager.R; +import com.cyanogenmod.filemanager.ui.ThemeManager; +import com.cyanogenmod.filemanager.ui.ThemeManager.Theme; +import com.cyanogenmod.filemanager.util.DialogHelper; + +import de.schlichtherle.truezip.crypto.raes.param.AesCipherParameters; +import de.schlichtherle.truezip.crypto.raes.Type0RaesParameters.KeyStrength; +import de.schlichtherle.truezip.key.KeyPromptingCancelledException; +import de.schlichtherle.truezip.key.KeyPromptingInterruptedException; +import de.schlichtherle.truezip.key.PromptingKeyProvider.Controller; +import de.schlichtherle.truezip.key.UnknownKeyException; + +/** + * A class that remembers all the secure storage + */ +public class SecureStorageKeyPromptDialog + implements de.schlichtherle.truezip.key.PromptingKeyProvider.View<AesCipherParameters> { + + private static final int MIN_PASSWORD_LENGTH = 8; + + private static final int MSG_REQUEST_UNLOCK_DIALOG = 1; + + private static boolean sResetInProgress; + private static boolean sDeleteInProgress; + private static transient AesCipherParameters sOldUnlockKey = null; + private static transient AesCipherParameters sUnlockKey = null; + private static transient AesCipherParameters sOldUnlockKeyTemp = null; + private static transient AesCipherParameters sUnlockKeyTemp = null; + private static final Object WAIT_SYNC = new Object(); + + /** + * An activity that simulates a dialog over the activity that requested the key prompt. + */ + public static class SecureStorageKeyPromptActivity extends Activity + implements OnClickListener, TextWatcher { + + private AlertDialog mDialog; + + private TextView mMessage; + private EditText mOldKey; + private EditText mKey; + private EditText mRepeatKey; + private TextView mValidationMsg; + private Button mUnlock; + + private boolean mNewStorage; + private boolean mResetPassword; + private boolean mDeleteStorage; + + AesCipherParameters mOldKeyParams; + AesCipherParameters mKeyParams; + + @Override + @SuppressWarnings("deprecation") + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Check with java.io.File instead of TFile because TFile#exists() will + // check for password key, which is currently locked + mNewStorage = !SecureConsole.getSecureStorageRoot().getFile().exists(); + mResetPassword = sResetInProgress; + mDeleteStorage = sDeleteInProgress; + + // Set the theme before setContentView + Theme theme = ThemeManager.getCurrentTheme(this); + theme.setBaseTheme(this, true); + + // Load the dialog's custom layout + ViewGroup v = (ViewGroup) LayoutInflater.from(this).inflate( + R.layout.unlock_dialog_message, null); + mMessage = (TextView) v.findViewById(R.id.unlock_dialog_message); + mOldKey = (EditText) v.findViewById(R.id.unlock_old_password); + mOldKey.addTextChangedListener(this); + mKey = (EditText) v.findViewById(R.id.unlock_password); + mKey.addTextChangedListener(this); + mRepeatKey = (EditText) v.findViewById(R.id.unlock_repeat); + mRepeatKey.addTextChangedListener(this); + View oldPasswordLayout = v.findViewById(R.id.unlock_old_password_layout); + View repeatLayout = v.findViewById(R.id.unlock_repeat_layout); + mValidationMsg = (TextView) v.findViewById(R.id.unlock_validation_msg); + + // Load resources + int messageResourceId = R.string.secure_storage_unlock_key_prompt_msg; + int positiveButtonLabelResourceId = R.string.secure_storage_unlock_button; + String title = getString(R.string.secure_storage_unlock_title); + if (mNewStorage) { + positiveButtonLabelResourceId = R.string.secure_storage_create_button; + title = getString(R.string.secure_storage_create_title); + messageResourceId = R.string.secure_storage_unlock_key_new_msg; + } else if (mResetPassword) { + positiveButtonLabelResourceId = R.string.secure_storage_reset_button; + title = getString(R.string.secure_storage_reset_title); + messageResourceId = R.string.secure_storage_unlock_key_reset_msg; + TextView passwordLabel = (TextView) v.findViewById(R.id.unlock_password_title); + passwordLabel.setText(R.string.secure_storage_unlock_new_key_title); + } else if (mDeleteStorage) { + positiveButtonLabelResourceId = R.string.secure_storage_delete_button; + title = getString(R.string.secure_storage_delete_title); + messageResourceId = R.string.secure_storage_unlock_key_delete_msg; + } + + // Set the message according to the storage creation status + mMessage.setText(messageResourceId); + repeatLayout.setVisibility(mNewStorage || mResetPassword ? View.VISIBLE : View.GONE); + oldPasswordLayout.setVisibility(mResetPassword ? View.VISIBLE : View.GONE); + + // Set validation msg + mValidationMsg.setText(getString(R.string.secure_storage_unlock_validation_length, + MIN_PASSWORD_LENGTH)); + mValidationMsg.setVisibility(View.VISIBLE); + + // Create the dialog + mDialog = DialogHelper.createTwoButtonsDialog(this, + positiveButtonLabelResourceId, R.string.cancel, + theme.getResourceId(this,"ic_secure_drawable"), title, v, this); + mDialog.setOnDismissListener(new OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + mDialog.dismiss(); + finish(); + } + }); + mDialog.setOnCancelListener(new OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + sUnlockKeyTemp = null; + mDialog.cancel(); + finish(); + } + }); + mDialog.setCanceledOnTouchOutside(false); + + // Apply the theme to the custom view of the dialog + applyTheme(this, v); + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + + DialogHelper.delegateDialogShow(this, mDialog); + mUnlock = mDialog.getButton(DialogInterface.BUTTON_POSITIVE); + mUnlock.setEnabled(false); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + // Unlock the wait + synchronized (WAIT_SYNC) { + WAIT_SYNC.notify(); + } + } + + @Override + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_POSITIVE: + // Create the AES parameter and set to the prompting view + if (mResetPassword) { + AesCipherParameters params = new AesCipherParameters(); + params.setPassword(mOldKey.getText().toString().toCharArray()); + params.setKeyStrength(KeyStrength.BITS_128); + sOldUnlockKeyTemp = params; + } + AesCipherParameters params = new AesCipherParameters(); + params.setPassword(mKey.getText().toString().toCharArray()); + params.setKeyStrength(KeyStrength.BITS_128); + sUnlockKeyTemp = params; + + // We ended with this dialog + dialog.dismiss(); + break; + + case DialogInterface.BUTTON_NEGATIVE: + // User had cancelled the dialog + dialog.cancel(); + + break; + + default: + break; + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // Ignore + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // Ignore + } + + @Override + public void afterTextChanged(Editable s) { + // Validations: + // * Key must be MIN_PASSWORD_LENGTH characters or more + // * Repeat == Key + String oldkey = mOldKey.getText().toString(); + String key = mKey.getText().toString(); + String repeatKey = mRepeatKey.getText().toString(); + boolean validLength = key.length() >= MIN_PASSWORD_LENGTH && + (!mResetPassword || (mResetPassword && oldkey.length() >= MIN_PASSWORD_LENGTH)); + boolean validEquals = key.equals(repeatKey); + boolean valid = validLength && ((mNewStorage && validEquals) || !mNewStorage); + mUnlock.setEnabled(valid); + + if (!validLength) { + mValidationMsg.setText(getString(R.string.secure_storage_unlock_validation_length, + MIN_PASSWORD_LENGTH)); + mValidationMsg.setVisibility(View.VISIBLE); + } else if (mNewStorage && !validEquals) { + mValidationMsg.setText(R.string.secure_storage_unlock_validation_equals); + mValidationMsg.setVisibility(View.VISIBLE); + } else { + mValidationMsg.setVisibility(View.INVISIBLE); + } + } + + private void applyTheme(Context ctx, ViewGroup root) { + // Apply the current theme + Theme theme = ThemeManager.getCurrentTheme(ctx); + theme.setBackgroundDrawable(ctx, root, "background_drawable"); + theme.setTextColor(ctx, mMessage, "text_color"); + theme.setTextColor(ctx, mOldKey, "text_color"); + theme.setTextColor(ctx, (TextView) root.findViewById(R.id.unlock_old_password_title), + "text_color"); + theme.setTextColor(ctx, mKey, "text_color"); + theme.setTextColor(ctx, (TextView) root.findViewById(R.id.unlock_password_title), + "text_color"); + theme.setTextColor(ctx, mRepeatKey, "text_color"); + theme.setTextColor(ctx, (TextView) root.findViewById(R.id.unlock_repeat_title), + "text_color"); + theme.setTextColor(ctx, mValidationMsg, "text_color"); + mValidationMsg.setCompoundDrawablesWithIntrinsicBounds( + theme.getDrawable(ctx, "filesystem_warning_drawable"), //$NON-NLS-1$ + null, null, null); + } + } + + SecureStorageKeyPromptDialog() { + super(); + sResetInProgress = false; + sDeleteInProgress = false; + sOldUnlockKey = null; + sUnlockKey = null; + } + + @Override + public void promptWriteKey(Controller<AesCipherParameters> controller) + throws UnknownKeyException { + controller.setKey(getOrPromptForKey(false)); + if (sResetInProgress) { + // Not needed any more. Reads are now done with new key + sOldUnlockKey = null; + sResetInProgress = false; + } + } + + @Override + public void promptReadKey(Controller<AesCipherParameters> controller, boolean invalid) + throws UnknownKeyException { + if (!sResetInProgress && invalid) { + sUnlockKey = null; + } + controller.setKey(getOrPromptForKey(true)); + } + + /** + * {@hide} + */ + void umount() { + // Discard current keys + sResetInProgress = false; + sDeleteInProgress = false; + sOldUnlockKey = null; + sUnlockKey = null; + } + + /** + * {@hide} + */ + void reset() { + // Discard current keys + sResetInProgress = true; + sDeleteInProgress = false; + sOldUnlockKey = null; + sUnlockKey = null; + } + + /** + * {@hide} + */ + void delete() { + sDeleteInProgress = true; + sResetInProgress = false; + sOldUnlockKey = null; + } + + /** + * Method that return or prompt the user for the secure storage key + * + * @param read If should return the read or write key + * @return AesCipherParameters The AES cipher parameters + */ + private static synchronized AesCipherParameters getOrPromptForKey(boolean read) + throws UnknownKeyException { + // Check if we have a cached key + if (read && sResetInProgress && sOldUnlockKey != null) { + return sOldUnlockKey; + } + if (sUnlockKey != null) { + return sUnlockKey; + } + + // Need to prompt the user for the secure storage key, so we open a overlay activity + // to show the prompt dialog + Handler handler = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message inputMessage) { + Context ctx = FileManagerApplication.getInstance(); + Intent intent = new Intent(ctx, SecureStorageKeyPromptActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + ctx.startActivity(intent); + } + }; + handler.sendEmptyMessage(MSG_REQUEST_UNLOCK_DIALOG); + + // Wait for the response + synchronized (WAIT_SYNC) { + try { + WAIT_SYNC.wait(); + } catch (InterruptedException ex) { + throw new KeyPromptingInterruptedException(ex); + } + } + + // Request for authentication is done. We need to exit from delete status + sDeleteInProgress = false; + + // Check if the user cancelled the dialog + if (sUnlockKeyTemp == null) { + throw new KeyPromptingCancelledException(); + } + + // Move temporary params to real params + sUnlockKey = sUnlockKeyTemp; + sOldUnlockKey = sOldUnlockKeyTemp; + + AesCipherParameters key = sUnlockKey; + if (sResetInProgress && read) { + key = sOldUnlockKey; + } + return key; + } +} diff --git a/src/com/cyanogenmod/filemanager/console/shell/ShellConsole.java b/src/com/cyanogenmod/filemanager/console/shell/ShellConsole.java index bc97b25a..7a1f5a9e 100644 --- a/src/com/cyanogenmod/filemanager/console/shell/ShellConsole.java +++ b/src/com/cyanogenmod/filemanager/console/shell/ShellConsole.java @@ -16,6 +16,7 @@ package com.cyanogenmod.filemanager.console.shell; +import android.content.Context; import android.util.Log; import com.cyanogenmod.filemanager.FileManagerApplication; @@ -286,7 +287,7 @@ public abstract class ShellConsole extends Console implements Program.ProgramLis //Retrieve identity IdentityExecutable identityCmd = getExecutableFactory().newCreator().createIdentityExecutable(); - execute(identityCmd); + execute(identityCmd, null); this.mIdentity = identityCmd.getResult(); // Identity command is required for root console detection, // but Groups command is not used for now. Also, this command is causing @@ -297,7 +298,7 @@ public abstract class ShellConsole extends Console implements Program.ProgramLis //Try with groups GroupsExecutable groupsCmd = getExecutableFactory().newCreator().createGroupsExecutable(); - execute(groupsCmd); + execute(groupsCmd, null); this.mIdentity.setGroups(groupsCmd.getResult()); } } catch (Exception ex) { @@ -372,10 +373,10 @@ public abstract class ShellConsole extends Console implements Program.ProgramLis * {@inheritDoc} */ @Override - public final synchronized void execute(final Executable executable) - throws ConsoleAllocException, InsufficientPermissionsException, - CommandNotFoundException, NoSuchFileOrDirectory, - OperationTimeoutException, ExecutionException, ReadOnlyFilesystemException { + public synchronized void execute(Executable executable, Context ctx) + throws ConsoleAllocException, InsufficientPermissionsException, NoSuchFileOrDirectory, + OperationTimeoutException, ExecutionException, CommandNotFoundException, + ReadOnlyFilesystemException { execute(executable, false); } @@ -1192,7 +1193,7 @@ public abstract class ShellConsole extends Console implements Program.ProgramLis return false; } - if (this.mActiveCommand.getCommand() != null) { + if (this.mActiveCommand != null && this.mActiveCommand.getCommand() != null) { try { boolean isCancellable = true; if (this.mActiveCommand instanceof AsyncResultProgram) { |