本文共 11989 字,大约阅读时间需要 39 分钟。
原书:
译者:
协议:
有一种方法可以通过文件描述符共享文件,而不是让其他应用访问公共文件。 此方法可用在内容供应器和服务中。 对方的应用可以通过文件描述符读取/写入文件,这些文件描述符通过在内容供应器或服务中,打开私人文件来获得。
其他应用直接访问文件的共享方式,与文件描述符的共享方式的比较如下表 4.6-2。 优点是访问权限的变化,以及允许访问的应用范围。 特别是从安全角度来看,这是一个很大的优点,可以详细控制允许访问的应用。
表 4.6-2 应用内文件共享方式的比较
文件共享方式 | 验证或者访问权限设置 | 允许访问的应用范围 |
---|---|---|
允许其他应用直接访问的文件共享 | 读、写、读写 | 给予所有应用同等访问权限 |
通过文件描述符的文件共享 | 读、写、仅添加、读写、读+添加 | 可以控制是否将权限授予应用,它们尝试独立和暂时访问内容供应器和服务。 |
在上述两种文件共享方法中,这是很常见的,因为向其他应用提供文件写入权限时,文件内容的完整性很难得到保证。 当多个应用并行写入时,可能会破坏文件内容的数据结构,导致应用无法正常工作。 因此,在与其他应用共享文件时,只允许只读权限。
以下是通过内容供应器的文件共享的实现示例,及其示例代码。
要点:
1) 源应用是内部应用,因此可以保存敏感信息。
2) 即使是由内部的内容供应器产生的结果,也要验证结果数据的安全性。
InhouseProvider.java
package org.jssec.android.file.inhouseprovider;import java.io.File;import java.io.FileNotFoundException;import java.io.FileOutputStream;import java.io.IOException;import org.jssec.android.shared.SigPerm;import org.jssec.android.shared.Utils;import android.content.ContentProvider;import android.content.ContentValues;import android.content.Context;import android.database.Cursor;import android.net.Uri;import android.os.ParcelFileDescriptor;public class InhouseProvider extends ContentProvider { private static final String FILENAME = "sensitive.txt"; // In-house signature permission private static final String MY_PERMISSION = "org.jssec.android.file.inhouseprovider.MY_PERMISSION"; // In-house certificate hash value private static String sMyCertHash = null; private static String myCertHash(Context context) { if (sMyCertHash == null) { if (Utils.isDebuggable(context)) { // Certificate hash value of debug.keystore "androiddebugkey" sMyCertHash = "0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255"; } else { // Certificate hash value of keystore "my company key" sMyCertHash = "D397D343 A5CBC10F 4EDDEB7C A10062DE 5690984F 1FB9E88B D7B3A7C2 42E142CA"; } } return sMyCertHash; } @Override public boolean onCreate() { File dir = getContext().getFilesDir(); FileOutputStream fos = null; try { fos = new FileOutputStream(new File(dir, FILENAME)); // *** POINT 1 *** The source application is In house application, so sensitive information can be saved. fos.write(new String("Sensitive information").getBytes()); } catch (IOException e) { android.util.Log.e("InhouseProvider", "failed to read file"); } finally { try { fos.close(); } catch (IOException e) { android.util.Log.e("InhouseProvider", "failed to close file"); } } return true; } @Override public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { // Verify that in-house-defined signature permission is defined by in-house application. if (!SigPerm.test(getContext(), MY_PERMISSION, myCertHash(getContext()))) { throw new SecurityException( "In-house-defined signature permission is not defined by in-house application."); } File dir = getContext().getFilesDir(); File file = new File(dir, FILENAME); // Always return read-only, since this is sample int modeBits = ParcelFileDescriptor.MODE_READ_ONLY; return ParcelFileDescriptor.open(file, modeBits); } @Override public String getType(Uri uri) { return ""; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { return null; } @Override public Uri insert(Uri uri, ContentValues values) { return null; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { return 0; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { return 0; }}
InhouseUserActivity.java
package org.jssec.android.file.inhouseprovideruser;import java.io.FileInputStream;import java.io.FileNotFoundException;import java.io.IOException;import org.jssec.android.shared.PkgCert;import org.jssec.android.shared.SigPerm;import org.jssec.android.shared.Utils;import android.app.Activity;import android.content.Context;import android.content.pm.PackageManager;import android.content.pm.ProviderInfo;import android.net.Uri;import android.os.Bundle;import android.os.ParcelFileDescriptor;import android.view.View;import android.widget.TextView;public class InhouseUserActivity extends Activity { // Content Provider information of destination (requested provider) private static final String AUTHORITY = "org.jssec.android.file.inhouseprovider"; // In-house signature permission private static final String MY_PERMISSION = "org.jssec.android.file.inhouseprovider.MY_PERMISSION"; // In-house certificate hash value private static String sMyCertHash = null; private static String myCertHash(Context context) { if (sMyCertHash == null) { if (Utils.isDebuggable(context)) { // Certificate hash value of debug.keystore "androiddebugkey" sMyCertHash = "0EFB7236 328348A9 89718BAD DF57F544 D5CCB4AE B9DB34BC 1E29DD26 F77C8255"; } else { // Certificate hash value of keystore "my company key" sMyCertHash = "D397D343 A5CBC10F 4EDDEB7C A10062DE 5690984F 1FB9E88B D7B3A7C2 42E142CA"; } } return sMyCertHash; } // Get package name of destination (requested) content provider. private static String providerPkgname(Context context, String authority) { String pkgname = null; PackageManager pm = context.getPackageManager(); ProviderInfo pi = pm.resolveContentProvider(authority, 0); if (pi != null) pkgname = pi.packageName; return pkgname; } public void onReadFileClick(View view) { logLine("[ReadFile]"); // Verify that in-house-defined signature permission is defined by in-house application. if (!SigPerm.test(this, MY_PERMISSION, myCertHash(this))) { logLine(" In-house-defined signature permission is not defined by in-house application."); return; } // Verify that the certificate of destination (requested) content provider application is in-house certificate. String pkgname = providerPkgname(this, AUTHORITY); if (!PkgCert.test(this, pkgname, myCertHash(this))) { logLine(" Destination (Requested) Content Provider is not in-house application."); return; } // Only the information which can be disclosed to in-house only content provider application, can be included in a request. ParcelFileDescriptor pfd = null; try { pfd = getContentResolver().openFileDescriptor( Uri.parse("content://" + AUTHORITY), "r"); } catch (FileNotFoundException e) { android.util.Log.e("InhouseUserActivity", "no file"); } if (pfd != null) { FileInputStream fis = new FileInputStream(pfd.getFileDescriptor()); if (fis != null) { try { byte[] buf = new byte[(int) fis.getChannel().size()]; fis.read(buf); // *** POINT 2 *** Handle received result data carefully and securely, // even though the data came from in-house applications. // Omitted, since this is a sample. Please refer to "3.2 Handling Input Data Carefully and Securely." logLine(new String(buf)); } catch (IOException e) { android.util.Log.e("InhouseUserActivity", "failed to read file"); } finally { try { fis.close(); } catch (IOException e) { android.util.Log.e("ExternalFileActivity", "failed to close file"); } } } try { pfd.close(); } catch (IOException e) { android.util.Log.e("ExternalFileActivity", "failed to close file descriptor"); } } else { logLine(" null file descriptor"); } } private TextView mLogView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); mLogView = (TextView) findViewById(R.id.logview); } private void logLine(String line) { mLogView.append(line); mLogView.append("¥n"); }}
以上所解释的安全考虑,重点在于文件。 还需要考虑作为文件容器的目录的安全性。 以下说明了目录的访问权限设置的安全性考虑。
在 Android 中,有一些方法可以在应用目录中获取/创建子目录。 主要如表 4.6-3。
表 4.6-3 在应用目录中获取/创建子目录的方法
规定其它应用的访问权限 | 删除文件 |
---|---|
Context#getFilesDir() | 不可能(只有执行权限) |
Context#getCacheDir() | 不可能(只有执行权限) |
`Context#getDir(String name, |
int MODE)| 可以对
MODE设置如下:
MODE_PRIVATEMODE_WORLD_READABLE
MODE_WORLD_WRITEABLE` | 设置=>应用=>选择目标应用=>清除数据 |
这里特别需要注意的是Context#getDir()
的访问权限设置。 正如文件创建中所说明的,从安全设计的角度来看,目录基本上也应该设置为私有的。 当信息共享取决于访问权限设置时,可能会产生意想不到的副作用,所以应采取其他方法用于信息共享。
MODE_WORLD_READABLE
这是一个标志,为所有应用提供目录的只读权限。 所以所有应用都可以获取目录中的文件列表,和单个文件属性信息。 由于秘密文件可能不会被放置在这些目录中,所以通常不能使用该标志 [15]。
MODE_WORLD_WRITEABLE
该标志位其他应用提供目录的写入权限。 所有应用都可以创建/移动/重命名/删除目录中的文件。 这些操作与文件本身的访问权限设置(读/写/执行)没有关系,所以需要注意的是,仅仅使用目录的写入权限就能执行操作。 此标志允许其他应用随意删除或替换文件,因此一般不能使用。
[15]
MODE_WORLD_READABLE
和MODE_WORLD_WRITEABLE
在 API 17 和更高版本以及 API 24 和更高版本中弃用,使用它们将触发安全异常。
对于表 4.6-3 “用户删除”,请参考“4.6.2.4 应用应考虑文件范围而设计(必需)”。
共享首选项和数据库也由文件组成。 对于访问权限设置,对文件解释的内容也会在这里解释。 因此,共享首选项和数据库都应该创建为私有文件,与文件相同,内容共享应该由 Android 的应用间联动系统来实现。
下面将展示共享首选项的使用示例。 通过MODE_PRIVATE
,共享首选项被设置为私有文件。
import android.content.SharedPreferences;import android.content.SharedPreferences.Editor;// Ommision of a passage// Get Shared Preference . (If there's no Shared Preference, it's to be created.)// Point:Basically, specify MODE_PRIVATE mode.SharedPreferences preference = getSharedPreferences( PREFERENCE_FILE_NAME, MODE_PRIVATE);// Example of writing preference which value is charcter stringEditor editor = preference.edit();editor.putString("prep_key", "prep_value");// key:"prep_key", value:"prep_value"editor.commit();
对于数据库,请参考“4.5 使用 SQLite”。
自 Android 4.4(API Level 19)以来,外部存储访问的规范已更改为以下内容。
(1)如果应用需要读/写其外部存储器上的特定目录,则不需要使用<uses-permission>
声明WRITE_EXTERNAL_STORAGE
/READ_EXTERNAL_STORAGE
权限。(已更改)
(2)如果应用需要读取除外部存储器上特定目录以外的目录中的文件,则需要使用<uses-permission>
声明READ_EXTERNAL_STORAGE
权限。 (已更改)
(3)如果应用需要写入主外部存储器上的特定目录以外的目录中的文件,则需要使用<uses-permission>
声明WRITE_EXTERNAL_STORAGE
权限。
(4)应用无法写入次要外部存储器上的特定目录以外的目录中的文件。
在该规范中,根据 Android OS 的版本确定是否需要权限请求。 因此,如果应用支持包括 Android 4.3 和 4.4 在内的版本,则可能会导致应用需要用户不必要的许可。 因此,建议使用对应(1)的应用,如下所示使用<uses-permission>
的maxSdkVersion
属性。
AndroidManifest.xml
在运行 Android 7.0(API Level 24)或更高版本的设备上,引入了一种称为作用域目录访问 API的新 API。 作用域目录访问允许应用在未经许可的情况下,访问外部存储器上的特定目录。 在作用域目录访问中,将Environment
类中定义的目录作为参数传递给StorageVolume#createAccessIntent
方法,来创建一个意图。 通过startActivityForResult
发送此意图,可以启动一个对话框,在终端屏幕上请求访问权限,并且 - 如果用户授予权限 - 每个存储卷上的指定目录都可以访问。
表 4.6-4 可以通过作用域目录访问来访问的目录
DIRECTORY_MUSIC | 通用音乐文件的标准位置 |
---|---|
DIRECTORY_PODCASTS | 播客的标准目录 |
DIRECTORY_RINGTONES | ringtone 的标准目录 |
DIRECTORY_ALARMS | 闹铃的标准目录 |
DIRECTORY_NOTIFICATIONS | 提醒的标准目录 |
DIRECTORY_PICTURES | 图片的标准目录 |
DIRECTORY_MOVIES | 电影的标准目录 |
DIRECTORY_DOWNLOADS | 用户下载的文件的标准目录 |
DIRECTORY_DCIM | 相机产生的图片/视频文件的标准目录 |
DIRECTORY_DOCUMENTS | 用户创建的文档的标准目录 |
如果应用要访问的位置位于上述目录之一,并且该应用正在 Android 7.0 或更高版本的设备上运行,则建议使用作用域目录访问,原因如下。 对于必须继续支持 Android 7.0 以下的设备的应用,请参阅“4.6.3.4 Android 4.4(API级别19)及更高版本中的外部存储访问的规范更改”中,列出的AndroidManifest
中的示例代码。
转载地址:http://ylsfa.baihongyu.com/