From bc2b012e8c050162d1883d4411b4e18c412eaeff Mon Sep 17 00:00:00 2001 From: x0b Date: Fri, 4 Oct 2019 17:22:43 +0200 Subject: [PATCH] Enable auth for [safdav] A security review revealed that the safdav component is not guarded appropriately against unauthorized access. This commit enables basic authentication that is configured automatically, with the auth credentials being available via SafAccessProvider .getUsername() / .getPassword(). As a result, thumbnails are loaded directly from the ContentResolver by Glide. Signed-off-by: x0b --- .../FileExplorerRecyclerViewAdapter.java | 6 ++- .../Settings/FileAccessSettingsFragment.java | 16 +++++-- .../github/x0b/safdav/SafAccessProvider.java | 46 +++++++++++++++++-- .../io/github/x0b/safdav/SafDAVServer.java | 33 +++++++++++++ .../io/github/x0b/safdav/SafDirectServer.java | 17 ++++++- .../github/x0b/safdav/saf/ProviderPaths.java | 2 +- 6 files changed, 107 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/RecyclerViewAdapters/FileExplorerRecyclerViewAdapter.java b/app/src/main/java/ca/pkay/rcloneexplorer/RecyclerViewAdapters/FileExplorerRecyclerViewAdapter.java index ccb7231d..9c1fda38 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/RecyclerViewAdapters/FileExplorerRecyclerViewAdapter.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/RecyclerViewAdapters/FileExplorerRecyclerViewAdapter.java @@ -2,6 +2,7 @@ import android.content.Context; import android.content.res.Resources; +import android.net.Uri; import android.text.TextUtils; import android.util.TypedValue; import android.view.LayoutInflater; @@ -17,7 +18,7 @@ import ca.pkay.rcloneexplorer.R; import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; -import io.github.x0b.safdav.file.SafConstants; +import io.github.x0b.safdav.SafAccessProvider; import java.util.ArrayList; import java.util.List; @@ -105,9 +106,10 @@ public void onBindViewHolder(@NonNull final ViewHolder holder, int position) { .centerCrop() .placeholder(R.drawable.ic_file); if(localLoad) { + Uri contentUri = SafAccessProvider.getDirectServer(context).getDocumentUri('/'+item.getPath()); Glide .with(context) - .load(SafConstants.SAF_REMOTE_URL + item.getPath()) + .load(contentUri) .apply(glideOption) .thumbnail(0.1f) .into(holder.fileIcon); diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Settings/FileAccessSettingsFragment.java b/app/src/main/java/ca/pkay/rcloneexplorer/Settings/FileAccessSettingsFragment.java index ee705600..1687fef2 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Settings/FileAccessSettingsFragment.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Settings/FileAccessSettingsFragment.java @@ -9,10 +9,6 @@ import android.net.Uri; import android.os.Bundle; import android.preference.PreferenceManager; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -21,10 +17,14 @@ import android.widget.Switch; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.RecyclerView; import ca.pkay.rcloneexplorer.R; import ca.pkay.rcloneexplorer.Rclone; -import ca.pkay.rcloneexplorer.VirtualContentProvider; import es.dmoral.toasty.Toasty; +import io.github.x0b.safdav.SafAccessProvider; import io.github.x0b.safdav.file.SafConstants; import java.util.ArrayList; @@ -138,11 +138,17 @@ private void setSafEnabled(boolean isChecked) { } private void createSafRemote() { + String user = SafAccessProvider.getUser(getContext()); + String pass = SafAccessProvider.getPassword(getContext()); ArrayList options = new ArrayList<>(); options.add(SafConstants.SAF_REMOTE_NAME); options.add("webdav"); options.add("url"); options.add(SafConstants.SAF_REMOTE_URL); + options.add("user"); + options.add(user); + options.add("pass"); + options.add(pass); Process process = rclone.configCreate(options); try { diff --git a/safdav/src/main/java/io/github/x0b/safdav/SafAccessProvider.java b/safdav/src/main/java/io/github/x0b/safdav/SafAccessProvider.java index 19cd9d85..d75520db 100644 --- a/safdav/src/main/java/io/github/x0b/safdav/SafAccessProvider.java +++ b/safdav/src/main/java/io/github/x0b/safdav/SafAccessProvider.java @@ -1,22 +1,31 @@ package io.github.x0b.safdav; +import android.annotation.SuppressLint; import android.content.Context; +import android.content.SharedPreferences; +import android.util.Base64; +import io.github.x0b.safdav.file.SafConstants; import java.io.IOException; - -import io.github.x0b.safdav.file.SafConstants; +import java.security.SecureRandom; /** * Saf Emulation Server */ public class SafAccessProvider { + private static final String PREF_KEY_SAF_USER = "io.github.x0b.safdav.safDavUser"; + private static final String PREF_KEY_SAF_PASS = "io.github.x0b.safdav.safDavPass"; + private static SafDAVServer davServer; private static SafDirectServer directServer; + private static String user; + private static String password; public static SafDAVServer getServer(Context context){ - if(null == davServer){ - davServer = new SafDAVServer(SafConstants.SAF_REMOTE_PORT, context); + if(null == davServer) { + initAuth(context); + davServer = new SafDAVServer(SafConstants.SAF_REMOTE_PORT, user, password, context); try { davServer.start(); } catch (IOException e) { @@ -26,6 +35,26 @@ public static SafDAVServer getServer(Context context){ return davServer; } + @SuppressLint("ApplySharedPref") + private static void initAuth(Context context){ + SharedPreferences preferences = context.getSharedPreferences( + context.getPackageName() + "_preferences", Context.MODE_PRIVATE); + if(preferences.contains(PREF_KEY_SAF_PASS) && preferences.contains((PREF_KEY_SAF_USER))){ + password = preferences.getString(PREF_KEY_SAF_PASS, ""); + user = preferences.getString(PREF_KEY_SAF_USER, "dav"); + } else { + SecureRandom random = new SecureRandom(); + byte[] values = new byte[16]; + random.nextBytes(values); + password = Base64.encodeToString(values, Base64.NO_WRAP); + user = "dav"; + preferences.edit() + .putString(PREF_KEY_SAF_PASS, password) + .putString(PREF_KEY_SAF_USER, user) + .commit(); + } + } + public static SafDirectServer getDirectServer(Context context) { if(null == directServer) { directServer = new SafDirectServer(context); @@ -33,4 +62,13 @@ public static SafDirectServer getDirectServer(Context context) { return directServer; } + public static String getUser(Context context) { + initAuth(context); + return user; + } + + public static String getPassword(Context context) { + initAuth(context); + return password; + } } diff --git a/safdav/src/main/java/io/github/x0b/safdav/SafDAVServer.java b/safdav/src/main/java/io/github/x0b/safdav/SafDAVServer.java index 9b8565e9..e5824977 100644 --- a/safdav/src/main/java/io/github/x0b/safdav/SafDAVServer.java +++ b/safdav/src/main/java/io/github/x0b/safdav/SafDAVServer.java @@ -2,6 +2,7 @@ import android.content.Context; import android.net.Uri; +import android.util.Base64; import android.util.Log; import fi.iki.elonen.NanoHTTPD; import io.github.x0b.safdav.file.FileAccessError; @@ -16,6 +17,7 @@ import io.github.x0b.safdav.xml.XmlResponseSerialization; import java.io.InputStream; +import java.nio.charset.StandardCharsets; public class SafDAVServer extends NanoHTTPD { @@ -24,12 +26,14 @@ public class SafDAVServer extends NanoHTTPD { private final ItemAccess itemAccess; private final XmlResponseSerialization respSerializer; private ProviderPaths paths; + private String requiredAuthHeader; /** * SafDAV binds to localhost. You must provide your own port availability checking mechanism * Note that authentication is currently not supported. * @param port the device port to listen on */ + @Deprecated public SafDAVServer(int port, Context context) { super(hostname, port); itemAccess = new SafFileAccess(context); @@ -37,8 +41,29 @@ public SafDAVServer(int port, Context context) { respSerializer = new XmlResponseSerialization(); } + /** + * SafDAV binds to localhost. You must provide your own port availability checking mechanism + * Note that authentication is currently not supported. + * @param port the device port to listen on + * @param username username + * @param password password + * @param context context for file access + */ + public SafDAVServer(int port, String username, String password, Context context) { + super(hostname, port); + itemAccess = new SafFileAccess(context); + this.paths = new ProviderPaths(context); + respSerializer = new XmlResponseSerialization(); + this.requiredAuthHeader = "Basic " + Base64.encodeToString( + (username + ':' + password).getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP) ; + } + @Override public Response serve(IHTTPSession session) { + if(!checkAuthorization(session)){ + Log.e(TAG, "serve: request not (correctly) authenticated"); + return newFixedLengthResponse(Response.Status.UNAUTHORIZED, null, null); + } switch (session.getMethod()) { // reading methods case GET: @@ -64,6 +89,14 @@ public Response serve(IHTTPSession session) { } } + private boolean checkAuthorization(IHTTPSession session) { + if(null == requiredAuthHeader) { + return true; + } + String suppliedAuthHeader = session.getHeaders().get("authorization"); + return requiredAuthHeader.equals(suppliedAuthHeader); + } + private Response badRequest(String signature) { Log.d(TAG, signature + ": 400 Bad Request"); return newFixedLengthResponse(Response.Status.BAD_REQUEST, "text/html", "

BAD REQEST

"); diff --git a/safdav/src/main/java/io/github/x0b/safdav/SafDirectServer.java b/safdav/src/main/java/io/github/x0b/safdav/SafDirectServer.java index 865ba67a..ee56bdcd 100644 --- a/safdav/src/main/java/io/github/x0b/safdav/SafDirectServer.java +++ b/safdav/src/main/java/io/github/x0b/safdav/SafDirectServer.java @@ -2,9 +2,9 @@ import android.content.Context; import android.net.Uri; -import io.github.x0b.safdav.saf.DocumentsContractAccess; import io.github.x0b.safdav.file.ItemAccess; import io.github.x0b.safdav.file.SafItem; +import io.github.x0b.safdav.saf.DocumentsContractAccess; import io.github.x0b.safdav.saf.ProviderPaths; import java.util.List; @@ -65,4 +65,19 @@ public void delete(String path) { Uri itemUri = providerPaths.getUriByMappedPath(path); itemAccess.deleteItem(itemUri); } + + /** + * Get a content uri from a SafDAV server uri + * @param safDavUri the uri to decode - include any non-host path + * @return an accessible content uri + * @throws io.github.x0b.safdav.file.ItemNotFoundException if the uri is not accessible + */ + public Uri getContentUri(String safDavUri) { + return providerPaths.getUriByMappedPath(safDavUri); + } + + public Uri getDocumentUri(String safDavUri) { + Uri itemUri = providerPaths.getUriByMappedPath(safDavUri); + return itemAccess.readMeta(itemUri).getUri(); + } } diff --git a/safdav/src/main/java/io/github/x0b/safdav/saf/ProviderPaths.java b/safdav/src/main/java/io/github/x0b/safdav/saf/ProviderPaths.java index bb38f9cd..7a0fcbff 100644 --- a/safdav/src/main/java/io/github/x0b/safdav/saf/ProviderPaths.java +++ b/safdav/src/main/java/io/github/x0b/safdav/saf/ProviderPaths.java @@ -43,7 +43,7 @@ public SafItem getSafRootDirectory() { */ public Uri getUriByMappedPath(String requestUri) { if(null == requestUri || '/' != requestUri.charAt(0)){ - throw new IllegalArgumentException("You must request an actual path permission"); + throw new IllegalArgumentException("You must request an actual path permission, not " + requestUri); } Log.v(TAG, "Rewriting URI: " + requestUri);