安卓应用安全指南 4.6.3 处理文件 高级话题
原书:Android Application Secure Design/Secure Coding Guidebook 译者:飞龙 协议:CC BY-NC-SA 4.0
4.6.3.1 通过文件描述符的文件共享
有一种方法可以通过文件描述符共享文件,而不是让其他应用访问公共文件。 此方法可用在内容供应器和服务中。 对方的应用可以通过文件描述符读取/写入文件,这些文件描述符通过在内容供应器或服务中,打开私人文件来获得。
其他应用直接访问文件的共享方式,与文件描述符的共享方式的比较如下表 4.6-2。 优点是访问权限的变化,以及允许访问的应用范围。 特别是从安全角度来看,这是一个很大的优点,可以详细控制允许访问的应用。
表 4.6-2 应用内文件共享方式的比较
文件共享方式 | 验证或者访问权限设置 | 允许访问的应用范围 |
---|---|---|
允许其他应用直接访问的文件共享 | 读、写、读写 | 给予所有应用同等访问权限 |
通过文件描述符的文件共享 | 读、写、仅添加、读写、读 添加 | 可以控制是否将权限授予应用,它们尝试独立和暂时访问内容供应器和服务。 |
在上述两种文件共享方法中,这是很常见的,因为向其他应用提供文件写入权限时,文件内容的完整性很难得到保证。 当多个应用并行写入时,可能会破坏文件内容的数据结构,导致应用无法正常工作。 因此,在与其他应用共享文件时,只允许只读权限。
以下是通过内容供应器的文件共享的实现示例,及其示例代码。
要点:
1) 源应用是内部应用,因此可以保存敏感信息。
2) 即使是由内部的内容供应器产生的结果,也要验证结果数据的安全性。
InhouseProvider.java
代码语言:javascript复制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
代码语言:javascript复制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");
}
}
4.6.3.2 为目录设置访问权限
以上所解释的安全考虑,重点在于文件。 还需要考虑作为文件容器的目录的安全性。 以下说明了目录的访问权限设置的安全性考虑。
在 Android 中,有一些方法可以在应用目录中获取/创建子目录。 主要如表 4.6-3。
表 4.6-3 在应用目录中获取/创建子目录的方法
规定其它应用的访问权限 | 删除文件 |
---|---|
Context#getFilesDir() | 不可能(只有执行权限) |
Context#getCacheDir() | 不可能(只有执行权限) |
`Context#getDir(String name, |
int MODE)| 可以对
MODE设置如下:
MODE_PRIVATEMODE_WORLD_READABLEMODE_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 应用应考虑文件范围而设计(必需)”。
4.6.3.3 共享首选项和数据库文件的访问权限设置
共享首选项和数据库也由文件组成。 对于访问权限设置,对文件解释的内容也会在这里解释。 因此,共享首选项和数据库都应该创建为私有文件,与文件相同,内容共享应该由 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 string
Editor editor = preference.edit();
editor.putString("prep_key", "prep_value");// key:"prep_key", value:"prep_value"
editor.commit();
对于数据库,请参考“4.5 使用 SQLite”。
4.6.3.4 Android 4.4(API 级别 19)及更高版本中,外部存储访问的规范更改
自 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
代码语言:javascript复制<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.jssec.android.file.externaluser" >
<!-- In Android 4.0.3 (API Level 14) and later, the permission for reading external storages
has been defined and the application should decalre that it requires the permission.
In fact in Android 4.4 (API Level 19) and later, that must be declared to read other directories
than the package specific directories. -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:allowBackup="false" >
<activity
android:name=".ExternalUserActivity"
android:label="@string/app_name"
android:exported="true" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
4.6.3.5 Android 7.0(API Level 24)中的规范已修改,以便访问外部存储介质上的特定目录
在运行 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
中的示例代码。
- 授予访问外部存储的权限时,应用可以访问预期目标以外的目录。
- 使用存储器访问框架来要求用户选择可访问的目录,会导致繁琐的过程,用户必须在每次访问时配置一个选择器。 另外,当访问外部存储器的根目录时,整个存储器变成可访问的。