├── .gitignore ├── 1.md ├── 2.md ├── 3.md ├── 4.1.1.1.md ├── 4.1.1.2.md ├── 4.1.1.3.md ├── 4.1.1.4.md ├── 4.1.1.md ├── 4.1.2.md ├── 4.1.3.md ├── 4.1.md ├── 4.10.md ├── 4.2.1.1.md ├── 4.2.1.2.md ├── 4.2.1.3.md ├── 4.2.1.md ├── 4.2.2.md ├── 4.2.3.md ├── 4.2.md ├── 4.3.1.1.md ├── 4.3.1.2.md ├── 4.3.1.3.md ├── 4.3.1.4.md ├── 4.3.1.5.md ├── 4.3.1.md ├── 4.3.2.md ├── 4.3.md ├── 4.4.1.1.md ├── 4.4.1.2.md ├── 4.4.1.3.md ├── 4.4.1.4.md ├── 4.4.1.md ├── 4.4.2.md ├── 4.4.3.md ├── 4.4.md ├── 4.5.1.md ├── 4.5.2.md ├── 4.5.3.md ├── 4.5.md ├── 4.6.1.1.md ├── 4.6.1.2.md ├── 4.6.1.3.md ├── 4.6.1.4.md ├── 4.6.1.md ├── 4.6.2.md ├── 4.6.3.md ├── 4.6.md ├── 4.7.md ├── 4.8.md ├── 4.9.md ├── 4.md ├── 5.1.md ├── 5.2.1.md ├── 5.2.2.md ├── 5.2.3.md ├── 5.2.md ├── 5.3.1.md ├── 5.3.2.md ├── 5.3.3.md ├── 5.3.md ├── 5.4.1.md ├── 5.4.2.md ├── 5.4.3.md ├── 5.4.md ├── 5.5.1.md ├── 5.5.2.md ├── 5.5.3.md ├── 5.5.md ├── 5.6.1.md ├── 5.6.2.md ├── 5.6.3.md ├── 5.6.md ├── 5.7.md ├── 5.md ├── 6.md ├── README.md ├── SUMMARY.md ├── cover.jpg ├── img ├── 4-1-1.jpg ├── 4-1-2.jpg ├── 4-1-4.jpg ├── 4-1-5.jpg ├── 4-10-1.jpg ├── 4-10-2.jpg ├── 4-10-3.jpg ├── 4-2-1.jpg ├── 4-2-4.jpg ├── 4-2-5.jpg ├── 4-3-1.jpg ├── 4-4-1.jpg ├── 4-4-4.jpg ├── 4-4-5.jpg ├── 4-4-6.jpg ├── 4-5-1.jpg ├── 4-8-1.jpg ├── 4-8-2.jpg ├── 4-8-3.jpg ├── 4-9-1.jpg ├── 4-9-2.jpg ├── 5-1-1.jpg ├── 5-1-2.jpg ├── 5-1-3.jpg ├── 5-1-4.jpg ├── 5-2-1.jpg ├── 5-2-10.jpg ├── 5-2-2.jpg ├── 5-2-5.jpg ├── 5-2-6.jpg ├── 5-2-7.jpg ├── 5-2-8.jpg ├── 5-2-9.jpg ├── 5-3-1.jpg ├── 5-3-2.jpg ├── 5-3-3.jpg ├── 5-4-1.jpg ├── 5-4-2.jpg ├── 5-4-3.jpg ├── 5-4-4.jpg ├── 5-5-1.jpg ├── 5-5-2.jpg ├── 5-5-3.jpg ├── 5-5-4.jpg ├── 5-5-5.jpg ├── 5-5-6.jpg ├── 5-5-7.jpg ├── 5-6-1.jpg ├── 5-6-2.jpg ├── 5-6-3.jpg ├── 5-6-4.jpg ├── 6-1-1.jpg └── qr_alipay.png └── styles ├── ebook.css └── runoob.css /.gitignore: -------------------------------------------------------------------------------- 1 | _book 2 | Thumbs.db 3 | -------------------------------------------------------------------------------- /1.md: -------------------------------------------------------------------------------- 1 | # 一、简介 2 | 3 | (略) -------------------------------------------------------------------------------- /2.md: -------------------------------------------------------------------------------- 1 | # 二、本书结构 2 | 3 | (略) -------------------------------------------------------------------------------- /3.md: -------------------------------------------------------------------------------- 1 | # 三、安全设计和编程的基础知识 2 | 3 | (略) -------------------------------------------------------------------------------- /4.1.1.1.md: -------------------------------------------------------------------------------- 1 | #### 4.1.1.1 创建/使用私有活动 2 | 3 | 私有活动是其他应用程序无法启动的活动,因此它是最安全的活动。 4 | 5 | 当使用仅在应用程序中使用的活动(私有活动)时,只要你对类使用显示意图,那么你不必担心将它意外发送到任何其他应用程序。 但是,第三方应用程序可能会读取用于启动活动的意图。 因此,如果你将敏感信息放入用于启动活动的意图中,有必要采取对策,来确保它不会被恶意第三方读取。 6 | 7 | 下面展示了如何创建私有活动的示例代码。 8 | 9 | 要点(创建活动): 10 | 11 | 1) 不要指定`taskAffinity`。 12 | 13 | 2) 不要指定`launchMode`。 14 | 15 | 3) 将导出属性明确设置为`false`。 16 | 17 | 4) 仔细和安全地处理收到的意图,即使意图从相同的应用发送。 18 | 19 | 5) 敏感信息可以发送,因为它发送和接收所有同一应用中的信息。 20 | 21 | AndroidManifest.xml 22 | 23 | ```xml 24 | 25 | 27 | 28 | 32 | 33 | 34 | 35 | 36 | 37 | 41 | 42 | 43 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | ``` 55 | 56 | PrivateActivity.java 57 | 58 | ```java 59 | package org.jssec.android.activity.privateactivity; 60 | 61 | import android.app.Activity; 62 | import android.content.Intent; 63 | import android.os.Bundle; 64 | import android.view.View; 65 | import android.widget.Toast; 66 | 67 | public class PrivateActivity extends Activity { 68 | 69 | @Override 70 | public void onCreate(Bundle savedInstanceState) { 71 | super.onCreate(savedInstanceState); 72 | setContentView(R.layout.private_activity); 73 | // *** POINT 4 *** Handle the received Intent carefully and securely, even though the Intent was sent from the same application. 74 | // Omitted, since this is a sample. Please refer to "3.2 Handling Input Data Carefully and Securely." 75 | String param = getIntent().getStringExtra("PARAM"); 76 | Toast.makeText(this, String.format("Received param: ¥"%s¥"", param), Toast.LENGTH_LONG).show(); 77 | } 78 | 79 | public void onReturnResultClick(View view) { 80 | // *** POINT 5 *** Sensitive information can be sent since it is sending and receiving all within the same application. 81 | Intent intent = new Intent(); 82 | intent.putExtra("RESULT", "Sensitive Info"); 83 | setResult(RESULT_OK, intent); 84 | finish(); 85 | } 86 | } 87 | ``` 88 | 89 | 下面展示如何使用私有活动的示例代码。 90 | 91 | 要点(使用活动); 92 | 93 | 6) 不要为意图设置`FLAG_ACTIVITY_NEW_TASK`标志来启动活动。 94 | 95 | 7) 使用显式意图,以及用于调用相同应用中的活动的特定的类。 96 | 97 | 8) 由于目标活动位于同一个应用中,因此只能通过`putExtra()`发送敏感信息 [1]。 98 | 99 | > 警告:如果不遵守第 1, 2 和 6 点,第三方可能会读到意图。 更多详细信息,请参阅第 4.1.2.2 和 4.1.2.3 节。 100 | 101 | 9) 即使数据来自同一应用中的活动,也要小心并安全地处理收到的结果数据。 102 | 103 | PrivateUserActivity.java 104 | 105 | ```java 106 | package org.jssec.android.activity.privateactivity; 107 | 108 | import android.app.Activity; 109 | import android.content.Intent; 110 | import android.os.Bundle; 111 | import android.view.View; 112 | import android.widget.Toast; 113 | 114 | public class PrivateUserActivity extends Activity { 115 | 116 | private static final int REQUEST_CODE = 1; 117 | 118 | @Override 119 | public void onCreate(Bundle savedInstanceState) { 120 | super.onCreate(savedInstanceState); 121 | setContentView(R.layout.user_activity); 122 | } 123 | 124 | public void onUseActivityClick(View view) { 125 | // *** POINT 6 *** Do not set the FLAG_ACTIVITY_NEW_TASK flag for intents to start an activity. 126 | // *** POINT 7 *** Use the explicit Intents with the class specified to call an activity in the same application. 127 | Intent intent = new Intent(this, PrivateActivity.class); 128 | // *** POINT 8 *** Sensitive information can be sent only by putExtra() since the destination activity is in the same application. 129 | intent.putExtra("PARAM", "Sensitive Info"); 130 | startActivityForResult(intent, REQUEST_CODE); 131 | } 132 | 133 | @Override 134 | public void onActivityResult(int requestCode, int resultCode, Intent data) { 135 | super.onActivityResult(requestCode, resultCode, data); 136 | if (resultCode != RESULT_OK) return; 137 | switch (requestCode) { 138 | case REQUEST_CODE: 139 | String result = data.getStringExtra("RESULT"); 140 | // *** POINT 9 *** Handle the received data carefully and securely, 141 | // even though the data comes from an activity within the same application. 142 | // Omitted, since this is a sample. Please refer to "3.2 Handling Input Data Carefully and Securely." 143 | Toast.makeText(this, String.format("Received result: ¥"%s¥"", result), Toast.LENGTH_LONG).show(); 144 | break; 145 | } 146 | } 147 | } 148 | ``` 149 | -------------------------------------------------------------------------------- /4.1.1.2.md: -------------------------------------------------------------------------------- 1 | #### 4.1.1.2 创建/使用公共活动 2 | 3 | 公共活动是应该由大量未指定的应用程序使用的活动。 有必要注意的是,公共活动可能收到恶意软件发送的意图。 另外,使用公共活动时,有必要注意恶意软件也可以接收或阅读发送给他们的意图。 4 | 5 | 要点(创建活动): 6 | 7 | 1) 将导出属性显式设置为`true`。 8 | 9 | 2) 小心并安全地处理接收到的意图。 10 | 11 | 3) 返回结果时,请勿包含敏感信息。 12 | 13 | 下面展示了创建公共活动的示例代码。 14 | 15 | AndroidManifest.xml 16 | 17 | ```xml 18 | 19 | 21 | 22 | 26 | 27 | 28 | 29 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ``` 43 | 44 | PublicActivity.java 45 | 46 | ```java 47 | package org.jssec.android.activity.publicactivity; 48 | 49 | import android.app.Activity; 50 | import android.content.Intent; 51 | import android.os.Bundle; 52 | import android.view.View; 53 | import android.widget.Toast; 54 | 55 | public class PublicActivity extends Activity { 56 | 57 | @Override 58 | public void onCreate(Bundle savedInstanceState) { 59 | super.onCreate(savedInstanceState); 60 | setContentView(R.layout.main); 61 | // *** POINT 2 *** Handle the received intent carefully and securely. 62 | // Since this is a public activity, it is possible that the sending application may be malware. 63 | // Omitted, since this is a sample. Please refer to "3.2 Handling Input Data Carefully and Securely." 64 | String param = getIntent().getStringExtra("PARAM"); 65 | Toast.makeText(this, String.format("Received param: ¥"%s¥"", param), Toast.LENGTH_LONG).show(); 66 | } 67 | 68 | public void onReturnResultClick(View view) { 69 | // *** POINT 3 *** When returning a result, do not include sensitive information. 70 | // Since this is a public activity, it is possible that the receiving application may be malware. 71 | // If there is no problem if the data gets received by malware, then it can be returned as a result. 72 | Intent intent = new Intent(); 73 | intent.putExtra("RESULT", "Not Sensitive Info"); 74 | setResult(RESULT_OK, intent); 75 | finish(); 76 | } 77 | } 78 | ``` 79 | 80 | 接下来,这里是公共活动用户端的示例代码。 81 | 82 | 要点(使用活动): 83 | 84 | 4) 不要发送敏感信息。 85 | 86 | 5) 收到结果时,请仔细并安全地处理数据。 87 | 88 | PublicUserActivity.java 89 | 90 | ```java 91 | package org.jssec.android.activity.publicuser; 92 | import android.app.Activity; 93 | import android.content.ActivityNotFoundException; 94 | import android.content.Intent; 95 | import android.os.Bundle; 96 | import android.view.View; 97 | import android.widget.Toast; 98 | public class PublicUserActivity extends Activity { 99 | 100 | private static final int REQUEST_CODE = 1; 101 | 102 | @Override 103 | public void onCreate(Bundle savedInstanceState) { 104 | super.onCreate(savedInstanceState); 105 | setContentView(R.layout.main); 106 | } 107 | 108 | public void onUseActivityClick(View view) { 109 | try { 110 | // *** POINT 4 *** Do not send sensitive information. 111 | Intent intent = new Intent("org.jssec.android.activity.MY_ACTION"); 112 | intent.putExtra("PARAM", "Not Sensitive Info"); 113 | startActivityForResult(intent, REQUEST_CODE); 114 | } catch (ActivityNotFoundException e) { 115 | Toast.makeText(this, "Target activity not found.", Toast.LENGTH_LONG).show(); 116 | } 117 | } 118 | 119 | @Override 120 | public void onActivityResult(int requestCode, int resultCode, Intent data) { 121 | super.onActivityResult(requestCode, resultCode, data); 122 | // *** POINT 5 *** When receiving a result, handle the data carefully and securely. 123 | // Omitted, since this is a sample. Please refer to "3.2 Handling Input Data Carefully and Securely." 124 | if (resultCode != RESULT_OK) return; 125 | switch (requestCode) { 126 | case REQUEST_CODE: 127 | String result = data.getStringExtra("RESULT"); 128 | Toast.makeText(this, String.format("Received result: ¥"%s¥"", result), Toast.LENGTH_LONG).show(); 129 | break; 130 | } 131 | } 132 | } 133 | ``` -------------------------------------------------------------------------------- /4.1.1.md: -------------------------------------------------------------------------------- 1 | ### 4.1.1 示例代码 2 | 3 | 使用活动的风险和对策取决于活动的使用方式。 在本节中,我们根据活动的使用情况,对 4 种活动进行了分类。 你可以通过下面的图表来找出,你应该创建哪种类型的活动。 由于安全编程最佳实践根据活动的使用方式而有所不同,因此我们也将解释活动的实现。 4 | 5 | 表 4-1 活动类型的定义 6 | 7 | | 类型 | 定义 | 8 | | --- | --- | 9 | | 私有 | 不能由其他应用加载,所以是最安全的活动 | 10 | | 公共 | 应该由很多未指定的应用使用的活动 | 11 | | 伙伴 | 只能由可信的伙伴公司开发的应用使用的活动 | 12 | | 内部 | 只能由其他内部应用使用的活动 | 13 | 14 | ![](img/4-1-1.jpg) -------------------------------------------------------------------------------- /4.1.2.md: -------------------------------------------------------------------------------- 1 | ### 4.1.2 规则书 2 | 3 | 创建或向活动发送意图时,请务必遵循以下规则。 4 | 5 | #### 4.1.2.1 仅在应用内部使用的活动必须设置为私有(必需) 6 | 7 | 仅在单个应用中使用的活动,不需要能够从其他应用接收任何意图。 开发人员经常假设,应该是私有的活动不会受到攻击,但有必要将这些活动显式设置为私有,以阻止恶意内容被收到。 8 | 9 | AndroidManifest.xml 10 | 11 | ```xml 12 | 13 | 14 | 18 | ``` 19 | 20 | 21 | 22 | 意图过滤器不应该设置在仅用于单个应用的活动中。 由于意图过滤器的特性,以及工作原理,即使您打算向内部的私有活动发送意图,但如果通过意图过滤器发送,则可能会无意中启动另一个活动。 更多详细信息,请参阅高级主题“4.1.3.1 结合导出属性和意图过滤器设置(用于活动)”。 23 | 24 | AndroidManifest.xml(不推荐) 25 | 26 | ```xml 27 | 28 | 29 | 33 | 34 | 55 | 56 | 57 | 61 | 62 | ``` 63 | 64 | 任务和 Affinity 的更多信息,请参阅“Google Android 编程指南” [2],Google 开发者 API 指南“任务和返回栈” [3],“4.1.3.3 读取发送到活动的意图”和“4.1.3.4 根活动” 65 | 66 | > [2] Author Egawa, Fujii, Asano, Fujita, Yamada, Yamaoka, Sano, Takebata, “Google Android Programming 67 | Guide”, ASCII Media Works, July 2009 68 | 69 | > [3] http://developer.android.com/guide/components/tasks-and-back-stack.html 70 | 71 | #### 4.1.2.3 不要指定`launchMode`(必需) 72 | 73 | 活动的启动模式,用于控制启动活动时的设置,它用于创建新任务和活动实例。 默认情况下,它被设置为`"standard"`。 在`"standard"`设置中,新实例总是在启动活动时创建,任务遵循属于调用活动的任务,并且不可能创建新任务。 创建新任务时,其他应用可能会读取调用意图的内容,因此当敏感信息包含在意图中时,需要使用`"standard"`活动启动模式设置。 活动的启动模式可以在`AndroidManifest.xml`文件的`android:launchMode`属性中显式设置,但由于上面解释的原因,这不应该在活动的声明中设置,并且该值应该保留为默认的`"standard"`。 74 | 75 | AndroidManifest.xml 76 | 77 | ```xml 78 | 81 | 82 | 83 | 87 | 88 | ``` 89 | 90 | 请参阅“4.1.3.3 读取发送到活动的意图”和“4.1.3.4 根活动”。 91 | 92 | #### 4.1.2.4 不要为启动活动的意图设置`FLAG_ACTIVITY_NEW_TASK`标志(必需) 93 | 94 | 执行`startActivity()`或`startActivityForResult()`时,可以更改`Activity`的启动模式,并且在某些情况下可能会生成新任务。 因此有必要在执行期间不更改`Activity`的启动模式。 95 | 96 | 要更改`Activity`启动模式,使用`setFlags()`或`addFlags()`设置`Intent`标志,并将该`Intent`用作`startActivity()`或`startActivityForResult()`的参数。 `FLAG_ACTIVITY_NEW_TASK`是用于创建新任务的标志。 当设置`FLAG_ACTIVITY_NEW_TASK`时,如果被调用的`Activity`不存在于后台或前台,则会创建一个新任务。 `FLAG_ACTIVITY_MULTIPLE_TASK`标志可以与`FLAG_ACTIVITY_NEW_TASK`同时设置。 在这种情况下,总会创建一个新的任务。 新任务可以通过任一设置创建,因此不应使用处理敏感信息的意图来设置这些东西。 97 | 98 | ```java 99 | // *** POINT 6 *** Do not set the FLAG_ACTIVITY_NEW_TASK flag for the intent to start an activity. 100 | Intent intent = new Intent(this, PrivateActivity.class); 101 | intent.putExtra("PARAM", "Sensitive Info"); 102 | startActivityForResult(intent, REQUEST_CODE); 103 | ``` 104 | 105 | 另外,即使通过明确设置`FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS`标志创建了新任务,您也可能认为有一种方法可以防止读取`Intent`的内容。 但是,即使使用此方法,内容也可以由第三方读取,因此您应该避免使用`FLAG_ACTIVITY_NEW_TASK`。 106 | 107 | 请参阅“4.1.3.1 结合导出属性和意图过滤设置(针对活动)”,“4.1.3.3 读取发送到活动的意图”和“4.1.3.4 根活动”。 108 | 109 | #### 4.1.2.5 小心和安全地处理收到的意图 110 | 111 | 风险因`Activity`的类型而异,但在处理收到的`Intent`数据时,您应该做的第一件事是输入验证。 112 | 113 | 由于公共活动可以从不受信任的来源接收意图,它们可能会受到恶意软件的攻击。 另一方面,私有活动永远不会直接从其他应用收到任何意图,但目标应用中的公共活动可能会将恶意`Intent`转发给私有活动,因此您不应该认为私有活动不会收到任何恶意输入。 由于伙伴活动和内部活动也有恶意意图转发给他们的风险,因此有必要对这些意图进行输入验证。 114 | 115 | 请参阅“3.2 仔细和安全地处理输入数据” 116 | 117 | #### 4.1.2.6 在验证签名权限由内部应用定义之后,使用内部定义的签名权限(必需) 118 | 119 | 确保在创建活动时,通过定义内部签名权限来保护您的内部活动。 由于在`AndroidManifest.xml`文件中定义权限或声明权限请求不能提供足够的安全性,请务必参考“5.2.1.2 如何使用内部定义的签名权限,在内部应用之间进行通信”。 120 | 121 | #### 4.1.2.7 返回结果时,请注意目标应用产生的可能的信息泄露(必需) 122 | 123 | 当您使用`setResult()`返回数据时,目标应用的可靠性将取决于`Activity`类型。 当公共活动用于返回数据时,目标可能会成为恶意软件,在这种情况下,可能会以恶意方式使用该信息。 对于私有和内部活动,不需要过多担心返回的数据被恶意使用,因为它们被返回到您控制的应用。 伙伴活动中间有些东西。 124 | 125 | 如上所述,当从活动中返回数据时,您需要注意来自目标应用的信息泄漏。 126 | 127 | ```java 128 | public void onReturnResultClick(View view) { 129 | // *** POINT 6 *** Information that is granted to be disclosed to a partner application can be returned. 130 | Intent intent = new Intent(); 131 | intent.putExtra("RESULT", "Sensitive Info"); 132 | setResult(RESULT_OK, intent); 133 | finish(); 134 | } 135 | ``` 136 | 137 | #### 4.1.2.8 如果目标活动是预先确定的,则使用显式意图(必需) 138 | 139 | 当通过隐式意图使用`Activity`时,`Intent`发送到的`Activity`由 Android OS 确定。 如果意图被错误地发送到恶意软件,则可能发生信息泄漏。 另一方面,当通过显式意图使用`Activity`时,只有预期的`Activity`会收到`Intent`,所以这样更安全。 除非用户需要确定意图应该发送到哪个应用活动,否则应该使用显式意图并提前指定目标。 140 | 141 | ```java 142 | Intent intent = new Intent(this, PictureActivity.class); 143 | intent.putExtra("BARCODE", barcode); 144 | startActivity(intent); 145 | ``` 146 | 147 | 148 | ```java 149 | Intent intent = new Intent(); 150 | intent.setClassName( 151 | "org.jssec.android.activity.publicactivity", 152 | "org.jssec.android.activity.publicactivity.PublicActivity"); 153 | startActivity(intent); 154 | ``` 155 | 156 | 但是,即使通过显式意图使用其他应用的公共活动,目标活动也可能是恶意软件。 这是因为,即使通过软件包名称限制目标,恶意应用仍可能伪造与真实应用相同的软件包名称。 为了消除这种风险,有必要考虑使用伙伴或内部活动。 157 | 158 | 请参阅“4.1.3.1 组合导出属性和意图过滤器设置(对于活动)” 159 | 160 | 161 | #### 4.1.2.9 小心并安全地处理来自被请求活动的返回数据(必需) 162 | 163 | 根据您访问的活动类型,风险略有不同,但在处理作为返回值的收到的`Intent`数据,您始终需要对接收到的数据执行输入验证。 公共活动必须接受来自不受信任来源的返回意图,因此在访问公共活动时,返回的意图实际上可能是由恶意软件发送的。 人们往往错误地认为,私有活动返回的所有内容都是安全的,因为它们来源于同一个应用。 但是,由于从不可信来源收到的意图可能会间接转发,因此您不应盲目信任该意图的内容。 伙伴和内部活动在私有和公共活动中间有一定风险。 一定也要对这些活动输入验证。 更多信息,请参阅“3.2 仔细和安全地处理输入数据”。 164 | 165 | #### 4.1.2.10 如果与其他公司的应用链接,请验证目标活动(必需) 166 | 167 | 与其他公司的应用链接时,确保确定了白名单。 您可以通过在应用内保存公司的证书散列副本,并使用目标应用的证书散列来检查它。 这将防止恶意应用欺骗意图。 具体实现方法请参考示例代码“4.1.1.3 创建/使用伙伴活动”部分。 技术细节请参阅“4.1.3.2 验证请求应用”。 168 | 169 | #### 4.2.11 提供二手素材时,素材应受到同等保护(必需) 170 | 171 | 当受到权限保护的信息或功能素材被另一个应用提供时,您需要确保它具有访问素材所需的相同权限。 在 Android OS 权限安全模型中,只有已获得适当权限的应用才可以直接访问受保护的素材。 但是,存在一个漏洞,因为具有素材权限的应用可以充当代理,并允许非特权应用程序访问它。 基本上这与重新授权相同,因此它被称为“重新授权”问题。 请参阅“5.2.3.4 重新授权问题”。 172 | 173 | #### 4.2.12 敏感信息的发送应该尽可能限制(推荐) 174 | 175 | 您不应将敏感信息发送给不受信任的各方。 即使您正在连接特定的应用程序,仍有可能无意中将`Intent`发送给其他应用程序,或者恶意第三方可能会窃取您的意图。 请参阅“4.1.3.5 使用活动时的日志输出”。 176 | 177 | 将敏感信息发送到活动时,您需要考虑信息泄露的风险。 您必须假设,发送到公共活动的`Intent`中的所有数据都可以由恶意第三方获取。 此外,根据实现,向`伙伴或内部活动发送意图时,也存在各种信息泄漏的风险。 即使将数据发送到私有活动,也存在风险,意图中的数据可能通过`LogCat`泄漏。 意图附加部分中的信息不会输出到`LogCat`,因此最好在那里存储敏感信息。 178 | 179 | 但是,不首先发送敏感数据,是防止信息泄露的唯一完美解决方案,因此您应该尽可能限制发送的敏感信息的数量。 当有必要发送敏感信息时,最好的做法是只发送给受信任的活动,并确保信息不能通过`LogCat`泄露。 180 | 181 | 另外,敏感信息不应该发送到根活动。 根活动是创建任务时首先调用的活动。 例如,从启动器启动的活动始终是根活动。 182 | 183 | 根活动的更多详细信息,请参阅“4.1.3.3 发送到活动的意图”和“4.1.3.4 根活动”。 184 | -------------------------------------------------------------------------------- /4.1.md: -------------------------------------------------------------------------------- 1 | # 4.1 创建或使用活动 2 | -------------------------------------------------------------------------------- /4.10.md: -------------------------------------------------------------------------------- 1 | ## 4.10 使用通知 2 | 3 | Android 提供用于向最终用户发送消息的通知功能。 使用通知会使一个称为状态栏的区域出现在屏幕上,你可以在其中显示图标和消息。 4 | 5 | ![](img/4-10-1.jpg) 6 | 7 | 在 Android 5.0(API Level 21)中增强了通知的通信功能,即使在屏幕锁定时也可以通过通知显示消息,具体取决于用户和应用设置。 但是,不正确地使用通知,会导致私人信息(只应向最终用户自己显示)可能会被第三方看到。 出于这个原因,必须谨慎地注意隐私和安全性来实现此功能。 8 | 9 | 下表中总结了可见性选项的可能值和通知的相应行为。 10 | 11 | | 可见性的值 | 通知行为 | 12 | | --- | --- | 13 | | 公共 | 通知会显示在所有锁定屏幕上 | 14 | | 私有 | 通知显示在所有锁定的屏幕上;然而,在被密码保护的锁定屏幕上(安全锁),通知的标题和文本等字段是隐藏的(由公开可释放消息取代,私有信息是隐藏的) | 15 | | 秘密 | 通知不会显示在受密码或其他安全措施(安全锁)保护的锁定屏幕上。 (通知显示在不涉及安全锁的锁定屏幕上。) | 16 | 17 | 18 | ### 4.10.1 示例代码 19 | 20 | 当通知包含有关最终用户的私人信息时,必须从中排除了私人信息,之后才能添加到锁定屏幕来显示。 21 | 22 | ![](img/4-10-2.jpg) 23 | 24 | 下面展示了示例代码,说明了如何正确将通知用于包含私人数据的消息。 25 | 26 | 要点: 27 | 28 | 1) 将通知用于包含私人数据的消息,请准备适合公开显示的通知版本(屏幕锁定时显示)。 29 | 30 | 2) 不要在公开显示的通知中包含隐私信息(屏幕锁定时显示)。 31 | 32 | 3) 创建通知时将可见性显示设置为私有。 33 | 34 | 4) 当可见性设置为私有时,通知可能包含私人信息。 35 | 36 | VisibilityPrivateNotificationActivity.java 37 | 38 | ```java 39 | package org.jssec.notification.visibilityPrivate; 40 | 41 | import android.app.Activity; 42 | import android.app.Notification; 43 | import android.app.NotificationManager; 44 | import android.content.Context; 45 | import android.os.Build; 46 | import android.os.Bundle; 47 | import android.view.View; 48 | 49 | public class VisibilityPrivateNotificationActivity extends Activity { 50 | 51 | /** 52 | * Display a private Notification 53 | */ 54 | private final int mNotificationId = 0; 55 | @Override 56 | public void onCreate(Bundle savedInstanceState) { 57 | super.onCreate(savedInstanceState); 58 | setContentView(R.layout.activity_main); 59 | } 60 | 61 | public void onSendNotificationClick(View view) { 62 | // *** POINT 1 *** When preparing a Notification that includes private information, prepare an additional Noficiation for public display (displayed when the screen is locked). 63 | Notification.Builder publicNotificationBuilder = new Notification.Builder(this).setContentTitle("Notif 64 | ication : Public"); 65 | if (Build.VERSION.SDK_INT >= 21) 66 | publicNotificationBuilder.setVisibility(Notification.VISIBILITY_PUBLIC); 67 | // *** POINT 2 *** Do not include private information in Notifications prepared for public display (displayed when the screen is locked). 68 | publicNotificationBuilder.setContentText("Visibility Public : Omitting sensitive data."); 69 | publicNotificationBuilder.setSmallIcon(R.drawable.ic_launcher); 70 | Notification publicNotification = publicNotificationBuilder.build(); 71 | // Construct a Notification that includes private information. 72 | Notification.Builder privateNotificationBuilder = new Notification.Builder(this).setContentTitle("Notification : Private"); 73 | // *** POINT 3 *** Explicitly set Visibility to Private when creating Notifications. 74 | if (Build.VERSION.SDK_INT >= 21) 75 | privateNotificationBuilder.setVisibility(Notification.VISIBILITY_PRIVATE); 76 | 77 | // *** POINT 4 *** When Visibility is set to Private, Notifications may contain private information. 78 | privateNotificationBuilder.setContentText("Visibility Private : Including user info."); 79 | privateNotificationBuilder.setSmallIcon(R.drawable.ic_launcher); 80 | // When creating a Notification with Visibility=Private, we also create and register a separate 81 | Notification with Visibility=Public for public display. 82 | if (Build.VERSION.SDK_INT >= 21) 83 | privateNotificationBuilder.setPublicVersion(publicNotification); 84 | Notification privateNotification = privateNotificationBuilder.build(); 85 | //Although not implemented in this sample code, in many cases 86 | //Notifications will use setContentIntent(PendingIntent intent) 87 | //to ensure that an Intent is transmission when Notification 88 | //is clicked. In this case, it is necessary to take steps--depending 89 | //on the type of component being called--to ensure that the Intent 90 | //in question is called by safe methods (for example, by explicitly 91 | //using Intent). For information on safe methods for calling various 92 | //types of component, see the following sections. 93 | //4.1. Creating and using Activities 94 | //4.2. Sending and receiving Broadcasts 95 | //4.4. Creating and using Services 96 | NotificationManager notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE); 97 | notificationManager.notify(mNotificationId, privateNotification); 98 | } 99 | } 100 | ``` 101 | 102 | ### 4.10.2 规则书 103 | 104 | 创建通知时,应该遵循下列规则: 105 | 106 | #### 4.10.2.1 无论可见性设置如何,通知都不得包含敏感信息(尽管私有信息是例外情况)(必需) 107 | 108 | 在使用 Android 4.3(API 级别 18)或更高版本的终端上,用户可以使用“设置”窗口,授予应用读取通知的权限。 获得此权限的应用将能够读取通知中的所有信息;因此,通知中不得包含敏感信息。 (但是,根据“可见性”设置,通知中可能会包含私有信息)。 109 | 110 | 通知中包含的信息通常不会被发送通知的应用以外的应用读取。 但是,用户可以明确将权限授予某些用户选择的应用,来读取通知中的所有信息。 因为只有用户已授予权限的应用才能读取通知中的信息,所以在通知中包含用户的私有信息没有任何问题。 另一方面,如果在通知中包括除了用户的私有信息之外的敏感信息(例如,仅由应用开发者知道的秘密信息),则用户自己可以尝试读取通知中包含的信息,并且可以授予应用权限来查看这些信息;因此包含私有用户信息以外的敏感信息是有问题的。 111 | 112 | 特定方法和条件请见“4.10.3.1 用户授予的查看通知的权限”。 113 | 114 | #### 4.10.2.2 可见性为公共的通知,不能包含私有信息(必需) 115 | 116 | 在发送可见性为公共的通知时,私有用户信息不得包含在通知中。 当通知的可见性为公开时,即使屏幕被锁定,通知中的信息也会显示。 这是因为这种通知存在风险,私密信息可能被第三方物理邻近的终端看到和窃取。 117 | 118 | VisibilityPrivateNotificationActivity.java 119 | 120 | ```java 121 | // Prepare a Notification for public display (to be displayed on locked screens) that does not contain sensitive information 122 | Notification.Builder publicNotificationBuilder = new Notification.Builder(this).setContentTitle("Notification : Public"); 123 | publicNotificationBuilder.setVisibility(Notification.VISIBILITY_PUBLIC); 124 | // Do not include private information in Notifications for public display (to be displayed on locked screens) 125 | publicNotificationBuilder.setContentText("Visibility Public: sending notification without sensitive information "); 126 | publicNotificationBuilder.setSmallIcon(R.drawable.ic_launcher); 127 | ``` 128 | 129 | #### 4.10.2.3 对于包含私有信息的通知,可见性必须显式设置为私有或秘密(必需) 130 | 131 | 即使屏幕锁定,使用 Android 5.0(API Level 21)或更高版本的终端也会显示通知。 因此,当通知包含私有信息时,其可见性标志应显式设置为私有或秘密。 这是为了防止通知中包含的私有信息显示在锁定屏幕上。 132 | 133 | 目前,可见性的默认值被设置为私有,所以前述风险只有在该标志显式变为公共时才会出现。 但是,可见性的默认值可能会在未来发生变化; 出于这个原因,并且为了在处理信息时始终清楚地表达意图,必须对包含私有信息的通知,将可见性显式设置为私有。 134 | 135 | VisibilityPrivateNotificationActivity.java 136 | 137 | ```java 138 | // Create a Notification that includes private information 139 | Notification.Builder priavteNotificationBuilder = new Notification.Builder(this).setContentTitle("Notification : Private"); 140 | // *** POINT *** Explicitly set Visibility=Private when creating the Notification 141 | priavteNotificationBuilder.setVisibility(Notification.VISIBILITY_PRIVATE); 142 | ``` 143 | 144 | 私有信息的典型示例包括发送给用户的电子邮件,用户的位置数据,以及“5.5 处理隐私数据”部分列出的其他项目。 145 | 146 | 在使用 Android 4.3(API 级别 18)或更高版本的终端上,用户可以使用“设置”窗口,授予应用读取通知的权限,授予此权限的应用将能够读取通知中的所有信息;因此,除私有用户信息以外的敏感信息不得包含在通知中。 147 | 148 | #### 4.10.2.4 使用可见性为私有的通知,创建可见性为公共的额外通知用于展示(推荐) 149 | 150 | 当传递可见性为私有的信息时,最好同时创建一个额外的通知,用于公开展示,它的可见性为公开;这是为了限制锁定屏幕上显示的信息。 151 | 152 | 如果公开显示的通知未与可见性为私有的通知一起注册,则在屏幕锁定时将显示由操作系统准备的默认消息。 因此在这种情况下没有安全问题。 但是,为了在处理信息时始终清晰地表达意图,建议显示创建并注册公开显示的通知。 153 | 154 | VisibilityPrivateNotificationActivity.java 155 | 156 | ```java 157 | // Create a Notification that contains private information 158 | Notification.Builder privateNotificationBuilder = new Notification.Builder(this).setContentTitle("Notification : Private"); 159 | // *** POINT *** Explicitly set Visibility=Private when creating the Notification 160 | if (Build.VERSION.SDK_INT >= 21) 161 | privateNotificationBuilder.setVisibility(Notification.VISIBILITY_PUBLIC); 162 | // *** POINT *** Notifications with Visibility=Private may include private information 163 | privateNotificationBuilder.setContentText("Visibility Private : Including user info."); 164 | privateNotificationBuilder.setSmallIcon(R.drawable.ic_launcher); 165 | // When creating a Notification with Visibility=Private, simultaneously create and register a public-display Notification with Visibility=Public 166 | if (Build.VERSION.SDK_INT >= 21) 167 | privateNotificationBuilder.setPublicVersion(publicNotification); 168 | ``` 169 | 170 | ### 4.10.3 高级话题 171 | 172 | #### 4.10.3 用户授予的查看通知的权限 173 | 174 | 如上面“4.10.2.1 无论可见性设置如何,通知不得包含敏感信息(尽管私人信息是例外)”所述,在使用 Android 4.3(API Level 18)或更高版本的终端上,某些用户选择的应用,已被授予用户权限,可能会读取所有通知中的信息。 175 | 176 | 但是,为了使应用有资格获得此用户权限,应用必须实现从`NotificationListenerService`派生的服务。 177 | 178 | ![](img/4-10-3.jpg) 179 | 180 | 下面的代码展示了`NotificationListenerService`的用法。 181 | 182 | AndroidManifest.xml 183 | 184 | ```xml 185 | 187 | 191 | 194 | 195 | 197 | 198 | 199 | 200 | 201 | ``` 202 | 203 | MyNotificationListenerService.java 204 | 205 | ```java 206 | package org.jssec.notification.notificationListenerService; 207 | 208 | import android.app.Notification; 209 | import android.service.notification.NotificationListenerService; 210 | import android.service.notification.StatusBarNotification; 211 | import android.util.Log; 212 | 213 | public class MyNotificationListenerService extends NotificationListenerService { 214 | 215 | @Override 216 | public void onNotificationPosted(StatusBarNotification sbn) { 217 | // Notification is posted. 218 | outputNotificationData(sbn, "Notification Posted : "); 219 | } 220 | 221 | @Override 222 | public void onNotificationRemoved(StatusBarNotification sbn) { 223 | // Notification is deleted. 224 | outputNotificationData(sbn, "Notification Deleted : "); 225 | } 226 | 227 | private void outputNotificationData(StatusBarNotification sbn, String prefix) { 228 | Notification notification = sbn.getNotification(); 229 | int notificationID = sbn.getId(); 230 | String packageName = sbn.getPackageName(); 231 | long PostTime = sbn.getPostTime(); 232 | String message = prefix + "Visibility :" + notification.visibility + " ID : " + notificationID; 233 | message += " Package : " + packageName + " PostTime : " + PostTime; 234 | Log.d("NotificationListen", message); 235 | } 236 | } 237 | ``` 238 | 239 | 如上所述,通过使用`NotificationListenerService`获取用户权限,可以读取通知。 但是,由于通知中终端上包含的信息经常包含私有信息,因此在处理此类信息时需要小心。 240 | 241 | -------------------------------------------------------------------------------- /4.2.1.1.md: -------------------------------------------------------------------------------- 1 | #### 4.2.1.1 私有广播接收器 2 | 3 | 私人广播接收器是最安全的广播接收器,因为只能接收到从应用内发送的广播。 动态广播接收器不能注册为私有,所以私有广播接收器只包含静态广播接收器。 4 | 5 | 要点(接收广播): 6 | 7 | 1) 将导出属性显示设为`false` 8 | 9 | 2) 小心并安全地处理收到的意图,即使意图从相同的应用中发送 10 | 11 | 3) 敏感信息可以作为返回结果发送,因为请求来自相同应用 12 | 13 | AndroidManifest.xml 14 | 15 | ```xml 16 | 17 | 19 | 20 | 24 | 25 | 26 | 29 | 30 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ``` 42 | 43 | PrivateReceiver.java 44 | 45 | ```java 46 | package org.jssec.android.broadcast.privatereceiver; 47 | 48 | import android.app.Activity; 49 | import android.content.BroadcastReceiver; 50 | import android.content.Context; 51 | import android.content.Intent; 52 | import android.widget.Toast; 53 | 54 | public class PrivateReceiver extends BroadcastReceiver { 55 | 56 | @Override 57 | public void onReceive(Context context, Intent intent) { 58 | // *** POINT 2 *** Handle the received intent carefully and securely, 59 | // even though the intent was sent from within the same application. 60 | // Omitted, since this is a sample. Please refer to "3.2 Handling Input Data Carefully and Securely." 61 | String param = intent.getStringExtra("PARAM"); 62 | Toast.makeText(context, 63 | String.format("Received param: ¥"%s¥"", param), 64 | Toast.LENGTH_SHORT).show(); 65 | // *** POINT 3 *** Sensitive information can be sent as the returned results since the requests come from within the same application. 66 | setResultCode(Activity.RESULT_OK); 67 | setResultData("Sensitive Info from Receiver"); 68 | abortBroadcast(); 69 | } 70 | } 71 | ``` 72 | 73 | 向私有广播接收器发送广播的代码展示在下面: 74 | 75 | 要点(发送广播): 76 | 77 | 4) 使用带有指定类的显式意图,来调用相同应用中的接收器。 78 | 79 | 5) 敏感信息可以发送,因为目标接收器在相同应用中。 80 | 81 | 6) 小心并安全地处理收到的返回结果,即使数据来自相同应用中的接收器。 82 | 83 | PrivateSenderActivity.java 84 | 85 | ```java 86 | package org.jssec.android.broadcast.privatereceiver; 87 | 88 | import android.app.Activity; 89 | import android.content.BroadcastReceiver; 90 | import android.content.Context; 91 | import android.content.Intent; 92 | import android.os.Bundle; 93 | import android.view.View; 94 | import android.widget.TextView; 95 | 96 | public class PrivateSenderActivity extends Activity { 97 | 98 | public void onSendNormalClick(View view) { 99 | // *** POINT 4 *** Use the explicit Intent with class specified to call a receiver within the same application. 100 | Intent intent = new Intent(this, PrivateReceiver.class); 101 | // *** POINT 5 *** Sensitive information can be sent since the destination Receiver is within the same application. 102 | intent.putExtra("PARAM", "Sensitive Info from Sender"); 103 | sendBroadcast(intent); 104 | } 105 | 106 | public void onSendOrderedClick(View view) { 107 | // *** POINT 4 *** Use the explicit Intent with class specified to call a receiver within the same application. 108 | Intent intent = new Intent(this, PrivateReceiver.class); 109 | // *** POINT 5 *** Sensitive information can be sent since the destination Receiver is within the same application. 110 | intent.putExtra("PARAM", "Sensitive Info from Sender"); 111 | sendOrderedBroadcast(intent, null, mResultReceiver, null, 0, null, null); 112 | } 113 | 114 | private BroadcastReceiver mResultReceiver = new BroadcastReceiver() { 115 | @Override 116 | public void onReceive(Context context, Intent intent) { 117 | // *** POINT 6 *** Handle the received result data carefully and securely, 118 | // even though the data came from the Receiver within the same application. 119 | // Omitted, since this is a sample. Please refer to "3.2 Handling Input Data Carefully and Securely." 120 | String data = getResultData(); 121 | PrivateSenderActivity.this.logLine( 122 | String.format("Received result: ¥"%s¥"", data)); 123 | } 124 | }; 125 | 126 | private TextView mLogView; 127 | @Override 128 | public void onCreate(Bundle savedInstanceState) { 129 | super.onCreate(savedInstanceState); 130 | setContentView(R.layout.main); 131 | mLogView = (TextView)findViewById(R.id.logview); 132 | } 133 | 134 | private void logLine(String line) { 135 | mLogView.append(line); 136 | mLogView.append("¥n"); 137 | } 138 | } 139 | ``` 140 | -------------------------------------------------------------------------------- /4.2.1.2.md: -------------------------------------------------------------------------------- 1 | #### 4.2.1.2 公共广播接收器 2 | 3 | 公共广播接收器是可以从未指定的大量应用程序接收广播的广播接收器,因此有必要注意,它可能从恶意软件接收广播。 4 | 5 | 要点(接收广播): 6 | 7 | 1) 将导出属性显式设为`true`。 8 | 9 | 2) 小心并安全地处理收到的意图。 10 | 11 | 3) 返回结果时,不要包含敏感信息。 12 | 13 | 公共广播接收器的示例代码可以用于静态和动态广播接收器。 14 | 15 | PublicReceiver.java 16 | 17 | ```java 18 | package org.jssec.android.broadcast.publicreceiver; 19 | 20 | import android.app.Activity; 21 | import android.content.BroadcastReceiver; 22 | import android.content.Context; 23 | import android.content.Intent; 24 | import android.widget.Toast; 25 | 26 | public class PublicReceiver extends BroadcastReceiver { 27 | 28 | private static final String MY_BROADCAST_PUBLIC = 29 | "org.jssec.android.broadcast.MY_BROADCAST_PUBLIC"; 30 | public boolean isDynamic = false; 31 | 32 | private String getName() { 33 | return isDynamic ? "Public Dynamic Broadcast Receiver" : "Public Static Broadcast Receiver"; 34 | } 35 | 36 | @Override 37 | public void onReceive(Context context, Intent intent) { 38 | // *** POINT 2 *** Handle the received Intent carefully and securely. 39 | // Since this is a public broadcast receiver, the requesting application may be malware. 40 | // Omitted, since this is a sample. Please refer to "3.2 Handling Input Data Carefully and Securely." 41 | if (MY_BROADCAST_PUBLIC.equals(intent.getAction())) { 42 | String param = intent.getStringExtra("PARAM"); 43 | Toast.makeText(context, 44 | String.format("%s:¥nReceived param: ¥"%s¥"", getName(), param), 45 | Toast.LENGTH_SHORT).show(); 46 | } 47 | // *** POINT 3 *** When returning a result, do not include sensitive information. 48 | // Since this is a public broadcast receiver, the requesting application may be malware. 49 | // If no problem when the information is taken by malware, it can be returned as result. 50 | setResultCode(Activity.RESULT_OK); 51 | setResultData(String.format("Not Sensitive Info from %s", getName())); 52 | abortBroadcast(); 53 | } 54 | } 55 | ``` 56 | 57 | 静态广播接收器定义在`AndroidManifest.xml`中: 58 | 59 | AndroidManifest.xml 60 | 61 | ```xml 62 | 63 | 65 | 69 | 70 | 71 | 72 | 75 | 76 | 77 | 78 | 79 | 80 | 83 | 84 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | ``` 96 | 97 | 在动态广播接收器中,通过调用程序中的`registerReceiver()`或`unregisterReceiver()`来执行注册/注销。 为了通过按钮操作执行注册/注销,该按钮`PublicReceiverActivity`中定义。 由于动态广播接收器实例的作用域比`PublicReceiverActivity`长,因此不能将其保存为`PublicReceiverActivity`的成员变量。 在这种情况下,请将动态广播接收器实例保存为`DynamicReceiverService`的成员变量,然后从`PublicReceiverActivity`启动/结束`DynamicReceiverService`,来间接注册/注销动态广播接收器。 98 | 99 | DynamicReceiverService.java 100 | 101 | ```java 102 | package org.jssec.android.broadcast.publicreceiver; 103 | 104 | import android.app.Service; 105 | import android.content.Intent; 106 | import android.content.IntentFilter; 107 | import android.os.IBinder; 108 | import android.widget.Toast; 109 | 110 | public class DynamicReceiverService extends Service { 111 | 112 | private static final String MY_BROADCAST_PUBLIC = 113 | "org.jssec.android.broadcast.MY_BROADCAST_PUBLIC"; 114 | private PublicReceiver mReceiver; 115 | 116 | @Override 117 | public IBinder onBind(Intent intent) { 118 | return null; 119 | } 120 | 121 | @Override 122 | public void onCreate() { 123 | super.onCreate(); 124 | // Register Public Dynamic Broadcast Receiver. 125 | mReceiver = new PublicReceiver(); 126 | mReceiver.isDynamic = true; 127 | IntentFilter filter = new IntentFilter(); 128 | filter.addAction(MY_BROADCAST_PUBLIC); 129 | filter.setPriority(1); // Prioritize Dynamic Broadcast Receiver, rather than Static Broadcast Receiver. 130 | registerReceiver(mReceiver, filter); 131 | Toast.makeText(this, 132 | "Registered Dynamic Broadcast Receiver.", 133 | Toast.LENGTH_SHORT).show(); 134 | } 135 | 136 | @Override 137 | public void onDestroy() { 138 | super.onDestroy(); 139 | // Unregister Public Dynamic Broadcast Receiver. 140 | unregisterReceiver(mReceiver); 141 | mReceiver = null; 142 | Toast.makeText(this, 143 | "Unregistered Dynamic Broadcast Receiver.", 144 | Toast.LENGTH_SHORT).show(); 145 | } 146 | } 147 | ``` 148 | 149 | PublicReceiverActivity.java 150 | 151 | ```java 152 | package org.jssec.android.broadcast.publicreceiver; 153 | 154 | import android.app.Activity; 155 | import android.content.Intent; 156 | import android.os.Bundle; 157 | import android.view.View; 158 | 159 | public class PublicReceiverActivity extends Activity { 160 | 161 | @Override 162 | protected void onCreate(Bundle savedInstanceState) { 163 | super.onCreate(savedInstanceState); 164 | setContentView(R.layout.main); 165 | } 166 | 167 | public void onRegisterReceiverClick(View view) { 168 | Intent intent = new Intent(this, DynamicReceiverService.class); 169 | startService(intent); 170 | } 171 | 172 | public void onUnregisterReceiverClick(View view) { 173 | Intent intent = new Intent(this, DynamicReceiverService.class); 174 | stopService(intent); 175 | } 176 | } 177 | ``` 178 | 179 | 接下来,展示了将广播发送到公共广播接收器的示例代码。 当向公共广播接收器发送广播时,需要注意广播可以被恶意软件接收。 180 | 181 | 要点(发送广播): 182 | 183 | 4) 不要发送敏感信息 184 | 185 | 5) 接受广播时,小心并安全地处理结果数据 186 | 187 | PublicSenderActivity.java 188 | 189 | ```java 190 | package org.jssec.android.broadcast.publicsender; 191 | 192 | import android.app.Activity; 193 | import android.content.BroadcastReceiver; 194 | import android.content.Context; 195 | import android.content.Intent; 196 | import android.os.Bundle; 197 | import android.view.View; 198 | import android.widget.TextView; 199 | 200 | public class PublicSenderActivity extends Activity { 201 | 202 | private static final String MY_BROADCAST_PUBLIC = 203 | "org.jssec.android.broadcast.MY_BROADCAST_PUBLIC"; 204 | 205 | public void onSendNormalClick(View view) { 206 | // *** POINT 4 *** Do not send sensitive information. 207 | Intent intent = new Intent(MY_BROADCAST_PUBLIC); 208 | intent.putExtra("PARAM", "Not Sensitive Info from Sender"); 209 | sendBroadcast(intent); 210 | } 211 | 212 | public void onSendOrderedClick(View view) { 213 | // *** POINT 4 *** Do not send sensitive information. 214 | Intent intent = new Intent(MY_BROADCAST_PUBLIC); 215 | intent.putExtra("PARAM", "Not Sensitive Info from Sender"); 216 | sendOrderedBroadcast(intent, null, mResultReceiver, null, 0, null, null); 217 | } 218 | 219 | public void onSendStickyClick(View view) { 220 | // *** POINT 4 *** Do not send sensitive information. 221 | Intent intent = new Intent(MY_BROADCAST_PUBLIC); 222 | intent.putExtra("PARAM", "Not Sensitive Info from Sender"); 223 | //sendStickyBroadcast is deprecated at API Level 21 224 | sendStickyBroadcast(intent); 225 | } 226 | 227 | public void onSendStickyOrderedClick(View view) { 228 | // *** POINT 4 *** Do not send sensitive information. 229 | Intent intent = new Intent(MY_BROADCAST_PUBLIC); 230 | intent.putExtra("PARAM", "Not Sensitive Info from Sender"); 231 | //sendStickyOrderedBroadcast is deprecated at API Level 21 232 | sendStickyOrderedBroadcast(intent, mResultReceiver, null, 0, null, null); 233 | } 234 | 235 | public void onRemoveStickyClick(View view) { 236 | Intent intent = new Intent(MY_BROADCAST_PUBLIC); 237 | //removeStickyBroadcast is deprecated at API Level 21 238 | removeStickyBroadcast(intent); 239 | } 240 | 241 | private BroadcastReceiver mResultReceiver = new BroadcastReceiver() { 242 | 243 | @Override 244 | public void onReceive(Context context, Intent intent) { 245 | // *** POINT 5 *** When receiving a result, handle the result data carefully and securely. 246 | // Omitted, since this is a sample. Please refer to "3.2 Handling Input Data Carefully and Securely." 247 | String data = getResultData(); 248 | PublicSenderActivity.this.logLine( 249 | String.format("Received result: ¥"%s¥"", data)); 250 | } 251 | }; 252 | 253 | private TextView mLogView; 254 | @Override 255 | public void onCreate(Bundle savedInstanceState) { 256 | super.onCreate(savedInstanceState); 257 | setContentView(R.layout.main); 258 | mLogView = (TextView)findViewById(R.id.logview); 259 | } 260 | 261 | private void logLine(String line) { 262 | mLogView.append(line); 263 | mLogView.append("¥n"); 264 | } 265 | } 266 | ``` 267 | -------------------------------------------------------------------------------- /4.2.1.md: -------------------------------------------------------------------------------- 1 | ### 4.2.1 示例代码 2 | 3 | 接收广播需要创建广播接收器。 使用广播接收器的风险和对策,根据收到的广播的类型而有所不同。 你可以在以下判断流程中找到你的广播接收器。 接收应用无法检查发送广播的应用的包名称,它是链接伙伴所需的。 因此,无法创建用于伙伴的广播接收器。 4 | 5 | 表 4.2:广播接收器的类型定义: 6 | 7 | | 类型 | 定义 | 8 | | --- | --- | 9 | | 私有 | 只能接收来自相同应用的广播的广播接收器,所以是最安全的 | 10 | | 公共 | 可以接收来自未指定的大量应用的广播的广播接收器 | 11 | | 内部 | 只能接收来自其他内部应用的广播的广播接收器 | 12 | 13 | ![](img/4-2-1.jpg) 14 | 15 | 另外,根据定义方法,广播接收器可以分为两类:静态和动态。 它们之间的差异可以在下图中找到。 示例代码展示了每类的实现方法。 还描述了发送应用的实现方法,因为发送信息的对策取决于接收器来确定。 16 | 17 | 表 4.2-2 18 | 19 | | | 定义方法 | 特性 | 20 | | --- | --- | --- | 21 | | 静态 | 由`AndroidManifest.xml`中的``元素定义 | 1)存在一些限制,不能收到一些由系统发送的广播,如`ACTION_BATTERY_CHANGED`。2)从应用最初启动开始,卸载之前,可以收到广播。 | 22 | | 动态 | 通过在程序中调用`registerReceiver()`和`unregisterReceiver()`,动态注册和注销广播接收器 | 1)可以收到静态广播接收器收不到的广播。2)广播的接收时期可以由程序控制,例如,只有活动在前台时,可以接收广播。3)不能创建私有广播接收器。 | 23 | 24 | -------------------------------------------------------------------------------- /4.2.2.md: -------------------------------------------------------------------------------- 1 | ### 4.2.2 规则书 2 | 3 | 遵循下列规则来发送或接受广播。 4 | 5 | #### 4.2.2.1 仅在应用中使用的广播接收器必须设置为私有(必需) 6 | 7 | 仅在应用中使用的广播接收器应该设置为私有,以避免意外地从其他应用接收任何广播。 它将防止应用功能滥用或异常行为。 8 | 9 | 仅在同一应用内使用的接收器,不应设计为设置意图过滤器。 由于意图过滤器的特性,即使通过意图过滤器调用同一应用中的私有接收器,其他应用的公共私有也可能被意外调用。 10 | 11 | AndroidManifest.xml(不推荐) 12 | 13 | ```xml 14 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | ``` 24 | 25 | 请参阅“4.2.3.1 导出属性和意图过滤器设置的组合(对于接收器)”。 26 | 27 | #### 4.2.2.2 小心和安全地处理收到的意图(必需) 28 | 29 | 虽然风险因广播接收器的类型而异,但处理接收到的意图数据时,首先应该验证意图的安全性。 由于公共广播接收器从未指定的大量应用接收意图,它可能会收到恶意软件的攻击意图。 私有广播接收器将永远不会直接从其他应用接收任何意图,但公共组件从其他应用接收的意图数据,可能会转发到私有广播接收器。 所以不要认为收到的意图在没有任何验证的情况下,是完全安全的。 内部广播接收机具有一定程度的风险,因此还需要验证接收意图的安全性。 30 | 31 | 请参考“3.2 小心和安全地处理输入数据”。 32 | 33 | #### 4.2.2.3 验证签名权限是否由内部应用定义后,使用内部定义的签名权限(必需) 34 | 35 | 只接收内部应用发送的广播的内部广播接收器,应受内部定义的签名许可保护。 `AndroidManifest.xml`中的权限定义/权限请求声明不足以保护,因此请参阅“5.2.1.2 如何使用内部定义的签名权限在内部应用之间进行通信”。 通过对`receiverPermission`参数指定内部定义的签名权限来结束广播,需要相同的方式的验证。 36 | 37 | #### 4.2.2.4 返回结果信息时,清注意来自目标应用的结果信息泄露(必需) 38 | 39 | 通过`setResult()`返回结果信息的应用的可靠性取决于广播接收器的类型。 对于公共广播接收器,目标应用可能是恶意软件,可能存在恶意使用结果信息的风险。 对于私有广播接收器和内部广播接收器,结果的目的地是内部开发的应用,因此无需介意结果信息的处理。 40 | 41 | 如上所述,当从广播接收器返回结果信息时,需要注意从目标应用泄漏的结果信息。 42 | 43 | #### 4.2.2.5 使用广播发送敏感信息时,限制能收到的接收器(必需) 44 | 45 | 广播是所创建的系统,用于向未指定的大量应用广播信息或一次通知其时间。 因此,广播敏感信息需要谨慎设计,以防止恶意软件非法获取信息。 对于广播敏感信息,只有可靠的广播接收器可以接收它,而其他广播接收器则不能。 以下是广播发送方法的一些示例。 46 | 47 | + 方法是,通过使用显式意图,将广播仅仅发送给预期的可靠广播接收器,来固定地址。 48 | + 当它发送给同一个应用中的广播接收器时,通过`Intent#setClass(Context, Class)`指定地址。 具体代码,请参阅“4.2.1.1 私有广播接收器 - 接收/发送广播”的示例代码部分。 49 | + 当它发送到其他应用中的广播接收器时,通过`Intent#setClassName(String, String)`指定地址。 通过比较目标包中 APK 签名的开发人员密钥和白名单来发送广播,来确认允许的应用。 实际上下面的使用隐式意图的方法更实用。 50 | + 方法是,通过将`receiverPermission`指定为内部定义的签名权限,并使可靠的广播接收器声明使用此签名权限,来发送广播。具体代码请参阅“4.2.1.3 内部广播接收器 - 接收/发送广播”的示例代码部分。 另外,实现这种广播发送方法,需要应用规则“4.2.2.3 在验证签名权限由内部应用定义之后,使用内部定义的签名权限”。 51 | 52 | #### 4.2.2.6 粘性广播中禁止包含敏感信息(必需) 53 | 54 | 通常情况下,广播由可用的广播接收器接收后会消失。 另一方面,粘性广播(以下粘性广播包括粘性有序广播)即使由可用的广播接收器接收也不会从系统中消失,并且能够由`registerReceiver()`接收。 当粘性广播变得不必要时,可以随时用`removeStickyBroadcast()`任意删除它。 55 | 56 | 由于在预设情况下,粘性广播被隐式意图使用。 具有指定`receiverPermission`参数的广播无法发送。 出于这个原因,通过粘性广播发送的信息,可以被多个未指定的应用访问 - 包括恶意软件 - 因此敏感信息禁止以这种方式发送。 请注意,粘性广播在 Android 5.0(API Level 21)中已弃用。 57 | 58 | #### 4.2.2.7 注意不指定`receiverPermission`的有序广播无法传递(必需) 59 | 60 | 不指定`receiverPermission`参数的有序广播,可以由未指定的大量应用接收,包括恶意软件。 有序广播用于接收来自接收器的返回信息,并使几个接收器逐一执行处理。 广播按优先顺序发送给接收器。 因此,如果高优先级恶意软件先接收广播并执行`abortBroadcast()`,则广播将不会传送到后面的接收器。 61 | 62 | #### 4.2.2.8 小心并安全地处理来自广播接收器的返回的结果数据(必需) 63 | 64 | 基本上,考虑到接收结果可能是攻击数据,结果数据应该被安全地处理,尽管风险取决于返回结果数据的广播接收器的类型。 65 | 66 | 当发送方(源)广播接收器是公共广播接收器时,它从未指定的大量应用接收返回数据。 所以它也可能会收到恶意软件的攻击数据。 当发送方(源)广播接收器是私有广播接收者时,似乎没有风险。 然而,其他应用接收的数据可能会间接作为结果数据转发。 因此,如果没有任何验证,结果数据不应该被认为是安全的。 当发送方(源)广播接收器是内部广播接收器时,它具有一定程度的风险。 因此,考虑到结果数据可能是攻击数据,应该以安全的方式处理它。 67 | 68 | 请参考“3.2 小心和安全地处理输入数据”。 69 | 70 | #### 4.2.2.9 提供二手素材时,素材应该以相同保护级别提供(必需) 71 | 72 | 当由权限保护的信息或功能素材被二次提供给其他应用时,有必要通过声明与目标应用相同的权限来维持保护标准。 在 Android 权限安全模型中,权限仅管理来自应用的受保护素材的直接访问。 由于这些特点,所得素材可能会被提供给其他应用,而无需声明保护所需的权限。 这实际上与重新授权相同,因为它被称为重新授权问题。 请参阅“5.2.3.4 重新授权问题”。 73 | -------------------------------------------------------------------------------- /4.2.3.md: -------------------------------------------------------------------------------- 1 | ### 4.2.3 高级话题 2 | 3 | #### 4.2.3.1 结合导出属性和意图过滤器设置(用于接收器) 4 | 5 | 表 4.2-3 展示了实现接收器时,导出设置和意图过滤器元素的允许的组合。 下面介绍为什么原则上禁止使用带有意图过滤器定义的`exported ="false"`。 6 | 7 | 表 4.2-3 可用与否,导出属性和意图过滤器元素的组合 8 | 9 | | | 导出属性的值 | | | 10 | | --- | --- | --- | --- | 11 | | | True | False | 未指定 | 12 | | 意图过滤器已定义 | OK | 不使用 | 不使用 | 13 | | 意图过滤器未定义 | OK | OK | 不使用 | 14 | 15 | 未指定接收器的导出属性时,接收器是否为公共的,取决于该接收器的意图过滤器的存在与否 [6]。但是,在本手册中,禁止将导出的属性设置为不确定的。 通常,如前所述,最好避免依赖任何给定 API 的默认行为的实现;此外,如果存在明确的方法(如导出属性)来启用重要的安全相关设置,那么使用这些方法总是一个好主意。 16 | 17 | > [6] 如果意图过滤器已定义,接收器是公共的,否则是私有的。更多信息请参考 。 18 | 19 | 即使在相同的应用中将广播发送到私有接收器,其他应用中的公共接收器也可能会意外调用。 这就是为什么禁止指定带有意图过滤器定义的`exported ="false"`。 以下两张图展示了意外调用的发生情况。 20 | 21 | 图 4.2-4 是一个正常行为的例子,隐式意图只能在同一个应用中调用私有接收器(应用 A)。 意图过滤器(在图中,`action ="X"`)仅在应用 A 中定义,所以这是预期的行为。 22 | 23 | ![](img/4-2-4.jpg) 24 | 25 | 图 4.2-5 是个例子,应用 B 和应用 A 中都定义了意图过滤器(见图中的`action ="X"`)的。首先,当另一个应用(应用 C)通过 隐式意图发送广播,它们不被私有接收器(A-1)接收。 所以不会有任何安全问题。 (请参阅图中的橙色箭头标记。)从安全角度来看,问题是应用 A 对同一应用中的私有接收器的调用。 当应用 A 广播隐式意图时,不仅是相同应用中的私有接收器,而且具有相同意图过滤器定义的公共接收器(B-1)也可以接收意图。 (图中的红色箭头标记)。 在这种情况下,敏感信息可能会从应用 A 发送到 B。当应用 B 是恶意软件时,会导致敏感信息的泄漏。 当发送有序广播时,它可能会收到意外的结果信息。 26 | 27 | 然而,当广播接收器仅接收由系统发送的广播意图时,应使用带有意图过滤器定义的`exported="false"`。 其他组合不应使用。 这是基于这样一个事实,即系统发送的广播意图可以通过`exported="false"`来接收。 如果其他应用发送的意图的`ACTION`与系统发送的广播意图相同,则可能会通过接收它而导致意外行为。 但是,这可以通过指定`exported="false"`来防止。 28 | 29 | #### 4.2.3.2 接收器在启动应用之前不会被注册 30 | 31 | 请务必注意,在`AndroidManifest.xml`中定义的静态广播接收器,在安装后不会自动启用 [7]。应用只有在第一次启动后才能接收广播;因此,安装后无法使用接收的广播作为启动操作的触发器。 但是,如果在发送广播时设置了`Intent.FLAG_INCLUDE_STOPPED_PACKAGES`标志,则即使是尚未第一次启动的应用也会收到该广播。 32 | 33 | > [7] 在 3.0 之前的版本中,接收器可以通过安装 App 自动启动。 34 | 35 | #### 4.2.3.3 私有广播接收器可以接收由相同 UID 发送的广播 36 | 应用 37 | 38 | 相同的 UID 可以提供给几个应用。 即使它是私有广播接收器,也可以接收从 UID 相同的应用发送的广播。 但是,这不会是一个安全问题。 由于可以确保 UID 相同的应用具有用于签署 APK 的一致的开发人员密钥。 这意味着私有广播接收器收到的广播,只是从内部应用发送的广播。 39 | 40 | #### 4.2.3.4 广播的类型和特性 41 | 42 | 根据是否有序以及是否粘滞的组合,广播有四种类型。 要发送的广播类型基于广播发送方法而确定。 请注意,粘性广播在 Android 5.0(API Level 21)中已弃用。 43 | 44 | | 类型 | 发送方法 | 是否有序 | 是否粘性 | 45 | | --- | --- | --- | --- | 46 | | 普通 | `sendBroadcast()` | 否 | 否 | 47 | | 有序 | `sendOrderedBroadcast()` | 是 | 否 | 48 | | 粘性 | `sendStickyBroadcast()` | 否 | 是 | 49 | | 粘性有序 | `sendStickyOrderedBroadcast()` | 是 | 是 | 50 | 51 | 每个广播类型的特性描述如下: 52 | 53 | | 类型 | 特性 | 54 | | --- | --- | 55 | | 普通 | 普通广播发送到可接收的广播接收器时消失。 广播由多个广播接收器同时接收。 这与有序广播有所不同。 广播被允许由特定的广播接收机接收。 | 56 | | 有序 | 有序广播的特点是,可接收的广播接收器依次接收广播。 优先级较高的广播接收器较早收到。 当广播被传送到所有广播接收器或广播接收器调用`abortBroadcast()`,广播将消失。 广播被允许由声明了特定权限的广播接收器接收。 另外,广播接收器发送的结果信息,可以由发送者使用有序广播接收。 SMS 接收通知的广播(`SMS_RECEIVED`)是有序广播的代表性示例。 | 57 | | 粘性 | 粘性广播不会消失并保留在系统中,然后调用`registerReceiver()`的应用可以稍后接收粘性广播。 由于粘性广播与其他广播不同,它不会自动消失。 因此,当不需要粘性广播时,需要显式调用`removeStickyBroadcast()`来删除粘滞广播。 此外,带有特定权限的受限的广播接收器无法接收广播。 电池状态变化通知的广播(`ACTION_BATTERY_CHANGED`)是粘性广播的代表性示例。 | 58 | | 粘性有序 | 这是具有有序和粘性特征的广播。 与粘性广播相同,它不能仅仅允许带有特定权限的广播接收器接收广播。 | 59 | 60 | 从广播特性行为的角度来看,上表反过来排列在下面的表中。 61 | 62 | | 广播的特征行为 | 普通 | 有序 | 粘性 | 粘性有序 | 63 | | --- | --- | --- | --- | --- | 64 | | 由权限限制的广播接收器可以接收广播 | OK | OK | - | - | 65 | | 从广播接收器获得过程结果 | - | OK | - | OK | 66 | | 使广播接收器按顺序处理广播 | - | OK | - | OK | 67 | | 稍后收到已经发送的广播 | - | - | OK | OK | 68 | 69 | #### 4.2.3.5 广播信息可能输出到`LogCat` 70 | 71 | 发送/接收的广播基本上不会输出到`LogCat`。 然而,缺少权限导致接收/发送方的错误时,将输出错误日志。 由广播发送的意图信息包含在错误日志中,因此在发生错误之后,需要注意,发送广播时,意图的信息显示在`LogCat`中。 72 | 73 | 发送方的缺少权限的错误: 74 | 75 | ``` 76 | W/ActivityManager(266): Permission Denial: broadcasting Intent { act=org.jssec.android.broadcastreceive 77 | r.creating.action.MY_ACTION } from org.jssec.android.broadcast.sending (pid=4685, uid=10058) requires o 78 | rg.jssec.android.permission.MY_PERMISSION due to receiver org.jssec.android.broadcastreceiver.creating/ 79 | org.jssec.android.broadcastreceiver.creating.CreatingType3Receiver 80 | ``` 81 | 82 | 接收方的缺少权限的错误: 83 | 84 | ``` 85 | W/ActivityManager(275): Permission Denial: receiving Intent { act=org.jssec.android.broadcastreceiver.c 86 | reating.action.MY_ACTION } to org.jssec.android.broadcastreceiver.creating requires org.jssec.android.p 87 | ermission.MY_PERMISSION due to sender org.jssec.android.broadcast.sending (uid 10158) 88 | ``` 89 | 90 | #### 4.2.3.6 在主屏幕放置应用的快捷方式时,需要注意的东西 91 | 92 | 在下面的内容中,我们讨论了创建快捷方式时的一些需要注意的东西,它们用于从主屏幕启动应用,或者用于创建 URL 快捷方式,例如 Web 浏览器中的书签。 作为一个例子,我们考虑如下所示的实现。 93 | 94 | 在主屏幕放置应用的快捷方式: 95 | 96 | ```java 97 | Intent targetIntent = new Intent(this, TargetActivity.class); 98 | 99 | // Intent to request shortcut creation 100 | 101 | Intent intent = new Intent("com.android.launcher.action.INSTALL_SHORTCUT"); 102 | // Specify an Intent to be launched when the shortcut is tapped 103 | intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, targetIntent); 104 | Parcelable icon = Intent.ShortcutIconResource.fromContext(context, iconResource); 105 | intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, icon); 106 | intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, title); 107 | intent.putExtra("duplicate", false); 108 | 109 | // Use Broadcast to send the system our request for shortcut creation 110 | context.sendBroadcast(intent); 111 | ``` 112 | 113 | 在由上面的代码片段发送的广播中,接收器是主屏幕应用,并且很难识别包名; 我们必须谨慎记住,这是一个向公共接收器传递的隐式意图。 因此,此片段发送的广播,可以被任何任意应用接收,包括恶意软件;因此,在意图中包含敏感信息可能会造成信息泄漏的风险。 特别重要的是要注意,在创建基于 URL 的快捷方式时,秘密信息可能包含在 URL 本身中。 114 | 115 | 作为对策,有必要遵循“4.2.1.2 公共广播接收器 - 接收/发送广播”中列出的要点,并确保传输的意图不包含敏感信息。 116 | -------------------------------------------------------------------------------- /4.2.md: -------------------------------------------------------------------------------- 1 | ## 4.2 接收/发送广播 -------------------------------------------------------------------------------- /4.3.1.md: -------------------------------------------------------------------------------- 1 | ### 4.3.1 示例代码 2 | 3 | 使用内容供应器的风险和对策取决于内容供应器的使用方式。 在本节中,我们根据内容供应器的使用方式,对 5 种类型的内容供应器进行了分类。 您可以通过下面显示的图表,找出您应该创建哪种类型的内容供应器。 4 | 5 | 表 4.3-1 内容供应器类型定义 6 | 7 | | 类型 | 定义 | 8 | | --- | --- | 9 | | 私有 | 不能由其他应用使用的内容供应器,所以是最安全的 | 10 | | 公共 | 应该由未指定的大量应用使用的内容供应器 | 11 | | 伙伴 | 只能由可信的伙伴公司开发的特定应用使用的内容供应器 | 12 | | 内容 | 只能由其它内部应用使用的内容供应器 | 13 | | 临时 | 基本上是私有内容供应器,但允许特定应用访问特定 URI | 14 | 15 | ![](img/4-3-1.jpg) 16 | 17 | -------------------------------------------------------------------------------- /4.3.2.md: -------------------------------------------------------------------------------- 1 | ### 4.3.2 规则书 2 | 3 | 实现或使用内容供应器时,确保遵循以下规则。 4 | 5 | #### 4.3.2.1 仅仅在应用中使用的内容供应器必须设为私有(必需) 6 | 7 | 仅供单个应用使用的内容供应器不需要被其他应用访问,并且开发人员通常不会考虑攻击内容供应器的访问。 内容供应器基本上是共享数据的系统,因此它默认处理成公共的。 仅在单个应用中使用的内容供应器应该被显式设置为私有,并且它应该是私有内容供应器。 在 Android 2.3.1(API Level 9)或更高版本中,通过在`provider`元素中指定`android:exported="false"`,可以将内容供应器设置为私有。 8 | 9 | AndroidManifest.xml 10 | 11 | ```xml 12 | 13 | 17 | ``` 18 | 19 | #### 4.3.2.2 小心并安全地处理收到的请求参数(必需) 20 | 21 | 风险因内容供应器的类型而异,但在处理请求参数时,你应该做的第一件事是输入验证。 22 | 23 | 虽然内容供应器的每个方法,都有一个接口,应该接收 SQL 语句的成分参数,但实际上它只是交给系统中的任意字符串,所以需要注意内容供应器方需要假设,可能会提供意外的参数的情况。 24 | 25 | 由于公共内容提供应器可以接收来自不受信任来源的请求,因此可能会受到恶意软件的攻击。 另一方面,私有内容供应器永远不会直接收到来自其他应用的任何请求,但是目标应用中的公共活动,可能会将恶意意图转发给私有内容供应器,因此你不应该认为,私有内容供应器不能 接收任何恶意输入。 由于其他内容供应器也有将恶意意图转发给他们的风险,因此有必要对这些请求执行输入验证。 26 | 27 | 请参阅“3.2 小心和安全地处理输入数据”。 28 | 29 | #### 4.3.2.3 验证签名权限由内部定义之后,使用内部定义的签名权限(必需) 30 | 31 | 确保在创建内容供应器时,通过定义内部签名权限,来保护你的内部内容供应器。 由于在`AndroidManifest.xml`文件中定义权限或声明权限请求,没有提供足够的安全性,请务必参考“5.2.1.2 如何使用内部定义的签名权限在内部应用之间进行通信”。 32 | 33 | #### 4.3.2.4 返回结果时,请注意来自目标应用的结果的信息泄露的可能性(必须) 34 | 35 | 在`query()`或插入`insert()`的情况下,`Cursor`或`Uri`作为结果信息返回到发送请求的应用。 当敏感信息包含在结果信息中时,信息可能会从目标应用泄露。 在`update()`或`delete()`的情况下,更新/删除记录的数量作为结果信息返回给发送请求的应用。 在极少数情况下,取决于某些应用的规范,更新/删除记录的数量具有敏感含义,请注意这一点。 36 | 37 | #### 4.3.2.5 提供二手素材时,素材应该以相同级别的保护提供(必需) 38 | 39 | 当受到权限保护的信息或功能素材,被另一个应用提供时,你需要确保它具有访问素材所需的相同权限。 在 Android OS 权限安全模型中,只有已被授予适当权限的应用,才能直接访问受保护的素材。 但是,存在一个漏洞,因为具有素材权限的应用可以充当代理,并允许非特权应用的访问。 基本上这与重授权限相同,因此它被称为“重新授权”问题。 请参阅“5.2.3.4 重新授权问题”。 40 | 41 | #### 4.3.2.6 小心并安全地处理来自内容供应器的返回的结果数据(必需) 42 | 43 | 风险因内容供应器的类型而异,但在处理请求参数时,你应该做的第一件事是输入验证。 44 | 45 | 如果目标内容供应器是公共内容供应器,伪装成公共内容供应器的恶意软件可能会返回攻击性结果数据。 另一方面,如果目标内容供应器是私有内容供应器,则其风险较小,因为它从同一应用接收结果数据,但不应该认为,私有内容供应器不能接收任何恶意输入。 由于其他内容供应器也有将恶意数据返回给他们的风险,因此有必要对该结果数据执行输入验证。 46 | 47 | 请参阅“3.2 小心和安全地处理输入数据”。 48 | -------------------------------------------------------------------------------- /4.3.md: -------------------------------------------------------------------------------- 1 | ## 4.3 创建/使用内容供应器 2 | 3 | 由于`ContentResolver`和`SQLiteDatabase`的接口非常相似,所以常常有个误解,`Content Provider`与`SQLiteDatabase`的关系如此密切。 但是,实际上内容供应器只是提供了应用间数据共享的接口,所以需要注意的是它不会影响每种数据保存格式。 为了保存内容供应器中的数据,可以使用`SQLiteDatabase`,也可以使用其他保存格式,如 XML 文件格式。 以下示例代码中不包含任何数据保存过程,因此请在需要时添加它。 -------------------------------------------------------------------------------- /4.4.1.1.md: -------------------------------------------------------------------------------- 1 | #### 4.4.1.1 创建/使用私有服务 2 | 3 | 私有服务是不能由其他应用启动的服务,因此它是最安全的服务。 当使用仅在应用中使用的私有服务时,只要您对该类使用显式意图,那么您就不必担心意外将它发送到任何其他应用。 4 | 5 | 下面展示了如何使用`startService`类型服务的示例代码。 6 | 7 | 要点(创建服务): 8 | 9 | 1) 将导出属性显式设置为`false`。 10 | 11 | 2) 小心并安全地处理收到的意图,即使意图从相同应用发送。 12 | 13 | 3) 由于请求应用在同一应用中,所以可以发送敏感信息。 14 | 15 | AndroidManifest.xml 16 | 17 | ```xml 18 | 19 | 21 | 25 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ``` 43 | 44 | PrivateStartService.java 45 | 46 | ```java 47 | package org.jssec.android.service.privateservice; 48 | 49 | import android.app.Service; 50 | import android.content.Intent; 51 | import android.os.IBinder; 52 | import android.widget.Toast; 53 | 54 | public class PrivateStartService extends Service { 55 | 56 | // The onCreate gets called only one time when the service starts. 57 | @Override 58 | public void onCreate() { 59 | Toast.makeText(this, "PrivateStartService - onCreate()", Toast.LENGTH_SHORT).show(); 60 | } 61 | 62 | // The onStartCommand gets called each time after the startService gets called. 63 | @Override 64 | public int onStartCommand(Intent intent, int flags, int startId) { 65 | // *** POINT 2 *** Handle the received intent carefully and securely, 66 | // even though the intent was sent from the same application. 67 | // Omitted, since this is a sample. Please refer to "3.2 Handling Input Data Carefully and Securely." 68 | String param = intent.getStringExtra("PARAM"); 69 | Toast.makeText(this, 70 | String.format("PrivateStartService¥nReceived param: ¥"%s¥"", param), 71 | Toast.LENGTH_LONG).show(); 72 | return Service.START_NOT_STICKY; 73 | } 74 | 75 | // The onDestroy gets called only one time when the service stops. 76 | @Override 77 | public void onDestroy() { 78 | Toast.makeText(this, "PrivateStartService - onDestroy()", Toast.LENGTH_SHORT).show(); 79 | } 80 | 81 | @Override 82 | public IBinder onBind(Intent intent) { 83 | // This service does not provide binding, so return null 84 | return null; 85 | } 86 | } 87 | ``` 88 | 89 | 下面是使用私有服务的活动代码: 90 | 91 | 要点(使用服务): 92 | 93 | 4) 使用指定类的显式意图,调用同一应用程序的服务。 94 | 95 | 5) 由于目标服务位于同一应用中,因此可以发送敏感信息。 96 | 97 | 6) 即使数据来自同一应用中的服务,也要小心并安全地处理收到的结果数据。 98 | 99 | PrivateUserActivity.java 100 | 101 | ```java 102 | package org.jssec.android.service.privateservice; 103 | 104 | import android.app.Activity; 105 | import android.content.Intent; 106 | import android.os.Bundle; 107 | import android.view.View; 108 | 109 | public class PrivateUserActivity extends Activity { 110 | 111 | @Override 112 | public void onCreate(Bundle savedInstanceState) { 113 | super.onCreate(savedInstanceState); 114 | setContentView(R.layout.privateservice_activity); 115 | } 116 | 117 | // --- StartService control --- 118 | public void onStartServiceClick(View v) { 119 | // *** POINT 4 *** Use the explicit intent with class specified to call a service in the same application. 120 | Intent intent = new Intent(this, PrivateStartService.class); 121 | // *** POINT 5 *** Sensitive information can be sent since the destination service is in the same application. 122 | intent.putExtra("PARAM", "Sensitive information"); 123 | startService(intent); 124 | } 125 | 126 | public void onStopServiceClick(View v) { 127 | doStopService(); 128 | } 129 | 130 | @Override 131 | public void onStop() { 132 | super.onStop(); 133 | // Stop service if the service is running. 134 | doStopService(); 135 | } 136 | 137 | private void doStopService() { 138 | // *** POINT 4 *** Use the explicit intent with class specified to call a service in the same application. 139 | Intent intent = new Intent(this, PrivateStartService.class); 140 | stopService(intent); 141 | } 142 | 143 | // --- IntentService control --- 144 | public void onIntentServiceClick(View v) { 145 | // *** POINT 4 *** Use the explicit intent with class specified to call a service in the same application. 146 | Intent intent = new Intent(this, PrivateIntentService.class); 147 | // *** POINT 5 *** Sensitive information can be sent since the destination service is in the same application. 148 | intent.putExtra("PARAM", "Sensitive information"); 149 | startService(intent); 150 | } 151 | } 152 | ``` 153 | -------------------------------------------------------------------------------- /4.4.1.2.md: -------------------------------------------------------------------------------- 1 | #### 4.4.1.2 创建/使用公共服务 2 | 3 | 公共服务是应该由未指定的大量应用使用的服务。 有必要注意,它可能会收到恶意软件发送的信息(意图等)。 在使用公共服务的情况下,有必要注意,恶意软件可能会收到要发送的信息(意图等)。 4 | 5 | 下面展示了如何使用`startService`类型服务的示例代码。 6 | 7 | 要点(创建服务): 8 | 9 | 1) 将导出属性显式设置为`true`。 10 | 11 | 2) 小心并安全地处理接收到的意图。 12 | 13 | 3) 返回结果时,请勿包含敏感信息。 14 | 15 | AndroidManifest.xml 16 | 17 | ```xml 18 | 19 | 20 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ``` 43 | 44 | PublicIntentService.java 45 | 46 | ```java 47 | package org.jssec.android.service.publicservice; 48 | 49 | import android.app.IntentService; 50 | import android.content.Intent; 51 | import android.widget.Toast; 52 | 53 | public class PublicIntentService extends IntentService{ 54 | 55 | /** 56 | * Default constructor must be provided when a service extends IntentService class. 57 | * If it does not exist, an error occurs. 58 | */ 59 | public PublicIntentService() { 60 | super("CreatingTypeBService"); 61 | } 62 | 63 | // The onCreate gets called only one time when the Service starts. 64 | @Override 65 | public void onCreate() { 66 | super.onCreate(); 67 | Toast.makeText(this, this.getClass().getSimpleName() + " - onCreate()", Toast.LENGTH_SHORT).show(); 68 | } 69 | 70 | // The onHandleIntent gets called each time after the startService gets called. 71 | @Override 72 | protected void onHandleIntent(Intent intent) { 73 | // *** POINT 2 *** Handle intent carefully and securely. 74 | // Since it's public service, the intent may come from malicious application. 75 | // Omitted, since this is a sample. Please refer to "3.2 Handling Input Data Carefully and Securely." 76 | String param = intent.getStringExtra("PARAM"); 77 | Toast.makeText(this, String.format("Recieved parameter ¥"%s¥"", param), Toast.LENGTH_LONG).show(); 78 | } 79 | 80 | // The onDestroy gets called only one time when the service stops. 81 | @Override 82 | public void onDestroy() { 83 | Toast.makeText(this, this.getClass().getSimpleName() + " - onDestroy()", Toast.LENGTH_SHORT).show(); 84 | } 85 | } 86 | ``` 87 | 88 | 下面是使用公共服务的活动代码: 89 | 90 | 要点(使用服务): 91 | 92 | 4) 不要发送敏感信息。 93 | 94 | 5) 收到结果时,小心并安全地处理结果数据。 95 | 96 | AndroidManifest.xml 97 | 98 | ```xml 99 | 100 | 102 | 106 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | ``` 118 | 119 | PublicUserActivity.java 120 | 121 | ```java 122 | 123 | package org.jssec.android.service.publicserviceuser; 124 | 125 | import android.app.Activity; 126 | import android.content.Intent; 127 | import android.os.Bundle; 128 | import android.view.View; 129 | 130 | public class PublicUserActivity extends Activity { 131 | 132 | // Using Service Info 133 | private static final String TARGET_PACKAGE = "org.jssec.android.service.publicservice"; 134 | private static final String TARGET_START_CLASS = "org.jssec.android.service.publicservice.PublicStartService"; 135 | private static final String TARGET_INTENT_CLASS = "org.jssec.android.service.publicservice.PublicIntentService"; 136 | 137 | @Override 138 | public void onCreate(Bundle savedInstanceState) { 139 | super.onCreate(savedInstanceState); 140 | setContentView(R.layout.publicservice_activity); 141 | } 142 | 143 | // --- StartService control --- 144 | public void onStartServiceClick(View v) { 145 | Intent intent = new Intent("org.jssec.android.service.publicservice.action.startservice"); 146 | // *** POINT 4 *** Call service by Explicit Intent 147 | intent.setClassName(TARGET_PACKAGE, TARGET_START_CLASS); 148 | // *** POINT 5 *** Do not send sensitive information. 149 | intent.putExtra("PARAM", "Not sensitive information"); 150 | startService(intent); 151 | // *** POINT 6 *** When receiving a result, handle the result data carefully and securely. 152 | // This sample code uses startService(), so receiving no result. 153 | } 154 | 155 | public void onStopServiceClick(View v) { 156 | doStopService(); 157 | } 158 | 159 | // --- IntentService control --- 160 | public void onIntentServiceClick(View v) { 161 | Intent intent = new Intent("org.jssec.android.service.publicservice.action.intentservice"); 162 | // *** POINT 4 *** Call service by Explicit Intent 163 | intent.setClassName(TARGET_PACKAGE, TARGET_INTENT_CLASS); 164 | // *** POINT 5 *** Do not send sensitive information. 165 | intent.putExtra("PARAM", "Not sensitive information"); 166 | startService(intent); 167 | } 168 | 169 | @Override 170 | public void onStop(){ 171 | super.onStop(); 172 | // Stop service if the service is running. 173 | doStopService(); 174 | } 175 | 176 | // Stop service 177 | private void doStopService() { 178 | Intent intent = new Intent("org.jssec.android.service.publicservice.action.startservice"); 179 | // *** POINT 4 *** Call service by Explicit Intent 180 | intent.setClassName(TARGET_PACKAGE, TARGET_START_CLASS); 181 | stopService(intent); 182 | } 183 | } 184 | ``` 185 | -------------------------------------------------------------------------------- /4.4.1.md: -------------------------------------------------------------------------------- 1 | ### 4.4.1 示例代码 2 | 3 | 使用服务的风险和对策取决于服务的使用方式。 您可以通过下面展示的图表找出您应该创建的服务类型。 由于安全编码的最佳实践,根据服务的创建方式而有所不同,因此我们也将解释服务的实现。 4 | 5 | 表 4.4-1 服务类型的定义 6 | 7 | | 类型 | 定义 | 8 | | --- | --- | 9 | | 私有 | 不能由其他应用加载,所以是最安全的服务 | 10 | | 公共 | 应该由很多未指定的应用使用的服务 | 11 | | 伙伴 | 只能由可信的伙伴公司开发的应用使用的服务 | 12 | | 内部 | 只能由其他内部应用使用的服务 | 13 | 14 | ![](img/4-4-1.jpg) 15 | 16 | 有几种服务实现方法,您将选择匹配您想要创建的服务类型的方法。 表中列的条目展示了实现方法,并将它们分为 5 种类型。 “OK”表示可能的组合,其他表示不可能/困难的组合。 17 | 18 | 服务的详细实现方法,请参阅“4.4.3.2 如何实现服务”和每个服务类型的示例代码(在表中带有`*`标记)。 19 | 20 | 表 4.4-2 21 | 22 | | 类别 | 私有服务 | 公共服务 | 伙伴服务 | 内部服务 | 23 | | --- | --- | --- | --- | --- | 24 | | `startService`类型 | OK\* | OK | - | OK | 25 | | `IntentService`类型 | OK | OK\* | - | OK | 26 | | 本地绑定类型 | OK | - | - | - | 27 | | `Messenger`绑定类型 | OK | OK | - | OK\* | 28 | | AIDL 绑定类型 | OK | OK | OK\* | OK | 29 | 30 | 每种服务安全类型的示例代码展示在下面,通过表 4.4-2 中的使用`*`标记。 31 | -------------------------------------------------------------------------------- /4.4.2.md: -------------------------------------------------------------------------------- 1 | ### 4.4.2 规则书 2 | 3 | 实现或使用服务时,遵循下列规则。 4 | 5 | #### 4.4.2.1 仅仅在应用中使用的服务,必须设为私有(必需) 6 | 7 | 仅在应用(或同一个 UID)中使用的服务必须设置为“私有”。 它避免了应用意外地从其他应用接收意图,并最终防止应用的功能被使用,或应用的行为变得异常。 8 | 9 | 在`AndroidManifest.xml`中定义服务时,你在必须将导出属性设置为`false`。 10 | 11 | AndroidManifest.xml 12 | 13 | ```xml 14 | 15 | 16 | 17 | ``` 18 | 19 | 另外,这种情况很少见,但是当服务仅在应用中使用时,不要设置意图过滤器。原因是,由于意图过滤器的特性,可能会意外调用其他应用中的公共服务,虽然你打算调用应用内的私有服务。 20 | 21 | AndroidManifest.xml(不推荐) 22 | 23 | ```xml 24 | 25 | 26 | 27 | 28 | B`的顺序安装。在这种情况下,当应用 C 发送隐式意图时,私有服务(A-1)调用失败。 另一方面,由于应用 A 可以通过隐式意图,按照预期成功调用应用内的私有服务,因此在安全性(恶意软件的对策)方面不会有任何问题。 33 | 34 | ![](img/4-4-5.jpg) 35 | 36 | 图 4.4-6 展示了一个场景,应用以`B->A`的顺序安装。 就安全性而言,这里存在一个问题,应用 A 尝试通过发送隐式意图来,调用应用中的私有服务,但实际上调用了之前安装的应用 B 中的公共活动(B-1)。 由于这个漏洞,敏感信息可能会从应用 A 发送到应用 B。 如果应用 B 是恶意软件,它会导致敏感信息的泄漏。 37 | 38 | ![](img/4-4-6.jpg) 39 | 40 | 41 | 如上所示,使用意图过滤器向私有服务发送隐式意图,可能会导致意外行为,因此最好避免此设置。 42 | 43 | #### 4.4.3.2 如何实现服务 44 | 45 | 由于实现服务的方法是多种多样的,应该按安全类型进行选择,它由示例代码分类,本文对各个特性进行了简要说明。 它大致分为使用`startService`和使用`bindService`的情况。 还可以创建在`startService`和`bindService`中都可以使用的服务。 应该调查以下项目来确定服务的实现方法。 46 | 47 | + 是否将服务公开给其他应用(服务的公开) 48 | + 是否在运行中交换数据(相互发送/接收数据) 49 | + 是否控制服务(启动或完成) 50 | + 是否作为另一个进程执行(进程间通信) 51 | + 是否并行执行多个进程(并行进程) 52 | 53 | 表 4.4-3 显示了每个条目的实现方法类别和可行性。 “NG”代表不可能的情况,或者需要另一个框架的情况,它与所提供的函数不同。 54 | 55 | 表 4.4-4 服务的实现方法分类 56 | 57 | | 类别 | 服务公开 | 相互发送/接收数据 | 控制服务 | 进程间通信 | 并行进程 | 58 | | --- | --- | --- | --- | --- | --- | 59 | | `startService`类型 | OK | NG | OK | OK | NG | 60 | | `IntentService`类型 | OK | NG | NG | OK | NG | 61 | | 本地绑定类型 | NG | OK | OK | NG | NG | 62 | | `Messenger`绑定类型 | OK | OK | OK | OK | NG | 63 | | AIDL 绑定类型 | OK | OK | OK | OK | OK | 64 | 65 | `startService`类型 66 | 67 | 这是最基本的服务。 它继承了`Service`类,并通过`onStartCommand`执行过程。 68 | 69 | 在用户方,服务由意图指定,并通过`startService`调用。 由于结果等数据无法直接返回给源意图,因此应与其他方法(如广播)结合使用。 具体示例请参考“4.4.1.1 创建/使用私有服务”。 70 | 71 | 安全性检查应该由`onStartCommand`完成,但不能用于伙伴服务,因为无法获取来源的软件包名称。 72 | 73 | `IntentService`类型 74 | 75 | `IntentService`是通过继承`Service`创建的类。 调用方法与`startService`类型相同。 以下是与标准服务(`startService`类型)相比较的特征。 76 | 77 | + 意图的处理由`onHandleIntent`完成(不使用`onStartCommand`)。 78 | + 由另一个线程执行。 79 | + 过程将排队。 80 | 81 | 由于过程是由另一个线程执行的,因此调用会立即返回,并且面向意图的过程由队列系统顺序执行。 每个意图并不是并行处理的,但根据产品的要求,它也可以作为选项来选择,来简化实现。由于结果等数据不能返回给源意图,因此应该与其他方法(如广播)结合使用。 具体实例请参考“4.4.1.2 创建/使用公共服务”。 82 | 83 | 安全性检查应该由`onHandleIntent`来完成,但不能用于伙伴服务,因为无法获取来源的包名称。 84 | 85 | 本地绑定类型 86 | 87 | 这是一种实现本地服务的方法,它仅工作在与应用相同的过程中。 将类定义为从`Binder`类派生的类,并准备将`Service`中实现的特性(方法)提供给调用方。 88 | 89 | 在用户方,服务由意图指定并使用`bindService`调用。 这是绑定服务的所有方法中最简单的实现,但它的用途有限,因为它不能被其他进程启动,并且服务也不能公开。 具体实现示例,请参阅示例代码中包含的项目“`PrivateServiceLocalBind`服务”。 90 | 91 | 从安全角度来看,只能实现私有服务。 92 | 93 | `Messenger`绑定类型 94 | 95 | 这是一种方法,通过使用`Messenger`系统来实现与服务的链接。 96 | 97 | 由于`Messenger`可以提供为来自服务用户方的`Message`目标,因此可以相对容易地实现数据交换。 另外,由于过程要进行排队,因此它具有“线程安全”的特性。每个过程不可能并行,但根据产品的要求,它也可以作为选项来选择,来简化实现。 在用户端,服务由意图指定,通过`bindService`调用,具体实现示例请参见“4.4.1.4 创建/使用内部服务”。 98 | 99 | 安全检查需要在`onBind`或`Message Handler`中进行,但不能 用于伙伴服务,因为无法获取来源的包名称。 100 | 101 | AIDL 绑定类型 102 | 103 | 这是一种方法,通过使用 AIDL 系统实现与服务的链接。 接口通过 AIDL 定义,并将服务拥有的特性提供为方法。 另外,回调也可以通过在用户端实现由 AIDL 定义的接口来实现,多线程调用是可能的,但有必要在服务端明确实现互斥。 104 | 105 | 用户端可以通过指定意图并使用`bindService`来调用服务。 具体实现示例请参考“4.4.1.3 创建/使用伙伴服务”。 106 | 107 | 安全性检查必须在`onBind`中为内部服务执行,以及由 AIDL 为伙伴服务定义的接口的每种方法执行。 108 | 109 | 这可以用于本指南中描述的所有安全类型的服务。 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /4.4.md: -------------------------------------------------------------------------------- 1 | ## 4.4 创建/使用服务 2 | 3 | -------------------------------------------------------------------------------- /4.5.1.md: -------------------------------------------------------------------------------- 1 | ### 4.5.1 示例代码 2 | 3 | #### 4.5.1.1 创建/操作数据库 4 | 5 | 在 Android 应用中处理数据库时,可以通过使用`SQLiteOpenHelper` [10] 来实现数据库文件的适当安排和访问权限设置(拒绝其他应用访问的设置)。 下面是一个简单的应用示例,它在启动时创建数据库,并通过 UI 执行搜索/添加/更改/删除数据。 示例代码完成了 SQL 注入的防范,来避免来自外部的输入执行不正确的 SQL。 6 | 7 | > [10] 对于文件存储,可以将绝对文件路径指定为`SQLiteOpenHelper`构造函数的第二个参数(名称)。 因此,如果指定了 SD 卡路径,则需要注意,存储的文件可以被其他应用读取和写入。 8 | 9 | ![](img/4-5-1.jpg) 10 | 11 | 1) `SQLiteOpenHelper`应该用于创建数据库。 12 | 13 | 2) 使用占位符。 14 | 15 | 3) 根据应用要求验证输入值。 16 | 17 | SampleDbOpenHelper.java 18 | 19 | ```java 20 | package org.jssec.android.sqlite; 21 | 22 | import android.content.Context; 23 | import android.database.SQLException; 24 | import android.database.sqlite.SQLiteDatabase; 25 | import android.database.sqlite.SQLiteOpenHelper; 26 | import android.util.Log; 27 | import android.widget.Toast; 28 | 29 | public class SampleDbOpenHelper extends SQLiteOpenHelper { 30 | 31 | private SQLiteDatabase mSampleDb; //Database to store the data to be handled 32 | 33 | public static SampleDbOpenHelper newHelper(Context context) { 34 | //*** POINT 1 *** SQLiteOpenHelper should be used for database creation. 35 | return new SampleDbOpenHelper(context); 36 | } 37 | 38 | public SQLiteDatabase getDb() { 39 | return mSampleDb; 40 | } 41 | 42 | //Open DB by Writable mode 43 | public void openDatabaseWithHelper() { 44 | try { 45 | if (mSampleDb != null && mSampleDb.isOpen()) { 46 | if (!mSampleDb.isReadOnly())// Already opened by writable mode 47 | return; 48 | mSampleDb.close(); 49 | } 50 | mSampleDb = getWritableDatabase(); //It's opened here. 51 | } catch (SQLException e) { 52 | //In case fail to construct database, output to log 53 | Log.e(mContext.getClass().toString(), mContext.getString(R.string.DATABASE_OPEN_ERROR_MESSAGE)); 54 | Toast.makeText(mContext, R.string.DATABASE_OPEN_ERROR_MESSAGE, Toast.LENGTH_LONG).show(); 55 | } 56 | } 57 | 58 | //Open DB by ReadOnly mode. 59 | public void openDatabaseReadOnly() { 60 | try { 61 | if (mSampleDb != null && mSampleDb.isOpen()) { 62 | if (mSampleDb.isReadOnly())// Already opened by ReadOnly. 63 | return; 64 | mSampleDb.close(); 65 | } 66 | SQLiteDatabase.openDatabase(mContext.getDatabasePath(CommonData.DBFILE_NAME).getPath(), null, SQLiteDatabase.OPEN_READONLY); 67 | } catch (SQLException e) { 68 | //In case failed to construct database, output to log 69 | Log.e(mContext.getClass().toString(), mContext.getString(R.string.DATABASE_OPEN_ERROR_MESSAGE)); 70 | Toast.makeText(mContext, R.string.DATABASE_OPEN_ERROR_MESSAGE, Toast.LENGTH_LONG).show(); 71 | } 72 | } 73 | 74 | //Database Close 75 | public void closeDatabase() { 76 | try { 77 | if (mSampleDb != null && mSampleDb.isOpen()) { 78 | mSampleDb.close(); 79 | } 80 | } catch (SQLException e) { 81 | //In case failed to construct database, output to log 82 | Log.e(mContext.getClass().toString(), mContext.getString(R.string.DATABASE_CLOSE_ERROR_MESSAGE)); 83 | Toast.makeText(mContext, R.string.DATABASE_CLOSE_ERROR_MESSAGE, Toast.LENGTH_LONG).show(); 84 | } 85 | } 86 | 87 | //Remember Context 88 | private Context mContext; 89 | //Table creation command 90 | private static final String CREATE_TABLE_COMMANDS 91 | = "CREATE TABLE " + CommonData.TABLE_NAME + " (" 92 | + "_id INTEGER PRIMARY KEY AUTOINCREMENT, " 93 | + "idno INTEGER UNIQUE, " 94 | + "name VARCHAR(" + CommonData.TEXT_DATA_LENGTH_MAX + ") NOT NULL, " 95 | + "info VARCHAR(" + CommonData.TEXT_DATA_LENGTH_MAX + ")" 96 | + ");"; 97 | 98 | public SampleDbOpenHelper(Context context) { 99 | super(context, CommonData.DBFILE_NAME, null, CommonData.DB_VERSION); 100 | mContext = context; 101 | } 102 | 103 | @Override 104 | public void onCreate(SQLiteDatabase db) { 105 | try { 106 | db.execSQL(CREATE_TABLE_COMMANDS); //Execute DB construction command 107 | } catch (SQLException e) { 108 | //In case failed to construct database, output to log 109 | Log.e(this.getClass().toString(), mContext.getString(R.string.DATABASE_CREATE_ERROR_MESSAGE)); 110 | } 111 | } 112 | 113 | @Override 114 | public void onUpgrade(SQLiteDatabase arg0, int arg1, int arg2) { 115 | // It's to be executed when database version up. Write processes like data transition. 116 | } 117 | } 118 | ``` 119 | 120 | DataSearchTask.java(SQLite 数据库项目) 121 | 122 | ```java 123 | package org.jssec.android.sqlite.task; 124 | 125 | import org.jssec.android.sqlite.CommonData; 126 | import org.jssec.android.sqlite.DataValidator; 127 | import org.jssec.android.sqlite.MainActivity; 128 | import org.jssec.android.sqlite.R; 129 | import android.database.Cursor; 130 | import android.database.SQLException; 131 | import android.database.sqlite.SQLiteDatabase; 132 | import android.os.AsyncTask; 133 | import android.util.Log; 134 | 135 | //Data search task 136 | public class DataSearchTask extends AsyncTask { 137 | 138 | private MainActivity mActivity; 139 | private SQLiteDatabase mSampleDB; 140 | 141 | public DataSearchTask(SQLiteDatabase db, MainActivity activity) { 142 | mSampleDB = db; 143 | mActivity = activity; 144 | } 145 | 146 | @Override 147 | protected Cursor doInBackground(String... params) { 148 | String idno = params[0]; 149 | String name = params[1]; 150 | String info = params[2]; 151 | String cols[] = {"_id", "idno","name","info"}; 152 | Cursor cur; 153 | //*** POINT 3 *** Validate the input value according the application requirements. 154 | if (!DataValidator.validateData(idno, name, info)){ 155 | return null; 156 | } 157 | //When all parameters are null, execute all search 158 | if ((idno == null || idno.length() == 0) && 159 | (name == null || name.length() == 0) && 160 | (info == null || info.length() == 0) ) { 161 | try { 162 | cur = mSampleDB.query(CommonData.TABLE_NAME, cols, null, null, null, null, null); 163 | } catch (SQLException e) { 164 | Log.e(DataSearchTask.class.toString(), mActivity.getString(R.string.SEARCHING_ERROR_MESSAGE)); 165 | return null; 166 | } 167 | return cur; 168 | } 169 | //When No is specified, execute searching by No 170 | if (idno != null && idno.length() > 0) { 171 | String selectionArgs[] = {idno}; 172 | try { 173 | //*** POINT 2 *** Use place holder. 174 | cur = mSampleDB.query(CommonData.TABLE_NAME, cols, "idno = ?", selectionArgs, null, null, null); 175 | } catch (SQLException e) { 176 | Log.e(DataSearchTask.class.toString(), mActivity.getString(R.string.SEARCHING_ERROR_MESSAGE)); 177 | return null; 178 | } 179 | return cur; 180 | } 181 | //When Name is specified, execute perfect match search by Name 182 | if (name != null && name.length() > 0) { 183 | String selectionArgs[] = {name}; 184 | try { 185 | //*** POINT 2 *** Use place holder. 186 | cur = mSampleDB.query(CommonData.TABLE_NAME, cols, "name = ?", selectionArgs, null, null, null); 187 | } catch (SQLException e) { 188 | Log.e(DataSearchTask.class.toString(), mActivity.getString(R.string.SEARCHING_ERROR_MESSAGE)); 189 | return null; 190 | } 191 | return cur; 192 | } 193 | //Other than above, execute partly match searching with the condition of info. 194 | String argString = info.replaceAll("@", "@@"); //Escape $ in info which was received as input. 195 | argString = argString.replaceAll("%", "@%"); //Escape % in info which was received as input. 196 | argString = argString.replaceAll("_", "@_"); //Escape _ in info which was received as input. 197 | String selectionArgs[] = {argString}; 198 | try { 199 | //*** POINT 2 *** Use place holder. 200 | cur = mSampleDB.query(CommonData.TABLE_NAME, cols, "info LIKE '%' || ? || '%' ESCAPE '@'", selectionArgs, null, null, null); 201 | } catch (SQLException e) { 202 | Log.e(DataSearchTask.class.toString(), mActivity.getString(R.string.SEARCHING_ERROR_MESSAGE)); 203 | return null; 204 | } 205 | return cur; 206 | } 207 | 208 | @Override 209 | protected void onPostExecute(Cursor resultCur) { 210 | mActivity.updateCursor(resultCur); 211 | } 212 | } 213 | ``` 214 | 215 | DataValidator.java 216 | 217 | ```java 218 | package org.jssec.android.sqlite; 219 | 220 | public class DataValidator { 221 | 222 | //Validate the Input value 223 | //validate numeric characters 224 | public static boolean validateNo(String idno) { 225 | //null and blank are OK 226 | if (idno == null || idno.length() == 0) { 227 | return true; 228 | } 229 | //Validate that it's numeric character. 230 | try { 231 | if (!idno.matches("[1-9][0-9]*")) { 232 | //Error if it's not numeric value 233 | return false; 234 | } 235 | } catch (NullPointerException e) { 236 | //Detected an error 237 | return false; 238 | } 239 | return true; 240 | } 241 | 242 | // Validate the length of a character string 243 | public static boolean validateLength(String str, int max_length) { 244 | //null and blank are OK 245 | if (str == null || str.length() == 0) { 246 | return true; 247 | } 248 | //Validate the length of a character string is less than MAX 249 | try { 250 | if (str.length() > max_length) { 251 | //When it's longer than MAX, error 252 | return false; 253 | } 254 | } catch (NullPointerException e) { 255 | //Bug 256 | return false; 257 | } 258 | return true; 259 | } 260 | 261 | // Validate the Input value 262 | public static boolean validateData(String idno, String name, String info) { 263 | if (!validateNo(idno)) { 264 | return false; 265 | } 266 | if (!validateLength(name, CommonData.TEXT_DATA_LENGTH_MAX)) { 267 | return false; 268 | }else if(!validateLength(info, CommonData.TEXT_DATA_LENGTH_MAX)) { 269 | return false; 270 | } 271 | return true; 272 | } 273 | } 274 | ``` 275 | -------------------------------------------------------------------------------- /4.5.2.md: -------------------------------------------------------------------------------- 1 | ### 4.5.2 规则书 2 | 3 | 使用 SQLite 时,遵循以下规则: 4 | 5 | #### 4.5.2.1 正确设置 DB 文件位置和访问权限(必需) 6 | 7 | 考虑到 DB 文件数据的保护,DB 文件位置和访问权限设置是需要一起考虑的非常重要的因素。 例如,即使正确设置了文件访问权,如果 DB 文件位于无法设置访问权的位置,则任何人可以访问 DB 文件,例如, SD 卡。 如果它位于应用目录中,如果访问权限设置不正确,它最终将允许意外访问。 以下是正确分配和访问权限设置的一些要点,以及实现它们的方法。 为了保护数据库文件(数据),对于位置和访问权限设置,需要执行以下两点。 8 | 9 | 1) 位置 10 | 11 | 位于可以由`Context#getDatabasePath(String name)`获取的文件路径,或者在某些情况下,可以由`Context#getFilesDir11`获取的目录。 12 | 13 | 2) 访问权限 14 | 15 | 设置为`MODE_PRIVATE`(只能由创建文件的应用访问)模式。 16 | 17 | 通过执行以下2点,即可 18 | 创建其他应用无法访问的 DB 文件。 以下是执行它们的一些方法。 19 | 20 | 1. 使用`SQLiteOpenHelper` 21 | 2. 使用`Context#openOrCreateDatabase` 22 | 23 | 创建 DB 文件时,可以使用`SQLiteDatabase#openOrCreateDatabase`。 但是,使用此方法时,可以在某些 Android 智能手机设备中创建可从其他应用读取的 DB 文件。 所以建议避免这种方法,并使用其他方法。 上述量种方法的每个特征如下 [11] 24 | 25 | > [11] 这两种方法都提供了(包)目录下的路径,只能由指定的应用读取和写入。 26 | 27 | 使用`SQLiteOpenHelper` 28 | 29 | 当使用`SQLiteOpenHelper`时,开发人员不需要担心很多事情。 创建一个从`SQLiteOpenHelper`派生的类,并为构造器的参数指定DB名称(用于文件名)[12],然后满足上述安全要求的 DB 文件会自动创建。 30 | 31 | > [12] (未在 Android 参考中记录)由于可以在`SQLiteOpenHelper`实现中,将完整文件路径指定为数据库名称,因此需要注意无意中指定不能控制访问权限的地方(路径)(例如 SD 卡)。 32 | 33 | 对于如何使用,请参阅“4.5.1.1 创建/操作数据库”的具体使用方法。 34 | 35 | 使用`Context#openOrCreateDatabase` 36 | 37 | 使用`Context#openOrCreateDatabase`方法创建数据库时,文件访问权应由选项指定,在这种情况下,请明确指定`MODE_PRIVATE`。 38 | 39 | 对于文件安排,数据库名称(用于文件名)可以像`SQLiteOpenHelper`一样指定,文件将在满足上述安全要求的文件路径中自动创建。 但是,也可以指定完整路径,因此有必要注意指定 SD 卡时,即使指定`MODE_PRIVATE`,其他应用也可以访问。 40 | 41 | 42 | 43 | MainActivity.java(显式设定 DB 访问权的示例) 44 | 45 | ```java 46 | public void onCreate(Bundle savedInstanceState) { 47 | super.onCreate(savedInstanceState); 48 | setContentView(R.layout.main); 49 | //Construct database 50 | try { 51 | //Create DB by setting MODE_PRIVATE 52 | db = Context.openOrCreateDatabase("Sample.db", MODE_PRIVATE, null); 53 | } catch (SQLException e) { 54 | //In case failed to construct DB, log output 55 | Log.e(this.getClass().toString(), getString(R.string.DATABASE_OPEN_ERROR_MESSAGE)); 56 | return; 57 | } 58 | //Omit other initial process 59 | } 60 | ``` 61 | 62 | 访问权限有三种可能的设置:`MODE_PRIVATE`,`MODE_WORLD_READABLE`和`MODE_WORLD_WRITEABLE`。 这些常量可以由或运算符一起指定。 但是,除`API_PRIVATE`之外的所有设置,都将在 API 级别 17 和更高版本中被弃用,并且会在 API 级别 24 和更高版本中导致安全异常。 即使对于 API 级别 15 及更早版本的应用,通常最好不要使用这些标志 [13]。 63 | 64 | > [13] `MODE_WORLD_READABLE`和`MODE_WORLD_WRITEABLE`的更多信息,以及其使用的注意事项,请参见“4.6.3.2 访问目录的权限设置”。 65 | 66 | + `MODE_PRIVATE`只有创建者应用可以读写 67 | + `MODE_WORLD_READABLE`创建者应用可以读写,其他人只能读 68 | + `MODE_WORLD_WRITEABLE`创建者应用可以读写,其他人只能写 69 | 70 | #### 4.5.2.2 与其它应用共享 DB 数据时,将内容供应器用于访问控制(必需) 71 | 72 | 与其他应用共享 DB 数据的方法是,将 DB 文件创建为`WORLD_READABLE`,`WORLD_WRITEABLE`,以便其他应用直接访问。 但是,此方法不能限制访问或操作数据库的应用,因此数据可以由非预期的一方(应用)读或写。 因此,可以认为数据的机密性或一致性方面可能会出现一些问题,或者可能成为恶意软件的攻击目标。 73 | 74 | 如上所述,在 Android 中与其他应用共享数据库数据时,强烈建议使用内容供应器。 内容供应器存在一些优点,不仅从安全的角度来实现对 DB 的访问控制,而且从设计角度来看, DB 纲要结构可以隐藏到内容中。 75 | 76 | #### 4.5.2.3 在 DB 操作期间处理变量参数时,必需使用占位符(必需) 77 | 78 | 在防止 SQL 注入的意义上,将任意输入值并入 SQL 语句时,应使用占位符。 下面有两个方法用占位符执行 SQL。 79 | 80 | 1. 使用`SQLiteDatabase#compileStatement()`,获取`SQLiteStatement`,然后使用`SQLiteStatement#bindString()`或`bindLong()`等,将参数放置到占位符之后。 81 | 2. 在`SQLiteDatabese`类上调用`execSQL()`,`insert()`,`update()`,`delete()`,`query()`,`rawQuery()`和`replace()`时,使用具有占位符的 SQL 语句。 82 | 83 | 另外,通过使用`SQLiteDatabase#compileStatement()`执行`SELECT`命令时,存在“仅获取第一个元素作为`SELECT`命令的结果”的限制,所以用法是有限的。 84 | 85 | 在任何一种方法中,提供给占位符的数据内容最好根据应用要求事先检查。 以下是每种方法的进一步解释。 86 | 87 | 使用`SQLiteDatabase#compileStatement()`: 88 | 89 | 数据以下列步骤提供给占位符: 90 | 91 | 1. 使用`SQLiteDatabase#compileStatement()`获取包含占位符的 SQL 语句,如`SQLiteStatement`。 92 | 2. 使用`bindLong()`和`bindString()`方法为创建的`SQLiteStatement`对象设置占位符。 93 | 3. 通过`ExecSQLiteStatement`对象的`execute()`方法执行 SQL。 94 | 95 | DataInsertTask.java(占位符的用例): 96 | 97 | ```java 98 | //Adding data task 99 | public class DataInsertTask extends AsyncTask { 100 | 101 | private MainActivity mActivity; 102 | private SQLiteDatabase mSampleDB; 103 | 104 | public DataInsertTask(SQLiteDatabase db, MainActivity activity) { 105 | mSampleDB = db; 106 | mActivity = activity; 107 | } 108 | 109 | @Override 110 | protected Void doInBackground(String... params) { 111 | String idno = params[0]; 112 | String name = params[1]; 113 | String info = params[2]; 114 | //*** POINT 3 *** Validate the input value according the application requirements. 115 | if (!DataValidator.validateData(idno, name, info)) { 116 | return null; 117 | } 118 | // Adding data task 119 | //*** POINT 2 *** Use place holder 120 | String commandString = "INSERT INTO " + CommonData.TABLE_NAME + " (idno, name, info) VALUES (?, ?, ?)"; 121 | SQLiteStatement sqlStmt = mSampleDB.compileStatement(commandString); 122 | sqlStmt.bindString(1, idno); 123 | sqlStmt.bindString(2, name); 124 | sqlStmt.bindString(3, info); 125 | try { 126 | sqlStmt.executeInsert(); 127 | } catch (SQLException e) { 128 | Log.e(DataInsertTask.class.toString(), mActivity.getString(R.string.UPDATING_ERROR_MESSAGE)); 129 | } finally { 130 | sqlStmt.close(); 131 | } 132 | return null; 133 | } 134 | 135 | [...] 136 | } 137 | ``` 138 | 139 | 这是一种类型,它预先创建作为对象执行的 SQL 语句,并将参数分配给它。 执行的过程是固定的,所以没有发生 SQL 注入的可能。 另外,通过重用`SQLiteStatement`对象可以提高流程效率。 140 | 141 | 使用`SQLiteDatabase`提供的每个方法: 142 | 143 | `SQLiteDatabase`提供了两种类型的数据库操作方法。 一种是使用 SQL 语句,另一种是不使用 SQL 语句。 使用 SQL 语句的方法是`SQLiteDatabase#execSQL()`/`rawQuery()`,它以以下步骤执行。 144 | 145 | 1) 准备包含占位符的 SQL 语句。 146 | 147 | 2) 创建要分配给占位符的数据。 148 | 149 | 3) 传递 SQL 语句和数据作为参数,并为每个过程执行一个方法。 150 | 151 | 另一方面,`SQLiteDatabase#insert()/update()/delete()/query()/replace()`是不使用 SQL 语句的方法。当使用它们时,数据应该按照以下步骤来准备。 152 | 153 | 1) 如果有数据要插入/更新到数据库,请注册到`ContentValues`。 154 | 155 | 2) 传递`ContentValues`作为参数,并为每个过程执行一个方法(例如,`SQLiteDatabase#insert()`) 156 | 157 | `SQLiteDatabase#insert()`(每个过程的方法的用例): 158 | 159 | 160 | ```java 161 | private SQLiteDatabase mSampleDB; 162 | private void addUserData(String idno, String name, String info) { 163 | //Validity check of the value(Type, range), escape process 164 | if (!validateInsertData(idno, name, info)) { 165 | //If failed to pass the validation, log output 166 | Log.e(this.getClass().toString(), getString(R.string.VALIDATION_ERROR_MESSAGE)); 167 | return; 168 | } 169 | //Prepare data to insert 170 | ContentValues insertValues = new ContentValues(); 171 | insertValues.put("idno", idno); 172 | insertValues.put("name", name); 173 | insertValues.put("info", info); 174 | //Execute Inser 175 | try { 176 | mSampleDb.insert("SampleTable", null, insertValues); 177 | } catch (SQLException e) { 178 | Log.e(this.getClass().toString(), getString(R.string.DB_INSERT_ERROR_MESSAGE)); 179 | return; 180 | } 181 | } 182 | ``` 183 | 184 | 在这个例子中,SQL 命令不是直接写入,而是使用`SQLiteDatabase`提供的插入方法。 SQL 命令没有直接使用,所以在这种方法中也没有 SQL 注入的可能。 185 | 186 | -------------------------------------------------------------------------------- /4.5.3.md: -------------------------------------------------------------------------------- 1 | ### 4.5.3 高级话题 2 | 3 | #### 4.5.3.1 在 SQL 语句的`LIKE`断言中使用通配符时,应该实现转义过程 4 | 5 | 当所使用的字符串包含`LIKE`断言的通配符(`%`,`_`),作为占位符的输入值时,除非处理正确,否则它将用作通配符,因此必须根据需要事先转义处理。 通配符应该用作单个字符(`%`或`_`)时,需要转义处理。 6 | 7 | 根据下面的示例代码,使用`ESCAPE`子句执行实际的转义过程。 8 | 9 | 使用`LIKE`情况下的`ESCAPE`过程: 10 | 11 | ```java 12 | //Data search task 13 | public class DataSearchTask extends AsyncTask { 14 | 15 | private MainActivity mActivity; 16 | private SQLiteDatabase mSampleDB; 17 | private ProgressDialog mProgressDialog; 18 | 19 | public DataSearchTask(SQLiteDatabase db, MainActivity activity) { 20 | mSampleDB = db; 21 | mActivity = activity; 22 | } 23 | 24 | @Override 25 | protected Cursor doInBackground(String... params) { 26 | String idno = params[0]; 27 | String name = params[1]; 28 | String info = params[2]; 29 | String cols[] = {"_id", "idno","name","info"}; 30 | Cursor cur; 31 | 32 | [...] 33 | 34 | //Execute like search(partly match) with the condition of info 35 | //Point:Escape process should be performed on characters which is applied to wild card 36 | String argString = info.replaceAll("@", "@@"); // Escape $ in info which was received as input 37 | argString = argString.replaceAll("%", "@%"); // Escape % in info which was received as input 38 | argString = argString.replaceAll("_", "@_"); // Escape _ in info which was received as input 39 | String selectionArgs[] = {argString}; 40 | try { 41 | //Point:Use place holder 42 | cur = mSampleDB.query("SampleTable", cols, "info LIKE '%' || ? || '%' ESCAPE '@'", selectionArgs, null, null, null); 43 | } catch (SQLException e) { 44 | Toast.makeText(mActivity, R.string.SERCHING_ERROR_MESSAGE, Toast.LENGTH_LONG).show(); 45 | return null; 46 | } 47 | return cur; 48 | } 49 | 50 | @Override 51 | protected void onPostExecute(Cursor resultCur) { 52 | mProgressDialog.dismiss(); 53 | mActivity.updateCursor(resultCur); 54 | } 55 | } 56 | ``` 57 | 58 | #### 4.5.3.2 不能用占位符时,在 SQL 命令中使用外部输入 59 | 60 | 当执行 SQL 语句,并且过程目标是 DB 对象,如表的创建/删除时,占位符不能用于表名的值。 基本上,数据库不应该使用外部输入的任意字符串来设计,以防占位符不能用于该值。 61 | 62 | 当由于规范或特性的限制,而无法使用占位符时,无论输入值是否危险,都应在执行前进行验证,并且需要执行必要的过程。 63 | 64 | 基本上,应该执行: 65 | 66 | 1. 使用字符串参数时,应该对于字符进行转义或引用处理。 67 | 2. 使用数字值参数时,请确认不包含数值以外的字符。 68 | 3. 用作标识符或命令时,请验证是否包含不能使用的字符以及(1)。 69 | 70 | 参考: (日文) 71 | 72 | #### 4.5.3.3 采取数据库非预期覆盖的对策 73 | 74 | 通过`SQLiteOpenHelper#getReadableDatabase`或`getWriteableDatabase`获取数据库实例时,通过使用任一方法 [14],DB 将以可读/可写状态打开。 另外,与`Context#openOrCreateDatabase`,`SQLiteDatabase#openOrCreateDatabase`相同。这意味着 DB 的内容可能会被应用操作,或实现中的缺陷意外覆盖。 基本上,它可以由应用规范和实现范围来支持,但是当实现仅需要读取功能的功能(如应用的搜索功能等)时,通过只读方式打开数据库,可能会简化设计或检查,从而提高应用质量,因此建议视情况而定。 75 | 76 | > [14] `getReableDatabase()`和`getWritableDatabase`可能返回同一个对象。 它的规范是,如果可写对象由于磁盘满了而无法生成,它将返回只读对象。 (`getWritableDatabase()`会在磁盘满了的情况下产生错误) 77 | 78 | 特别是,通过对`SQLiteDatabase#openDatabase`指定`OPEN_READONLY`打开数据库。 79 | 80 | 以只读打开数据库: 81 | 82 | ```java 83 | [...] 84 | // Open DB(DB should be created in advance) 85 | SQLiteDatabase db 86 | = SQLiteDatabase.openDatabase(SQLiteDatabase.getDatabasePath("Sample.db"), null, OPEN_READONLY); 87 | ``` 88 | 89 | 参考: 90 | 91 | #### 4.5.3.4 根据应用需求,验证 DB 的输入输出数据的有效性 92 | 93 | SQLite 是类型容错的数据库,它可以将字符类型数据存储到在 DB 中声明为整数的列中。 对于数据库中的数据,包括数值类型的所有数据都作为纯文本的字符数据存储在数据库中。 所以搜索字符串类型,可以对整数类型的列执行(`LIKE '%123%'`等)。此外,由于在某些情况下,可以输入超过限制的数据,所以对 SQLite 中的值(有效性验证)的限制是不可信的,例如`VARCHAR(100)`。 94 | 95 | 因此,使用 SQLite 的应用需要非常小心 DB 的这种特性,并且有必要根据应用需求采取措施,不要将意外的数据存储到数据库,或不要获取意外的数据。 对策是以下两点。 96 | 97 | 1. 在数据库中存储数据时,请确认类型和长度是否匹配。 98 | 2. 从数据库中获取值时,验证数据是否超出假定的类型和长度。 99 | 100 | 下面是个代码示例,它验证了输入值是否大于 1。 101 | 102 | ```java 103 | public class MainActivity extends Activity { 104 | 105 | [...] 106 | 107 | //Process for adding 108 | private void addUserData(String idno, String name, String info) { 109 | //Check for No 110 | if (!validateNo(idno, CommonData.REQUEST_NEW)) { 111 | return; 112 | } 113 | //Inserting data process 114 | DataInsertTask task = new DataInsertTask(mSampleDbyhis); 115 | task.execute(idno, name, info); 116 | } 117 | 118 | [...] 119 | 120 | private boolean validateNo(String idno, int request) { 121 | if (idno == null || idno.length() == 0) { 122 | if (request == CommonData.REQUEST_SEARCH) { 123 | //When search process, unspecified is considered as OK. 124 | return true; 125 | } else { 126 | //Other than search process, null and blank are error. 127 | Toast.makeText(this, R.string.IDNO_EMPTY_MESSAGE, Toast.LENGTH_LONG).show(); 128 | return false; 129 | } 130 | } 131 | //Verify that it's numeric character 132 | try { 133 | // Value which is more than 1 134 | if (!idno.matches("[1-9][0-9]*")) { 135 | //In case of not numeric character, error 136 | Toast.makeText(this, R.string.IDNO_NOT_NUMERIC_MESSAGE, Toast.LENGTH_LONG).show(); 137 | return false; 138 | } 139 | } catch (NullPointerException e) { 140 | //It never happen in this case 141 | return false; 142 | } 143 | return true; 144 | } 145 | 146 | [...] 147 | } 148 | ``` 149 | 150 | #### 4.5.3.5 考虑 -- 储存在数据库中的数据 151 | 152 | 在 SQLite 视线中,将数据储存到文件是这样: 153 | 154 | + 所有包含数值类型的数据,都将作为纯文本的字符数据存储在 DB 文件中。 155 | + 执行 DB 的数据删除时,数据本身不会从 DB 文件中删除。 (只添加删除标记。) 156 | + 更新数据时,更新前的数据未被删除,仍保留在数据库文件中。 157 | 158 | 因此,“必须”删除的信息仍可能保留在 DB 文件中。 即使在这种情况下,也要根据本指导手册采取对策,并且启用 Android 安全功能时,数据/文件可能不会被第三方直接访问,包括其他应用。 但考虑到通过绕过 Android 的保护系统(如 root 权限)选取文件的情况,如果存储了对业务有巨大影响的数据,则应考虑不依赖于 Android 保护系统的数据保护。 159 | 160 | 由于上述原因,需要保护的重要数据,不应该存储在 SQLite 数据库中,即使设备取得了 root 权限。 在需要存储重要数据的情况下,有必要采取对策或加密整个数据库。 161 | 162 | 当需要加密时,有许多问题超出了本指南的范围,比如处理用于加密或代码混淆的密钥,所以目前建议,在开发处理数据的应用,数据对业务有巨大影响时咨询专家。 请参考“4.5.3.6 [参考] 加密 SQLite 数据库(Android `SQLCipher`)”,这里介绍加密数据库的库。 163 | 164 | #### 4.5.3.6 [参考] 加密 SQLite 数据库(Android `SQLCipher`) 165 | 166 | `SQLCipher`是为数据库提供透明 256 位 AES 加密的 SQLite 扩展。 它是开源的(BSD 许可证),由 Zetetic LLC 维护/管理。 在移动世界中,`SQLCipher`广泛用于诺基亚/ QT,苹果的 iOS。 167 | 168 | Android 项目的`SQLCipher`旨在支持 Android 环境中的 SQLite 数据库的标准集成加密。 通过为`SQLCipher`创建标准 SQLite 的 API,开发人员可以使用加密的数据库和平常一样的编码。 169 | 170 | 参考:。 171 | 172 | 如何使用: 173 | 174 | 应用开发者可以通过以下三个步骤使用`SQLCipher`。 175 | 176 | 1. 在应用的`lib`目录中找到`sqlcipher.jar`,`libdatabase_sqlcipher.so`,`libsqlcipher_android.so`和`libstlport_shared.so`。 177 | 2. 对于所有源文件,将所有`android.database.sqlite.*`更改为`info.guardianproject.database.sqlite.*`,它们由`import`指定。另外,`android.database.Cursor`可以照原样使用。 178 | 3. 在`onCreate()`中初始化数据库,打开数据库时设置密码。 179 | 180 | 简单的代码示例: 181 | 182 | ```java 183 | SQLiteDatabase.loadLibs(this); // First, Initialize library by using context. 184 | SQLiteOpenHelper.getWRITEABLEDatabase(passwoed): // Parameter is password(Suppose that it's string type and It's got in a secure way.) 185 | ``` 186 | 187 | 在撰写本文时,Android 版`SQLCipher`是 1.1.0 版,现在正在开发 2.0.0 版,现在已经公布了 RC4。 就过去在 Android 中的使用和 API 的稳定性而言,有必要稍后进行验证,但目前还可以看做 SQLite 的加密解决方案,它可以在 Android 中使用。 188 | 189 | 库的结构 190 | 191 | 下列 SDK 中包含的文件是使用`SQLCipher`所必须的。 192 | 193 | + `assets/icudt46l.zip` 2,252KB 194 | 195 | 当`icudt46l.dat`不存在于`/system/usr/icu/`下及其早期版本时,这是必需的。 当找不到`icudt46l.dat`时,此 zip 需要解压缩并使用。 196 | 197 | + `libs/armeabi/libdatabase_sqlcipher.so` 44KB 198 | + `libs/armeabi/libsqlcipher_android.so` 1,117KB 199 | + `libs/armeabi/libstlport_shared.so` 555KB 200 | 201 | 本地库,它在`SQLCipher`首次加载(调用`SQLiteDatabase#loadLibs()`)时被读取。 202 | 203 | + `libs/commons-codec.jar` 46KB 204 | + `libs/guava-r09.jar` 1,116KB 205 | + `libs/sqlcipher.jar` 102KB 206 | 207 | Java 库调用本地库。`sqlcipher.jar`是主要的,其它的由`sqlcipher.jar`引用。 208 | 209 | 总共大约 5.12MB。但是,当`icudt46l.zip`解压时,总共大约 7MB。 210 | -------------------------------------------------------------------------------- /4.5.md: -------------------------------------------------------------------------------- 1 | ## 4.5 使用 SQLite 2 | 3 | 通过使用 SQLite 创建/操作数据库时,在安全性方面有一些警告。 要点是合理设置数据库文件的访问权限,以及 SQL 注入的对策。 允许从外部直接读取/写入数据库文件(在多个应用程序之间共享)的数据库不在此处,假设在内容供应器的后端和应用本身中使用该数据库。 另外,在处理不太多敏感信息的情况下,建议采取下述对策,尽管这里可以处理一定程度的敏感信息。 4 | -------------------------------------------------------------------------------- /4.6.1.1.md: -------------------------------------------------------------------------------- 1 | #### 4.6.1.1 使用私有文件 2 | 3 | 这种情况下使用的文件,只能在同一个应用中读取/写入,并且这是使用文件的一种非常安全的方式。 原则上,无论存储在文件中的信息是否是公开的,尽可能使用私有文件,当与其他应用交换必要的信息时,应该使用另一个 Android 系统(内容供应器,服务)来完成。 4 | 5 | 要点: 6 | 7 | 1) 文件必须在应用目录中创建。 8 | 9 | 2) 文件的访问权限必须设置为私有模式,以免其他应用使用。 10 | 11 | 3) 可以存储敏感信息。 12 | 13 | 4) 对于存储在文件中的信息,请仔细和安全地处理文件数据。 14 | 15 | PrivateFileActivity.java 16 | 17 | ```java 18 | package org.jssec.android.file.privatefile; 19 | import java.io.File; 20 | import java.io.FileInputStream; 21 | import java.io.FileNotFoundException; 22 | import java.io.FileOutputStream; 23 | import java.io.IOException; 24 | import android.app.Activity; 25 | import android.os.Bundle; 26 | import android.view.View; 27 | import android.widget.TextView; 28 | public class PrivateFileActivity extends Activity { 29 | private TextView mFileView; 30 | private static final String FILE_NAME = "private_file.dat"; 31 | 32 | @Override 33 | public void onCreate(Bundle savedInstanceState) { 34 | super.onCreate(savedInstanceState); 35 | setContentView(R.layout.file); 36 | mFileView = (TextView) findViewById(R.id.file_view); 37 | } 38 | 39 | /** 40 | * Create file process 41 | * 42 | * @param view 43 | */ 44 | public void onCreateFileClick(View view) { 45 | FileOutputStream fos = null; 46 | try { 47 | // *** POINT 1 *** Files must be created in application directory. 48 | // *** POINT 2 *** The access privilege of file must be set private mode in order not to be used by other applications. 49 | fos = openFileOutput(FILE_NAME, MODE_PRIVATE); 50 | // *** POINT 3 *** Sensitive information can be stored. 51 | // *** POINT 4 *** Regarding the information to be stored in files, handle file data carefully and securely. 52 | // Omitted, since this is a sample. Please refer to "3.2 Handling Input Data Carefully and Securely." 53 | fos.write(new String("Not sensotive information (File Activity)¥n").getBytes()); 54 | } catch (FileNotFoundException e) { 55 | mFileView.setText(R.string.file_view); 56 | } catch (IOException e) { 57 | android.util.Log.e("PrivateFileActivity", "failed to read file"); 58 | } finally { 59 | if (fos != null) { 60 | try { 61 | fos.close(); 62 | } catch (IOException e) { 63 | android.util.Log.e("PrivateFileActivity", "failed to close file"); 64 | } 65 | } 66 | } 67 | finish(); 68 | } 69 | 70 | /** 71 | * Read file process 72 | * 73 | * @param view 74 | */ 75 | public void onReadFileClick(View view) { 76 | FileInputStream fis = null; 77 | try { 78 | fis = openFileInput(FILE_NAME); 79 | byte[] data = new byte[(int) fis.getChannel().size()]; 80 | fis.read(data); 81 | String str = new String(data); 82 | mFileView.setText(str); 83 | } catch (FileNotFoundException e) { 84 | mFileView.setText(R.string.file_view); 85 | } catch (IOException e) { 86 | android.util.Log.e("PrivateFileActivity", "failed to read file"); 87 | } finally { 88 | if (fis != null) { 89 | try { 90 | fis.close(); 91 | } catch (IOException e) { 92 | android.util.Log.e("PrivateFileActivity", "failed to close file"); 93 | } 94 | } 95 | } 96 | } 97 | 98 | /** 99 | * Delete file process 100 | * 101 | * @param view 102 | */ 103 | public void onDeleteFileClick(View view) { 104 | File file = new File(this.getFilesDir() + "/" + FILE_NAME); 105 | file.delete(); 106 | mFileView.setText(R.string.file_view); 107 | } 108 | } 109 | ``` 110 | 111 | PrivateUserActivity.java 112 | 113 | ```java 114 | package org.jssec.android.file.privatefile; 115 | 116 | import java.io.FileInputStream; 117 | import java.io.FileNotFoundException; 118 | import java.io.FileOutputStream; 119 | import java.io.IOException; 120 | import android.app.Activity; 121 | import android.content.Intent; 122 | import android.os.Bundle; 123 | import android.view.View; 124 | import android.widget.TextView; 125 | 126 | public class PrivateUserActivity extends Activity { 127 | 128 | private TextView mFileView; 129 | private static final String FILE_NAME = "private_file.dat"; 130 | 131 | @Override 132 | public void onCreate(Bundle savedInstanceState) { 133 | super.onCreate(savedInstanceState); 134 | setContentView(R.layout.user); 135 | mFileView = (TextView) findViewById(R.id.file_view); 136 | } 137 | 138 | private void callFileActivity() { 139 | Intent intent = new Intent(); 140 | intent.setClass(this, PrivateFileActivity.class); 141 | startActivity(intent); 142 | } 143 | 144 | /** 145 | * Call file Activity process 146 | * 147 | * @param view 148 | */ 149 | public void onCallFileActivityClick(View view) { 150 | callFileActivity(); 151 | } 152 | 153 | /** 154 | * Read file process 155 | * 156 | * @param view 157 | */ 158 | public void onReadFileClick(View view) { 159 | FileInputStream fis = null; 160 | try { 161 | fis = openFileInput(FILE_NAME); 162 | byte[] data = new byte[(int) fis.getChannel().size()]; 163 | fis.read(data); 164 | // *** POINT 4 *** Regarding the information to be stored in files, handle file data carefully and securely. 165 | // Omitted, since this is a sample. Please refer to "3.2 Handling Input Data Carefully and Securely." 166 | String str = new String(data); 167 | mFileView.setText(str); 168 | } catch (FileNotFoundException e) { 169 | mFileView.setText(R.string.file_view); 170 | } catch (IOException e) { 171 | android.util.Log.e("PrivateUserActivity", "failed to read file"); 172 | } finally { 173 | if (fis != null) { 174 | try { 175 | fis.close(); 176 | } catch (IOException e) { 177 | android.util.Log.e("PrivateUserActivity", "failed to close file"); 178 | } 179 | } 180 | } 181 | } 182 | 183 | /** 184 | * Rewrite file process 185 | * 186 | * @param view 187 | */ 188 | public void onWriteFileClick(View view) { 189 | FileOutputStream fos = null; 190 | try { 191 | // *** POINT 1 *** Files must be created in application directory. 192 | // *** POINT 2 *** The access privilege of file must be set private mode in order not to be used by other applications. 193 | fos = openFileOutput(FILE_NAME, MODE_APPEND); 194 | // *** POINT 3 *** Sensitive information can be stored. 195 | // *** POINT 4 *** Regarding the information to be stored in files, handle file data carefully and securely. 196 | // Omitted, since this is a sample. Please refer to "3.2 Handling Input Data Carefully and Securely." 197 | fos.write(new String("Sensitive information (User Activity)¥n").getBytes()); 198 | } catch (FileNotFoundException e) { 199 | mFileView.setText(R.string.file_view); 200 | } catch (IOException e) { 201 | android.util.Log.e("PrivateUserActivity", "failed to read file"); 202 | } finally { 203 | if (fos != null) { 204 | try { 205 | fos.close(); 206 | } catch (IOException e) { 207 | android.util.Log.e("PrivateUserActivity", "failed to close file"); 208 | } 209 | } 210 | } 211 | callFileActivity(); 212 | } 213 | } 214 | ``` 215 | -------------------------------------------------------------------------------- /4.6.1.2.md: -------------------------------------------------------------------------------- 1 | #### 4.6.1.2 使用公共只读文件 2 | 3 | 这是使用文件向未指定的大量应用公开内容的情况。 如果通过遵循以下几点来实现,那么它也是比较安全的文件使用方法。 请注意,在 API 级别 1 7及更高版本中,不推荐使用`MODE_WORLD_READABLE`变量来创建公共文件,并且在 API 级别 24 及更高版本中,会触发安全异常; 因此使用内容供应器的文件共享方法更可取。 4 | 5 | 要点: 6 | 7 | 1) 文件必须在应用目录中创建。 8 | 9 | 10 | 2) 文件的访问权限必须设置为其他应用只读。 11 | 12 | 3) 敏感信息不得存储。 13 | 14 | 4) 对于要存储在文件中的信息,请仔细和安全地处理文件数据。 15 | 16 | PublicFileActivity.java 17 | 18 | ```java 19 | package org.jssec.android.file.publicfile.readonly; 20 | 21 | import java.io.File; 22 | import java.io.FileInputStream; 23 | import java.io.FileNotFoundException; 24 | import java.io.FileOutputStream; 25 | import java.io.IOException; 26 | import android.app.Activity; 27 | import android.os.Bundle; 28 | import android.view.View; 29 | import android.widget.TextView; 30 | 31 | public class PublicFileActivity extends Activity { 32 | 33 | private TextView mFileView; 34 | private static final String FILE_NAME = "public_file.dat"; 35 | 36 | @Override 37 | public void onCreate(Bundle savedInstanceState) { 38 | super.onCreate(savedInstanceState); 39 | setContentView(R.layout.file); 40 | mFileView = (TextView) findViewById(R.id.file_view); 41 | } 42 | 43 | /** 44 | * Create file process 45 | * 46 | * @param view 47 | */ 48 | public void onCreateFileClick(View view) { 49 | FileOutputStream fos = null; 50 | try { 51 | // *** POINT 1 *** Files must be created in application directory. 52 | // *** POINT 2 *** The access privilege of file must be set to read only to other applications. 53 | // (MODE_WORLD_READABLE is deprecated API Level 17, 54 | // don't use this mode as much as possible and exchange data by using ContentProvider().) 55 | fos = openFileOutput(FILE_NAME, MODE_WORLD_READABLE); 56 | // *** POINT 3 *** Sensitive information must not be stored. 57 | // *** POINT 4 *** Regarding the information to be stored in files, handle file data carefully and securely. 58 | // Omitted, since this is a sample. Please refer to "3.2 Handling Input Data Carefully and Securely." 59 | fos.write(new String("Not sensitive information (Public File Activity)¥n").getBytes()); 60 | } catch (FileNotFoundException e) { 61 | mFileView.setText(R.string.file_view); 62 | } catch (IOException e) { 63 | android.util.Log.e("PublicFileActivity", "failed to read file"); 64 | } finally { 65 | if (fos != null) { 66 | try { 67 | fos.close(); 68 | } catch (IOException e) { 69 | android.util.Log.e("PublicFileActivity", "failed to close file"); 70 | } 71 | } 72 | } 73 | finish(); 74 | } 75 | 76 | /** 77 | * Read file process 78 | * 79 | * @param view 80 | */ 81 | public void onReadFileClick(View view) { 82 | FileInputStream fis = null; 83 | try { 84 | fis = openFileInput(FILE_NAME); 85 | byte[] data = new byte[(int) fis.getChannel().size()]; 86 | fis.read(data); 87 | String str = new String(data); 88 | mFileView.setText(str); 89 | } catch (FileNotFoundException e) { 90 | mFileView.setText(R.string.file_view); 91 | } catch (IOException e) { 92 | android.util.Log.e("PublicFileActivity", "failed to read file"); 93 | } finally { 94 | if (fis != null) { 95 | try { 96 | fis.close(); 97 | } catch (IOException e) { 98 | android.util.Log.e("PublicFileActivity", "failed to close file"); 99 | } 100 | } 101 | } 102 | } 103 | 104 | /** 105 | * Delete file process 106 | * 107 | * @param view 108 | */ 109 | public void onDeleteFileClick(View view) { 110 | File file = new File(this.getFilesDir() + "/" + FILE_NAME); 111 | file.delete(); 112 | mFileView.setText(R.string.file_view); 113 | } 114 | } 115 | ``` 116 | 117 | PublicUserActivity.java 118 | 119 | ```java 120 | package org.jssec.android.file.publicuser.readonly; 121 | 122 | import java.io.File; 123 | import java.io.FileInputStream; 124 | import java.io.FileNotFoundException; 125 | import java.io.FileOutputStream; 126 | import java.io.IOException; 127 | import android.app.Activity; 128 | import android.content.ActivityNotFoundException; 129 | import android.content.Context; 130 | import android.content.Intent; 131 | import android.content.pm.PackageManager.NameNotFoundException; 132 | import android.os.Bundle; 133 | import android.view.View; 134 | import android.widget.TextView; 135 | 136 | public class PublicUserActivity extends Activity { 137 | 138 | private TextView mFileView; 139 | private static final String TARGET_PACKAGE = "org.jssec.android.file.publicfile.readonly"; 140 | private static final String TARGET_CLASS = "org.jssec.android.file.publicfile.readonly.PublicFileActivity"; 141 | private static final String FILE_NAME = "public_file.dat"; 142 | 143 | @Override 144 | public void onCreate(Bundle savedInstanceState) { 145 | super.onCreate(savedInstanceState); 146 | setContentView(R.layout.user); 147 | mFileView = (TextView) findViewById(R.id.file_view); 148 | } 149 | 150 | private void callFileActivity() { 151 | Intent intent = new Intent(); 152 | intent.setClassName(TARGET_PACKAGE, TARGET_CLASS); 153 | try { 154 | startActivity(intent); 155 | } catch (ActivityNotFoundException e) { 156 | mFileView.setText("(File Activity does not exist)"); 157 | } 158 | } 159 | 160 | /** 161 | * Call file Activity process 162 | * 163 | * @param view 164 | */ 165 | public void onCallFileActivityClick(View view) { 166 | callFileActivity(); 167 | } 168 | 169 | /** 170 | * Read file process 171 | * 172 | * @param view 173 | */ 174 | public void onReadFileClick(View view) { 175 | FileInputStream fis = null; 176 | try { 177 | File file = new File(getFilesPath(FILE_NAME)); 178 | fis = new FileInputStream(file); 179 | byte[] data = new byte[(int) fis.getChannel().size()]; 180 | fis.read(data); 181 | // *** POINT 4 *** Regarding the information to be stored in files, handle file data carefully and securely. 182 | // Omitted, since this is a sample. Please refer to "3.2 Handling Input Data Carefully and Securely." 183 | String str = new String(data); 184 | mFileView.setText(str); 185 | } catch (FileNotFoundException e) { 186 | android.util.Log.e("PublicUserActivity", "no file"); 187 | } catch (IOException e) { 188 | android.util.Log.e("PublicUserActivity", "failed to read file"); 189 | } finally { 190 | if (fis != null) { 191 | try { 192 | fis.close(); 193 | } catch (IOException e) { 194 | android.util.Log.e("PublicUserActivity", "failed to close file"); 195 | } 196 | } 197 | } 198 | } 199 | 200 | /** 201 | * Rewrite file process 202 | * 203 | * @param view 204 | */ 205 | public void onWriteFileClick(View view) { 206 | FileOutputStream fos = null; 207 | boolean exception = false; 208 | try { 209 | File file = new File(getFilesPath(FILE_NAME)); 210 | // Fail to write in. FileNotFoundException occurs. 211 | fos = new FileOutputStream(file, true); 212 | fos.write(new String("Not sensitive information (Public User Activity)¥n").getBytes()); 213 | } catch (IOException e) { 214 | mFileView.setText(e.getMessage()); 215 | exception = true; 216 | } finally { 217 | if (fos != null) { 218 | try { 219 | fos.close(); 220 | } catch (IOException e) { 221 | exception = true; 222 | } 223 | } 224 | } 225 | if (!exception) 226 | callFileActivity(); 227 | } 228 | 229 | private String getFilesPath(String filename) { 230 | String path = ""; 231 | try { 232 | Context ctx = createPackageContext(TARGET_PACKAGE, 233 | Context.CONTEXT_RESTRICTED); 234 | File file = new File(ctx.getFilesDir(), filename); 235 | path = file.getPath(); 236 | } catch (NameNotFoundException e) { 237 | android.util.Log.e("PublicUserActivity", "no file"); 238 | } 239 | return path; 240 | } 241 | } 242 | ``` 243 | -------------------------------------------------------------------------------- /4.6.1.3.md: -------------------------------------------------------------------------------- 1 | #### 4.6.1.3 创建公共读写文件 2 | 3 | 这是一种文件用法,它允许未指定的大量应用的读写访问。 4 | 5 | 未指定的大量应用可以读写,意思不用多说了。 恶意软件也可以读取和写入,因此数据的可信度和安全性将永远不会得到保证。 另外,即使在没有恶意的情况下,也不能控制文件中的数据格式或写入的时间。 所以这种类型的文件在功能方面几乎不实用。 6 | 7 | 如上所述,从安全性和应用设计的角度来看,不可能安全地使用读写文件,因此应该避免使用读写文件。 8 | 9 | 要点: 10 | 11 | 不要创建允许来自其他应用的读写操作的文件。 -------------------------------------------------------------------------------- /4.6.1.4.md: -------------------------------------------------------------------------------- 1 | #### 4.6.1.4 使用外部存储器(公共读写)文件 2 | 3 | 将文件存储在 SD 卡等外部存储器中时,就是这种情况。当存储比较庞大的信息(放置从 Web 下载的文件)或者将信息带出到外部时(备份等)时,应该使用它。 4 | 5 | 对于未指定的大量应用,“外部存储器文件(公共读写)”与“公共读写文件“有相同特性。另外,对于声明使用`android.permission.WRITE_EXTERNAL_STORAGE`权限的应用,它和“公共读写文件”具有相同的特性。因此,应尽可能减少“外部存储器(公共读写)文件”的使用。 6 | 7 | 按照 Android 应用的惯例,备份文件很可能是在外部存储器中创建的。但是,如上所述,外部存储器中的文件存在被其他应用(包括恶意软件)篡改/删除的风险。因此,在输出备份的应用中,为了最小化应用规范或设计方面的风险,一些设计是必要的,例如显示“尽快将备份文件复制到 PC 等安全位置”。 8 | 9 | 要点: 10 | 11 | 1) 不得存储敏感信息。 12 | 13 | 2) 文件必须存储在每个应用的唯一目录中。 14 | 15 | 3) 对于要存储在文件中的信息,请仔细和安全地处理文件数据。 16 | 17 | 4) 请求应用的文件写入应该按照规范禁止。 18 | 19 | AndroidManifest.xml 20 | 21 | ```xml 22 | 23 | 25 | 31 | 33 | 37 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | ``` 49 | 50 | ExternalFileActivity.java 51 | 52 | ```java 53 | package org.jssec.android.file.externalfile; 54 | 55 | import java.io.File; 56 | import java.io.FileInputStream; 57 | import java.io.FileNotFoundException; 58 | import java.io.FileOutputStream; 59 | import java.io.IOException; 60 | import android.app.Activity; 61 | import android.os.Bundle; 62 | import android.view.View; 63 | import android.widget.TextView; 64 | 65 | public class ExternalFileActivity extends Activity { 66 | 67 | private TextView mFileView; 68 | private static final String TARGET_TYPE = "external"; 69 | private static final String FILE_NAME = "external_file.dat"; 70 | 71 | @Override 72 | public void onCreate(Bundle savedInstanceState) { 73 | super.onCreate(savedInstanceState); 74 | setContentView(R.layout.file); 75 | mFileView = (TextView) findViewById(R.id.file_view); 76 | 77 | } 78 | /** 79 | * Create file process 80 | * 81 | * @param view 82 | */ 83 | public void onCreateFileClick(View view) { 84 | FileOutputStream fos = null; 85 | try { 86 | // *** POINT 1 *** Sensitive information must not be stored. 87 | // *** POINT 2 *** Files must be stored in the unique directory per application. 88 | File file = new File(getExternalFilesDir(TARGET_TYPE), FILE_NAME); 89 | fos = new FileOutputStream(file, false); 90 | // *** POINT 3 *** Regarding the information to be stored in files, handle file data carefully and securely. 91 | // Omitted, since this is a sample. Please refer to "3.2 Handling Input Data Carefully and Securely." 92 | fos.write(new String("Non-Sensitive Information(ExternalFileActivity)¥n") 93 | .getBytes()); 94 | } catch (FileNotFoundException e) { 95 | mFileView.setText(R.string.file_view); 96 | } catch (IOException e) { 97 | android.util.Log.e("ExternalFileActivity", "failed to read file"); 98 | } finally { 99 | if (fos != null) { 100 | try { 101 | fos.close(); 102 | } catch (IOException e) { 103 | android.util.Log.e("ExternalFileActivity", "failed to close file"); 104 | } 105 | } 106 | } 107 | finish(); 108 | } 109 | 110 | /** 111 | * Read file process 112 | * 113 | * @param view 114 | */ 115 | public void onReadFileClick(View view) { 116 | FileInputStream fis = null; 117 | try { 118 | File file = new File(getExternalFilesDir(TARGET_TYPE), FILE_NAME); 119 | fis = new FileInputStream(file); 120 | byte[] data = new byte[(int) fis.getChannel().size()]; 121 | fis.read(data); 122 | // *** POINT 3 *** Regarding the information to be stored in files, handle file data carefully and securely. 123 | // Omitted, since this is a sample. Please refer to "3.2 Handling Input Data Carefully and Securely." 124 | String str = new String(data); 125 | mFileView.setText(str); 126 | } catch (FileNotFoundException e) { 127 | mFileView.setText(R.string.file_view); 128 | } catch (IOException e) { 129 | android.util.Log.e("ExternalFileActivity", "failed to read file"); 130 | } finally { 131 | if (fis != null) { 132 | try { 133 | fis.close(); 134 | } catch (IOException e) { 135 | android.util.Log.e("ExternalFileActivity", "failed to close file"); 136 | } 137 | } 138 | } 139 | } 140 | 141 | /** 142 | * Delete file process 143 | * 144 | * @param view 145 | */ 146 | public void onDeleteFileClick(View view) { 147 | File file = new File(getExternalFilesDir(TARGET_TYPE), FILE_NAME); 148 | file.delete(); 149 | mFileView.setText(R.string.file_view); 150 | } 151 | } 152 | ``` 153 | 154 | 使用的示例代码: 155 | 156 | ExternalFileUser.java 157 | 158 | ```java 159 | package org.jssec.android.file.externaluser; 160 | 161 | import java.io.File; 162 | import java.io.FileInputStream; 163 | import java.io.FileNotFoundException; 164 | import java.io.IOException; 165 | import android.app.Activity; 166 | import android.app.AlertDialog; 167 | import android.content.ActivityNotFoundException; 168 | import android.content.Context; 169 | import android.content.DialogInterface; 170 | import android.content.Intent; 171 | import android.content.pm.PackageManager.NameNotFoundException; 172 | import android.os.Bundle; 173 | import android.view.View; 174 | import android.widget.TextView; 175 | 176 | public class ExternalUserActivity extends Activity { 177 | 178 | private TextView mFileView; 179 | private static final String TARGET_PACKAGE = "org.jssec.android.file.externalfile"; 180 | private static final String TARGET_CLASS = "org.jssec.android.file.externalfile.ExternalFileActivity"; 181 | private static final String TARGET_TYPE = "external"; 182 | private static final String FILE_NAME = "external_file.dat"; 183 | 184 | @Override 185 | public void onCreate(Bundle savedInstanceState) { 186 | super.onCreate(savedInstanceState); 187 | setContentView(R.layout.user); 188 | mFileView = (TextView) findViewById(R.id.file_view); 189 | } 190 | private void callFileActivity() { 191 | Intent intent = new Intent(); 192 | intent.setClassName(TARGET_PACKAGE, TARGET_CLASS); 193 | try { 194 | startActivity(intent); 195 | } catch (ActivityNotFoundException e) { 196 | mFileView.setText("(File Activity does not exist)"); 197 | } 198 | } 199 | 200 | /** 201 | * Call file Activity process 202 | * 203 | * @param view 204 | */ 205 | public void onCallFileActivityClick(View view) { 206 | callFileActivity(); 207 | } 208 | 209 | /** 210 | * Read file process 211 | * 212 | * @param view 213 | */ 214 | public void onReadFileClick(View view) { 215 | FileInputStream fis = null; 216 | try { 217 | File file = new File(getFilesPath(FILE_NAME)); 218 | fis = new FileInputStream(file); 219 | byte[] data = new byte[(int) fis.getChannel().size()]; 220 | fis.read(data); 221 | // *** POINT 3 *** Regarding the information to be stored in files, handle file data carefully and securely. 222 | // Omitted, since this is a sample. Please refer to "3.2 Handling Input Data Carefully and Securely." 223 | String str = new String(data); 224 | mFileView.setText(str); 225 | } catch (FileNotFoundException e) { 226 | mFileView.setText(R.string.file_view); 227 | } catch (IOException e) { 228 | android.util.Log.e("ExternalUserActivity", "failed to read file"); 229 | } finally { 230 | if (fis != null) { 231 | try { 232 | fis.close(); 233 | } catch (IOException e) { 234 | android.util.Log.e("ExternalUserActivity", "failed to close file"); 235 | } 236 | } 237 | } 238 | } 239 | 240 | /** 241 | * Rewrite file process 242 | * 243 | * @param view 244 | */ 245 | public void onWriteFileClick(View view) { 246 | // *** POINT 4 *** Writing file by the requesting application should be prohibited as the specification. 247 | // Application should be designed supposing malicious application may overwrite or delete file. 248 | final AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this); 249 | alertDialogBuilder.setTitle("POINT 4"); 250 | alertDialogBuilder.setMessage("Do not write in calling appllication."); 251 | alertDialogBuilder.setPositiveButton("OK", 252 | new DialogInterface.OnClickListener() { 253 | @Override 254 | public void onClick(DialogInterface dialog, int which) { 255 | callFileActivity(); 256 | } 257 | }); 258 | alertDialogBuilder.create().show(); 259 | } 260 | 261 | private String getFilesPath(String filename) { 262 | String path = ""; 263 | try { 264 | Context ctx = createPackageContext(TARGET_PACKAGE, 265 | Context.CONTEXT_IGNORE_SECURITY); 266 | File file = new File(ctx.getExternalFilesDir(TARGET_TYPE), filename); 267 | path = file.getPath(); 268 | } catch (NameNotFoundException e) { 269 | android.util.Log.e("ExternalUserActivity", "no file"); 270 | } 271 | return path; 272 | } 273 | } 274 | ``` 275 | 276 | AndroidManifest.xml 277 | 278 | ```xml 279 | 280 | 282 | 286 | 287 | 288 | 292 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | ``` 304 | -------------------------------------------------------------------------------- /4.6.1.md: -------------------------------------------------------------------------------- 1 | ### 4.6.1 示例代码 2 | 3 | 如上所述,文件原则上应该是私有的。 但是,由于某些原因,有时文件应该由其他应用直接读写。 按照安全角度分类和比较中文件类型如表 4.6-1 所示。 它们根据文件存储位置或其他应用的访问权限分为四类。 下面展示了每个文件类别的示例代码,并在其中添加了每个的解释。 4 | 5 | 表 4.6-1 按照安全角度的文件类别和比较 6 | 7 | | 文件类别 | 其它应用的访问权限 | 储存位置 | 概述 | 8 | | --- | --- | --- | --- | 9 | | 私有文件 | NA | 应用目录中 | (1)只能在应用中读写,(2)可以处理敏感数据,(3)文件原则上应该是这个类型 | 10 | | 只读公共文件 | 读 | 应用目录中 | (1)其它应用和用户可读,(2)可以处理公开给应用外部的信息 | 11 | | 读写公共文件 | 读写 | 应用目录中 | (1)其它应用和用户可以读写,(2)从安全和应用设计角度来看,不应该使用 | 12 | | 外部存储设备(读写文件) | 读写 | 外部存储设备,例如 SD 卡 | (1)没有访问控制,(2)其它应用和用户总是可以读写或删除文件,(3)应该以最小需求使用,(4)可以处理很大的文件 | 13 | -------------------------------------------------------------------------------- /4.6.2.md: -------------------------------------------------------------------------------- 1 | ### 4.6.2 规则书 2 | 3 | 遵循以下规则: 4 | 5 | #### 4.6.2.1 文件原则上必须创建为私有(必需) 6 | 7 | 如“4.6 处理文件”和“4.6.1.3 使用公共读/写文件”所述,无论要存储的信息的内容如何,原则上都应该将文件设置为私有。 从 Android 安全角度来看,交换信息及其访问控制应该在 Android 系统中完成,如内容供应器和服务,并且如果存在不可能的因素,则应该考虑由文件访问权限作为替代方法。 8 | 9 | 请参阅每个文件类型的示例代码和以下规则条目。 10 | 11 | #### 4.6.2.2 禁止创建允许来自其他应用的读写访问的文件(必需) 12 | 13 | 如“4.6.1.3 使用公共读/写文件”中所述,当允许其他应用读取/写入文件时,存储在文件中的信息无法控制。 因此,从安全和功能/设计的角度来看,不应该用公共读/写文件共享信息。 14 | 15 | #### 4.6.2.3 使用存储在外部存储器如 SD 卡)的文件,应该尽可能最小(必需) 16 | 17 | 如“4.6.1.4 使用外部存储器(公共读写)文件”中所述,出于安全和功能的考虑,将文件存储在外部存储器(如 SD 卡)中,会导致潜在的问题。 另一方面,与应用目录相比,SD 卡可以处理更大范围的文件,并且这是可以用于将数据带出到应用之外的唯一存储器。 所以,可能有很多情况下必须使用它,取决于应用的规范。 18 | 19 | 将文件存储在外部存储器中时,考虑到未指定的大量应用和用户可以读/写/删除文件,所以有必要考虑以下各点以及示例代码中提及的要点,来设计应用。 20 | 21 | + 原则上,敏感信息不应保存在外部存储器的文件中。 22 | + 将敏感信息保存在外部存储器的文件中时,应将其加密。 23 | + 将文件保存在外部存储器时,如果被其他应用或用户篡改,将会出现问题,应该用电子签名保存。 24 | + 当读入外部存储器中的文件时,请在验证读取的数据安全性后使用数据。 25 | + 应该这样设计应用,假设外部存储器中的文件始终可以被删除。 26 | 27 | 请参考“4.6.2.4 应用应该在考虑文件范围的情况下设计”。 28 | 29 | #### 4.6.2.4 应用应该在考虑文件范围的情况下设计(必需) 30 | 31 | 保存在应用目录中的数据,被以下用户操作删除。 它与应用的范围是一致的,并且与应用的范围相比,它的独特之处在于它比应用的范围小。 32 | 33 | + 卸载应用 34 | + 删除每个应用的数据和缓存(设置=>应用=>选择目标应用) 35 | 36 | 保存在外部存储器中的文件,如 SD 卡,文件的范围比应用的范围长。 另外,还需要考虑以下情况。 37 | 38 | + 文件由用户删除 39 | + 取出/替换/取消挂载 SD 卡 40 | + 文件由恶意软件删除 41 | 42 | 如上所述,由于文件范围取决于文件的保存位置而有所不同,不仅从保护敏感信息的角度,而且从实现应用的正确行为的角度,有必要选择文件保存位置。 43 | -------------------------------------------------------------------------------- /4.6.md: -------------------------------------------------------------------------------- 1 | ## 4.6 处理文件 2 | 3 | 根据 Android 安全设计理念,文件仅用于信息持久化和临时保存(缓存),原则上它应该是私有的。 在应用之间交换信息不应该直接通过文件,而应该通过应用间的连接系统(如内容供应器或服务)来交换。 通过使用此功能,可以实现应用间访问控制。 4 | 5 | 由于无法在 SD 卡等外部存储设备上执行足够的访问控制,因此文件应限制仅在必要时通过功能方式使用,例如处理大型文件,或将信息传输到其他位置时(PC 等等)。 基本上,包含敏感信息的文件不应保存在外部存储设备中。 在需要将敏感信息保存在外部设备文件中的情况下,需要采取加密等对策,但这里没有提及。 6 | -------------------------------------------------------------------------------- /4.7.md: -------------------------------------------------------------------------------- 1 | ## 4.7 使用可浏览的意图 2 | 3 | Android 应用可以设计为从浏览器启动,并对应网页链接。 这个功能被称为“可浏览的意图”。 通过在清单文件中指定 URI 模式,应用将响应具有其 URI 模式的链接转移(用户点击等),并且应用以链接作为参数启动。 4 | 5 | 此外,使用 URI 模式从浏览器启动相应应用的方法不仅支持 Android,也支持 iOS 和其他平台,这通常用于 Web 应用与外部应用之间的链接等。例如, 在 Twitter 应用或 Facebook 应用中定义了以下 URI 模式,并且在 Android 和 iOS 中从浏览器启动相应的应用。 6 | 7 | 表 4.7-1 8 | 9 | | URL 模式 | 相应应用 | 10 | | --- | --- | 11 | | `fb://` | Facebook | 12 | | `twitter://` | Twitter | 13 | 14 | 考虑到联动性和便利性,功能似乎非常方便,但存在一些风险,即该功能被恶意第三方滥用。 可以假设的是,它们滥用应用功能,通过准备一个恶意网站,它的链接的 URL 具有不正确的参数,或者它们通过欺骗智能手机用户安装恶意软件,它包含相同的 URI 模式,来获取包含在 URL 中的信息。 使用“可浏览的意图”来对付这些风险时有一些要注意的地方。 15 | 16 | ### 4.7.1 示例代码 17 | 18 | 使用“可浏览的意图”的应用的示例代码如下: 19 | 20 | 要点: 21 | 22 | 1) (网页侧)不得包含敏感信息。 23 | 24 | 2) 仔细和安全地处理 URL 参数。 25 | 26 | Starter.html 27 | 28 | ```html 29 | 30 | 31 | 32 | 33 | Login 34 | 35 | 36 | ``` 37 | 38 | AndroidManifest.xml 39 | 40 | ```xml 41 | 42 | 47 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | // Accept implicit Intent 58 | 59 | // Accept Browsable intent 60 | 61 | // Accept URI 'secure://jssec' 62 | 63 | 64 | 65 | 66 | 67 | ``` 68 | 69 | BrowsableIntentActivity.java 70 | 71 | ```java 72 | package org.jssec.android.browsableintent; 73 | 74 | import android.app.Activity; 75 | import android.content.Intent; 76 | import android.net.Uri; 77 | import android.os.Bundle; 78 | import android.widget.TextView; 79 | 80 | public class BrowsableIntentActivity extends Activity { 81 | 82 | @Override 83 | public void onCreate(Bundle savedInstanceState) { 84 | super.onCreate(savedInstanceState); 85 | setContentView(R.layout.activity_browsable_intent); 86 | Intent intent = getIntent(); 87 | Uri uri = intent.getData(); 88 | if (uri != null) { 89 | // Get UserID which is passed by URI parameter 90 | // *** POINT 2 *** Handle the URL parameter carefully and securely. 91 | // Omitted, since this is a sample. Please refer to "3.2 Handling Input Data Carefully and Securely." 92 | String userID = "User ID = " + uri.getQueryParameter("user"); 93 | TextView tv = (TextView)findViewById(R.id.text_userid); 94 | tv.setText(userID); 95 | } 96 | } 97 | } 98 | ``` 99 | 100 | ### 4.7.2 规则书 101 | 102 | 使用“可浏览的意图”时,需要遵循以下规则: 103 | 104 | #### 4.7.2.1 (网页端)敏感信息不得包含在相应链接的参数中(必需) 105 | 106 | 当点击浏览器中的链接时,会发出一个意图,该意图的数据中有 URL 值(可以通过`Intent#getData`获取),并且带有相应意图过滤器的应用,从 Android 系统启动。 107 | 108 | 此时,当几个应用设置意图过滤器来接收相同的 URI 模式时,应用选择对话框将显示,与隐式意图正常启动相同,并启动用户选择的应用。 如果应用选择对话框中列出了恶意软件,则用户可能会错误地启动恶意软件,并将 URL 中的参数发送到恶意软件。 109 | 110 | 如上所述,需要避免直接在 URL 参数中包含敏感信息,因为它用于创建一般网页链接,所有包含在网页链接 URL 中的参数都可以提供给恶意软件。 111 | 112 | 用户 ID 和密码包含在 URL 中的例子: 113 | 114 | ``` 115 | insecure://sample/login?userID=12345&password=abcdef 116 | ``` 117 | 118 | 此外,即使 URL 参数仅包含非敏感内容,如用户ID,在由'可浏览的意图'启动后,在应用中输入密码时,用户可能会启动恶意软件并向其输入密码。所以应该考虑,一些规范,例如整个登录过程,在应用端完成。 在设计应用时必须记住它,并且由'可浏览的意图'启动应用,等同于由隐式意图启动,并且不保证启动了有效的应用。 119 | 120 | #### 4.7.2.2 小心和安全地处理 URL 参数(必需) 121 | 122 | 发送给应用的 URL 参数,并不总是来自合法的 Web 页面,因为匹配 URI 模式链接不仅可以由开发者生成,也可以由任何人生成。 另外,没有方法可以验证 URL 参数是否从有效网页发送。 123 | 124 | 因此,在使用 URL 参数之前,有必要验证 URL 参数的安全性,例如,检查是否包含意外值。 125 | -------------------------------------------------------------------------------- /4.8.md: -------------------------------------------------------------------------------- 1 | ## 4.8 输出到 LogCat 2 | 3 | 在 Android 中有一种名为 LogCat 的日志机制,不仅系统日志信息,还有应用日志信息也会输出到 LogCat。 LogCat 中的日志信息可以从同一设备中的其他应用中读出 [17],因此向L ogcat 输出敏感信息的应用,被认为具有信息泄露的漏洞。 敏感信息不应输出到 LogCat。 4 | 5 | > [17] 输出到 LogCat 的日志信息,可以由声明`READ_LOGS`权限的应用读取。 但是,在 Android 4.1 及更高版本中,无法读取其他应用输出的日志信息。 但智能手机用户可以通过 ADB ,阅读输出到 logcat 的每个日志信息。 6 | 7 | 从安全角度来看,在发行版应用中,最好不要输出任何日志。 但是,即使在发行版应用的情况下,在某些情况下也会出于某种原因输出日志。 在本章中,我们将介绍一些方法,以安全的方式将消息输出到 LogCat,即使在发行版应用中也是如此。 除此解释外,请参考“4.8.3.1 发行版应用中日志输出的两种思路”。 8 | 9 | ### 4.8.1 示例代码 10 | 11 | 接下来是在发行版应用中,通过 ProGuard 控制输出到 LogCat 的日志的方法。 ProGuard 是自动删除不需要的代码(如未使用的方法等)的优化工具之一。 12 | 13 | `android.util.Log`类有五种类型的日志输出方法:`Log.e()`, `Log.w()`, `Log.i()`, `Log.d()`, `Log.v()`。对于日志信息,有意输出的日志信息(以下称为“操作日志信息”)应该区分于不适合发行版应用的信息(以下称为开发日志信息),例如调试日志。建议使用`Log.e()/w()/i()`输出操作日志信息,并使用`Log.d()/v()`输出开发日志。正确使用五种日志输出方法的详细信息,请参阅“4.8.3.2 日志级别和日志输出方法的选择标准”,另外请参考“4.8.3.3 调试日志和`VERBOSE`日志并不总是自动删除”。 14 | 15 | 这是一个以安全方式使用 LogCat 的例子。此示例包括用于输出调试日志的`Log.d()`和`Log.v()`。如果应用用于发布,这两种方法将被自动删除。在此示例代码中,ProGuard 用于自动删除调用`Log.d()/v()`的代码块。 16 | 17 | 要点: 18 | 19 | 1) 敏感信息不能由`Log.e()/w()/i()`,`System.out / err`输出。 20 | 21 | 2) 敏感信息应由`Log.d()/v()`在需要时输出。 22 | 23 | 3) 不应使用`Log.d()/v()`的返回值(以替换或比较为目的)。 24 | 25 | 4) 当你构建应用来发布时,你应该在代码中引入机制,自动删除不合适的日志记录方法(如`Log.d()`或`Log.v()`)。 26 | 27 | 5) 必须使用发行版构建配置来创建用于(发布)发行的 APK 文件。 28 | 29 | ProGuardActivity.java 30 | 31 | ```java 32 | package org.jssec.android.log.proguard; 33 | 34 | import android.app.Activity; 35 | import android.os.Bundle; 36 | import android.util.Log; 37 | 38 | public class ProGuardActivity extends Activity { 39 | 40 | final static String LOG_TAG = "ProGuardActivity"; 41 | 42 | @Override 43 | public void onCreate(Bundle savedInstanceState) { 44 | super.onCreate(savedInstanceState); 45 | setContentView(R.layout.activity_proguard); 46 | // *** POINT 1 *** Sensitive information must not be output by Log.e()/w()/i(), System.out/err. 47 | Log.e(LOG_TAG, "Not sensitive information (ERROR)"); 48 | Log.w(LOG_TAG, "Not sensitive information (WARN)"); 49 | Log.i(LOG_TAG, "Not sensitive information (INFO)"); 50 | // *** POINT 2 *** Sensitive information should be output by Log.d()/v() in case of need. 51 | // *** POINT 3 *** The return value of Log.d()/v()should not be used (with the purpose of substitution or comparison). 52 | Log.d(LOG_TAG, "sensitive information (DEBUG)"); 53 | Log.v(LOG_TAG, "sensitive information (VERBOSE)"); 54 | } 55 | } 56 | ``` 57 | 58 | proguard-project.txt 59 | 60 | ``` 61 | # prevent from changing class name and method name etc. 62 | -dontobfuscate 63 | # *** POINT 4 *** In release build, the build configurations in which Log.d()/v() are deleted automatica 64 | lly should be constructed. 65 | -assumenosideeffects class android.util.Log { 66 | public static int d(...); 67 | public static int v(...); 68 | } 69 | ``` 70 | 71 | 要点 5:必须使用发行版构建配置来创建用于(发布)发行的 APK 文件。 72 | 73 | ![](img/4-8-1.jpg) 74 | 75 | 开发版应用(调试版本)和发行版应用(发布版本)之间的LogCat 输出差异如下图 4.8-2 所示。 76 | 77 | ![](img/4-8-2.jpg) 78 | 79 | ### 4.8.2 规则书 80 | 81 | 输出消息记录时,遵循以下规则: 82 | 83 | #### 4.8.2.1 操作日志信息中不能包含敏感信息(必需) 84 | 85 | 输出到 LogCat 的日志可以从其他应用中读取,因此敏感信息(如用户的登录信息)不应该由发行版应用输出。 在开发过程中,不必编写输出敏感信息的代码,或者在发布之前需要删除所有这些代码。 86 | 87 | 为了遵循这个规则,首先,不要在操作日志信息中包含敏感信息。 此外,建议构建系统,在构建发行版时,删除输出敏感信息的代码。 请参阅“4.8.2.2 构建生成系统,在构建发行版时,自动删除输出开发日志信息的代码(推荐)”。 88 | 89 | #### 4.8.2.2 构建生成系统,在构建发行版时,自动删除输出开发日志信息的代码(推荐) 90 | 91 | 开发应用时,有时最好将敏感信息输出到日志中,来检查过程内容和调试,例如复杂逻辑过程中的临时操作结果,程序内部状态信息,通信协议的数据结构。在开发过程中,将敏感信息作为调试日志输出并不重要,在这种情况下,相应的日志输出代码应该在发布之前删除,如“4.8.2.1 操作日志信息中不能包含敏感信息(必需)”所述。 92 | 93 | 为了在构建发行版时,确实删除了输出开发日志信息的代码,应该构建系统,使用某些工具自动执行代码删除。 “4.8.1 示例代码”中介绍的 ProGuard 可以用于此方法。如下所述,用 ProGuard 删除代码有一些值得注意的地方。这里应该将系统用于一些应用,它通过`Log.d()/ v()`输出开发日志信息,根据“4.8.3.2 日志级别和日志输出方法的选择标准”。 94 | 95 | ProGuard 会自动删除不需要的代码,如未使用的方法。通过指定`Log.d()/ v()`作为`-assumenosideeffects`选项的参数,`Log.d(),`Log.v()`的调用被视为不必要的代码,并且这些代码将被删除。 96 | 97 | ``` 98 | -assumenosideeffects class android.util.Log { 99 | public static int d(...); 100 | public static int v(...); 101 | } 102 | ``` 103 | 104 | 如果使用这个自动删除系统,请注意`Log.d()`,`Log.v()`代码在使用其返回值时不会被删除,因此不应该使用`Log.d()`,`Log.v()`的返回值。 例如,下一个代码中的`Log.v()`不会被删除。 105 | 106 | ```java 107 | int i = android.util.Log.v("tag", "message"); 108 | System.out.println(String.format("Log.v() returned %d. ", i)); //Use the returned value of Log.v() for examination 109 | ``` 110 | 111 | 如果你想重复使用源代码,则应保持项目环境的一致性,包括 ProGuard 设置。 例如,预设`Log.d()`和`Log.v()`的源代码将被上面的 ProGuard 设置自动删除。 如果在未设置 ProGuard 的其他项目中使用此源代码,则不会删除`Log.d()`和`Log.v()`,因此可能会泄露敏感信息。 重用源代码时,应确保包括 ProGuard 设置在内的项目环境的一致性。 112 | 113 | #### 4.8.2.3 输出`Throwable`对象时,使用`Log.d()/v()`(推荐) 114 | 115 | 如“4.8.1 示例代码”和“4.8.3.2 日志级别和日志输出方法的选择标准”中所述,输出敏感信息不应通过`Log.e()/w()/i()`输出来记录。 另一方面,为了使开发者输出程序异常的细节来记录,当异常发生时,在某些情况下,堆栈踪迹通过`Log.e(..., Throwable tr)/w(..., Throwable tr)/i(..., Throwable 116 | tr)`输出到 LogCat。 但是,敏感信息有时可能包含在堆栈踪迹中,因为它显示程序的详细内部结构。 例如,当`SQLiteException`按原样输出时,会输出 SQL 语句的类型,因此可能会提供 SQL 注入攻击的线索。 因此,建议在输出`Throwable`对象时,仅使用`Log.d()/v()`方法。 117 | 118 | #### 4.8.2.4 仅仅将`android.util.Log`类的方法用于日志输出(推荐) 119 | 120 | 在开发过程中,你可以通过`System.out / err`输出日志,来验证应用的行为是否按预期工作。 当然,日志可以通过`System.out / err`的`print()/ println()`方法输出到 LogCat,但强烈建议仅使用`android.util.Log`类的方法,原因如下。 121 | 122 | 在输出日志时,一般根据信息的紧急程度,正确使用最合适的输出方法,并控制输出。 例如,使用严重错误,注意,简单应用的信息通知等类别。 然而,在这种情况下,在发布时需要输出的信息(操作日志信息),和可以包括敏感信息(开发日志信息)的信息,通过相同的方法输出。 所以,当删除输出敏感信息的代码时,可能会存在一些删除操作被忽略掉的危险。 123 | 124 | 除此之外,当使用`android.util.Log`和`System.out / err`进行日志输出时,与仅使用`android.util.Log`相比,需要考虑的因素会增加,因此可能会出现一些错误,比如 一些删除被忽略掉了。 125 | 126 | 为了减少上述错误发生的风险,建议仅使用`android.util.Log`类的方法。 127 | 128 | ### 4.8.3 高级话题 129 | 130 | #### 4.8.3.1 发布版应用中日志输出的两种思路 131 | 132 | 发布版应用中有两种思考日志输出的方式。一个是任何日志都不应该输出,另一个是用于以后分析的必要信息应该作为日志输出。从安全角度来看,最好是,任何日志都不应该在发行版应用中输出,但有时候,即使在发行版本应用中,出于各种原因也会输出日志。每种思考方式按照以下描述。 133 | 134 | 前者是“任何日志都不应该输出”,这是因为,在发行版应用中输出日志没有那么重要,并且存在泄露敏感信息的风险。这是因为开发人员没有办法在 Android 应用运行环境中收集发行版应用的日志信息,这与许多 Web 应用的运行环境不同。基于这种思想,日志代码仅用于开发阶段,并且在构建发行版应用时删除所有日志代码。 135 | 136 | 后者是“必要的信息应作为日志输出,以供日后分析”,作为客户支持中,分析应用错误的最终选项,以防你的客户支持有任何疑问。 基于这个想法,如上所述,有必要准备系统来防止人为错误并将其引入到项目中,因为如果你没有系统,则必须记住避免在发行版应用中记录敏感信息。 137 | 138 | 更多日志方法的信息,请参考下面的链接: 139 | 140 | 适用于贡献者/日志的代码风格指南 141 | 142 | http://source.android.com/source/code-style.html#log-sparingly 143 | 144 | #### 4.8.3.2 日志级别和日志输出方法的选择标准 145 | 146 | 在 Android 中的`android.util.Log`类中定义了五个日志级别(`ERROR`,`WARN`,`INFO`,`DEBUG`,`VERBOSE`)。 使用`android.util.Log`类输出日志消息时,应该选择最合适的方法,如表 4.8-1 所示,它展示了日志级别和方法的选择标准。 147 | 148 | 表 4.8-1 日志级别和方法的选择标准 149 | 150 | | 日志级别 | 方法 | 要输出的日志信息 | 151 | | --- | --- | --- | 152 | | `ERROR` | `Log.e()` | 应用处于错误状态时,输出的日志信息 | 153 | | `WARN` | `Log.w()` | 应用面临非预期严重情况时,输出的日志信息 | 154 | | `INFO` | `Log.i()` | 与上面不同,用于提示应用状态中任何值得注意的更改或者结果 | 155 | | `DEBUG` | `Log.d()` | 应用的内部状态信息,开发应用时,需要临时输出,用于分析特定 bug 的成因 | 156 | | `VERBOSE` | `Log.v()` | 不属于上面任何一个的日志信息。应用开发者以多种目的输出。例如,输出服务器通信信息来转储。 | 157 | 158 | 发行版应用的注意事项: 159 | 160 | `e/w/i`: 161 | 162 | 日志信息可能由用户参考,因此可以在开发版应用和发行版应用中输出。 因此,敏感信息不应该在这些级别输出。 163 | 164 | `d/v`: 165 | 166 | 日志信息仅适用于应用开发人员。 因此,这种类型的信息不应该在发行版的情况下输出。 167 | 168 | 更多日志方法的信息,请参考下面的链接: 169 | 170 | 适用于贡献者/日志的代码风格指南 171 | 172 | http://source.android.com/source/code-style.html#log-sparingly 173 | 174 | #### 4.8.3.3 `DEBUG`和`VERBOSE`日志并不总是自动删除 175 | 176 | 以下引用自`android.util.Log`类 [18] 的开发人员参考。 177 | 178 | > [18] ttp://developer.android.com/reference/android/util/Log.html 179 | 180 | 按照啰嗦程度的顺序排列,从最少到最多是`ERROR`,`WARN`,`INFO`,`DEBUG`,`VERBOSE`。 除了在开发期间,绝不应该将`VERBOSE`编译进应用。 `DEBUG`日志被编译但在运行时剥离。 始终保留`ERROR`,`WARN`,`INFO`日志。 181 | 182 | 在阅读了上述文章之后,一些开发人员可能会误解`Log`类的行为,如下所示。 183 | 184 | + 构建发行版时不编译`Log.v()`调用,`VERBOSE`日志从不输出。 185 | + 编译`Log.v()`调用,但执行时绝不输出`DEBUG`日志。 186 | 187 | 但是,日志记录方法从来不会表现成这样,并且无论使用调试模式还是发布模式编译,都会输出所有消息。 如果仔细阅读文档,你将能够认识到,文档的要点与日志方法的行为无关,而是日志的基本策略。 188 | 189 | 在本章中,我们通过使用 ProGuard 引入了示例代码以获得上述的预期结果。 190 | 191 | #### 4.8.3.4 从汇编中移除敏感信息 192 | 193 | 如果为了删除`Log.d()`方法而使用 ProGuard 构建以下代码,有必要记住,`ProGuard`会保留为日志信息构造字符串的语句(代码的第一行),即使它删除了 `Log.d()`方法的调用(代码的第二行)。 194 | 195 | ```java 196 | String debug_info = String.format("%s:%s", "Sensitive information1", "Sensitive information2"); 197 | if (BuildConfig.DEBUG) android.util.Log.d(TAG, debug_info); 198 | ``` 199 | 200 | 以下反汇编显示了使用 ProGuard 发布上述代码的结果。 实际上,没有`Log.d()`调用过程,但你可以看到字符串一致性定义,例如`Sensitive information1`,和`String#format()`方法的调用过程,不会被删除并仍然存在。 201 | 202 | ```smali 203 | const-string v1, "%s:%s" 204 | const/4 v2, 0x2 205 | new-array v2, v2, [Ljava/lang/Object; 206 | const/4 v3, 0x0 207 | const-string v4, "Sensitive information 1" 208 | aput-object v4, v2, v3 209 | const/4 v3, 0x1 210 | const-string v4, "Sensitive information 2" 211 | aput-object v4, v2, v3 212 | invoke-static {v1, v2}, Ljava/lang/String;->format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang 213 | /String; 214 | move-result-object v0 215 | ``` 216 | 217 | 实际上,找到反汇编 APK 文件的组装日志输出信息特定部分并不容易。 但是,在某些处理机密信息的应用中,这种类型的过程在某些情况下不应保留在 APK 文件中。 你应该像下面那样实现你的应用,来避免在字节码中保留敏感信息的后果。 在发行版中,编译器优化将完全删除以下代码。 218 | 219 | ```java 220 | if (BuildConfig.DEBUG) { 221 | String debug_info = String.format("%s:%s", " Snsitive information 1", "Sensitive information 2"); 222 | if (BuildConfig.DEBUG) android.util.Log.d(TAG, debug_info); 223 | } 224 | ``` 225 | 226 | 此外,ProGuard 无法删除以下日志消息代码`("result:" + value)`。 227 | 228 | ```java 229 | Log.d(TAG, "result:" + value); 230 | ``` 231 | 232 | 在这种情况下,你可以通过以下方式解决问题。 233 | 234 | ```java 235 | if (BuildConfig.DEBUG) Log.d(TAG, "result:" + value); 236 | ``` 237 | 238 | #### 4.8.3.5 意图的内容输出到了 LogCat 239 | 240 | 使用活动时,需要注意,因为`ActivityManager`将意图的内容输出到 LogCat。 请参阅“4.1.3.5 使用活动时的日志输出”。 241 | 242 | #### 4.8.3.6 限制输出到`System.out / err`的日志 243 | 244 | `System.out / err`方法将所有消息输出到 LogCat。 即使开发者没有在他们的代码中使用这些方法,Android 也可以向`System.out / err`发送一些消息,例如,在以下情况下,Android 会将堆栈踪迹发送到`System.err`方法。 245 | 246 | + 使用`Exception#printStackTrace()`时 247 | + 隐式输出到`System.err`时(当异常没有被应用捕获时,它会由系统提供给`Exception#printStackTrace()`。) 248 | 249 | 你应该适当地处理错误和异常,因为堆栈踪迹包含应用的独特信息。 250 | 251 | 我们介绍一种改变`System.out / err`默认输出目标的方法。 当你构建发行版应用时,以下代码将`System.out / err`方法的输出重定向到任何地方。 但是,你应该考虑此重定向是否会导致应用或系统故障,因为代码会暂时覆盖`System.out / err`方法的默认行为。 此外,这种重定向仅对你的应用有效,对系统进程毫无价值。 252 | 253 | OutputRedirectApplication.java 254 | 255 | ```java 256 | package org.jssec.android.log.outputredirection; 257 | 258 | import java.io.IOException; 259 | import java.io.OutputStream; 260 | import java.io.PrintStream; 261 | import android.app.Application; 262 | 263 | public class OutputRedirectApplication extends Application { 264 | 265 | // PrintStream which is not output anywhere 266 | private final PrintStream emptyStream = new PrintStream(new OutputStream() { 267 | public void write(int oneByte) throws IOException { 268 | // do nothing 269 | } 270 | }); 271 | 272 | @Override 273 | public void onCreate() { 274 | // Redirect System.out/err to PrintStream which doesn't output anywhere, when release build. 275 | // Save original stream of System.out/err 276 | PrintStream savedOut = System.out; 277 | PrintStream savedErr = System.err; 278 | // Once, redirect System.out/err to PrintStream which doesn't output anywhere 279 | System.setOut(emptyStream); 280 | System.setErr(emptyStream); 281 | // Restore the original stream only when debugging. (In release build, the following 1 line is deleted byProGuard.) 282 | resetStreams(savedOut, savedErr); 283 | } 284 | 285 | // All of the following methods are deleted byProGuard when release. 286 | private void resetStreams(PrintStream savedOut, PrintStream savedErr) { 287 | System.setOut(savedOut); 288 | System.setErr(savedErr); 289 | } 290 | } 291 | ``` 292 | 293 | AndroidManifest.xml 294 | 295 | ```xml 296 | 298 | 303 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | ``` 315 | 316 | proguard-project.txt 317 | 318 | ``` 319 | # Prevent from changing class name and method name, etc 320 | -dontobfuscate 321 | # In release build, delete call from Log.d()/v() automatically. 322 | -assumenosideeffects class android.util.Log { 323 | public static int d(...); 324 | public static int v(...); 325 | } 326 | # In release build, delete resetStreams() automatically. 327 | -assumenosideeffects class org.jssec.android.log.outputredirection.OutputRedirectApplication { 328 | private void resetStreams(...); 329 | } 330 | ``` 331 | 332 | 开发版应用(调试版)和发布版应用(发行版)之间的 LogCat 输出差异如下图 4.8-3 所示。 333 | 334 | ![](img/4-8-3.jpg) 335 | 336 | -------------------------------------------------------------------------------- /4.md: -------------------------------------------------------------------------------- 1 | # 四、以安全方式使用技术 -------------------------------------------------------------------------------- /5.2.2.md: -------------------------------------------------------------------------------- 1 | ### 5.2.2 规则书 2 | 3 | 使用内部权限时,请确保遵循以下规则: 4 | 5 | #### 5.2.2.1 Android 的系统危险权限只能用于保护用户资产(必需) 6 | 7 | 由于不建议你使用自己的危险权限(请参阅“5.2.2.2 你自己的危险权限不得使用(必需)”),我们将在使用 Android 操作系统的系统危险权限的前提下进行。 8 | 9 | 不像其他三种类型的权限,危险权限具有这个特性,需要用户同意授予应用权限,在声明了危险权限的设备上安装应用时,将显示以下屏幕:随后, 用户可以知道应用试图使用的权限级别(危险权限和正常权限),当用户点击“安装”时,应用将被授予权限,然后安装。 10 | 11 | ![](img/5-2-7.jpg) 12 | 13 | 应用可以处理开发人员希望保护的用户资产。 我们必须意识到,危险的权限只能保护用户资产,因为用户只是授予权限的人。 另一方面,开发人员想要保护的资产不能用上述方法保护。 14 | 15 | 例如,假设应用具有一个组件,只与内部应用通信,它不允许从其他公司的任何应用访问该组件,并且通过危险权限的保护来实现。 当用户根据判断,向另一家公司的应用授予权限时,需要保护的内部资产可能通过应用授权来利用。 为了在此类情况下保护内部资产,我们建议使用内部定义的签名权限。 16 | 17 | #### 5.2.2.2 不能使用你自己的危险权限(必需) 18 | 19 | 即使使用内部定义的危险权限,在某些情况下,屏幕提示“请求允许来自用户的权限”也不会显示。 这意味着,有时根据用户判断来请求权限的特性(危险权限的特征)不起作用。 因此,指导手册规定“不得使用内部定义的危险权限”。 20 | 21 | 为了解释它,我们假设有两种类型的应用。 第一种类型的应用定义了内部危险权限,并且它让受此权限保护的组件公开。 我们称之为`ProtectedApp`。 另一个是我们称为`AttackerApp`,它试图利用`ProtectedApp`的组件。 我们还假设`AttackerApp`不仅声明了使用它的权限,而且还定义了相同的权限。 22 | 23 | 在以下情况下,`AttackerApp`可以在未经用户同意的情况下,使用`ProtectedApp`的组件: 24 | 25 | 1. 当用户安装`AttackerApp`时,安装将在没有屏幕提示的情况下完成,它要求用户授予应用危险权限。 26 | 2. 同样,当用户安装`ProtectedApp`时,安装将会完成而没有任何特别的警告。 27 | 3. 当用户启动`AttackerApp`后,`AttackerApp`可以访问`ProtectedApp`的组件,而不会被用户检测到,这可能会导致损失。 28 | 29 | 这种情况的原因在下面解释。 当用户尝试首先安装`AttackerApp`时,在特定设备上,尚未使用`uses-permission`来定义声明的权限。 没有发现错误,Android 操作系统将继续安装。 由于只有在安装时用户才需要同意危险权限,因此已安装的应用将被视为已被授予权限。 因此,如果稍后安装的应用的组件受到名称相同的危险权限的保护,则在未经用户同意的情况下,事先安装的应用将能够利用该组件。 30 | 31 | 此外,由于在安装应用时,确保存在 Android OS 定义的系统危险权限,每次安装具有`uses-permission`的应用时,都会显示用户验证提示。 只有在自定义危险权限的情况下才会出现此问题。 在写这篇文章的时候,还没有开发出可行方法,在这种情况下保护组件的访问。 因此,你不得使用你自己的危险权限。 32 | 33 | #### 5.2.2.3 你自己的签名权限必需仅在提供方定义(必需) 34 | 35 | 如“5.2.1.2 如何使用内部定义的签名权限,在内部应用之间进行通信”中所示,在进行内部应用之间的内部通信时,通过检查签名权限,可以确保安全性。 当使用这种机制时,保护级别为签名的权限的定义,必须写在具有组件的提供方应用的`AndroidManifest.xml`中,但用户方应用不能定义签名权限。 36 | 37 | 此规则也适用于`signatureOrSystem`权限。原因如下。 38 | 39 | 我们假设,在提供方应用之前安装了多个用户方应用,并且每个用户方应用,不仅要求提供方应用定义的签名权限,而且还定义了相同的权限。 在这些情况下,所有用户方应用都可以在安装提供方应用之后,立即访问提供方应用。 随后,卸载先安装的用户方应用时,权限的定义也将被删除,然后该权限将变为未定义。 因此,其余的用户方应用将无法访问提供方应用。 40 | 41 | 以这种方式,当用户方应用定义了一个自定义权限时,它可能会意外地将权限设置为未定义。因此,只有提供需要保护的组件的提供方应用才应该定义权限,并且必须避免在用户方定义权限。 42 | 43 | 通过如上所述的那样,自定义权限将在安装提供方应用时由 Android OS 应用,并且在卸载应用时权限将变得未定义。因此,由于权限定义总是对应提供方应用的定义,因此可以提供适当的组件并对其进行保护。请注意,这个观点成立,是因为对于内部定义的签名权限,用户方应用被授予权限,而不管应用在相互通信中的安装顺序 [24]。 44 | 45 | > [24] 如果使用正常/危险权限,并且用户方应用安装在提供方应用之前,则该权限将不会授予用户方应用,权限仍未定义。 因此,即使在安装了提供方应用之后,也不能访问组件。 46 | 47 | #### 5.2.2.4 验证内部定义的签名权限是否由内部应用定义(必需) 48 | 49 | 实际上,只有通过`AnroidManifest.xml`声明签名权限并使用权限来保护组件,才能说是足够安全。 此问题的详细信息,请参阅“高级主题”部分中的“5.2.3.1 绕过自定义签名权限的 Android 操作系统特性及其对策”。 50 | 51 | 以下是安全并正确使用内部定义的签名权限的步骤。 52 | 53 | 首先,在`AndroidManifest.xml`中编写如下代码: 54 | 55 | 在提供方应用的`AndroidManifest.xml`中定义内部签名权限。(权限定义) 56 | 57 | 例如:`` 58 | 59 | 在提供方应用的`AndroidManifest.xml`中,使用要保护的组件的权限属性强制执行权限。 (执行权限) 60 | 61 | 例如:`...` 62 | 63 | 在每个用户方应用的`AndroidManifest.xml`中,使用`uses-permission`标签声明内部定义的签名权限,来访问要保护的组件。 (使用权限声明) 64 | 65 | 例如:`` 66 | 67 | 下面,在源代码中实现这些: 68 | 69 | 在处理组件的请求之前,首先验证内部定义的签名权限是否由内部应用定义。 如果不是,请忽略该请求。 (保护提供方组件) 70 | 71 | 在访问组件之前,请先验证内部定义的签名权限是否由内部应用定义。 否则,请勿访问组件(用户方组件中的保护)。 72 | 73 | 最后,使用 Android Studio 的签名功能之前,执行下列事情: 74 | 75 | 使用相同的开发人员密钥,对所有互相通信的应用的 APK 进行签名。 76 | 77 | 在此,对于如何实现“确认内部定义签名权限已由内部应用定义”的具体要点,请参阅“5.2.1.2 如何使用内部定义的签名权限,在内部应用之间进行通信”。 78 | 79 | 此规则也适用于`signatureOrSystem`权限。 80 | 81 | #### 5.2.2.5 不应该使用你自己的普通权限(推荐) 82 | 83 | 应用只需在`AndroidManifest.xml`中使用`uses-permission`声明,即可使用正常权限。 因此,你不能使用正常权限,来保护组件免受恶意软件的安装。 84 | 85 | 此外,在使用自定义普通权限进行应用间通信的情况下,应用是否可以被授予权限取决于安装顺序。 例如,当你安装已声明使用普通权限的应用(用户方法),并且在另一应用(提供者端)之前,它拥有已定义权限的组件,用户方应用将无法 访问受权限保护的组件,即使稍后安装提供方应用也是如此。 86 | 87 | 作为一种方法,防止由于安装顺序而导致的应用间通信丢失,你可以考虑在通信中的每个应用中定义权限。 通过这种方式,即使在提供方应用之前安装了用户方应用,所有用户方应用也将能够访问提供方应用。 但是,它会产生一种情况,即在卸载第一个安装的用户方应用时,权限未定义。 因此,即使有其他用户方应用,他们也无法访问提供方应用。 88 | 89 | 如上所述,存在损害应用可用性的风险,因此不应使用你自己的正常权限。 90 | 91 | #### 5.2.2.6 你自己的权限名称的字符串应该是应用包名的扩展(推荐) 92 | 93 | 94 | 当多个应用使用相同名称定义权限时,将使用先安装的应用所定义的保护级别。 如果首先安装的应用定义了正常权限,并且稍后安装的应用使用相同的名称定义了签名权限,则签名权限的保护将不可用。 即使没有恶意的意图,多个应用之间的权限名称冲突,也可能导致任何应用的行为成为意外的保护级别。 为防止发生此类事故,建议权限名称扩展于定义权限的应用的包名(以它开头),如下所示。 95 | 96 | ``` 97 | (package name).permission.(identifying string) 98 | ``` 99 | 100 | 例如,为`org.jssec.android.sample`包定义`READ`访问权限时,以下名称将是首选。 101 | 102 | ``` 103 | org.jssec.android.sample.permission.READ 104 | ``` 105 | -------------------------------------------------------------------------------- /5.2.md: -------------------------------------------------------------------------------- 1 | ## 5.2 权限和保护级别 2 | 3 | 权限内有四种类型的保护级别,它们包括正常,危险,签名和签名或系统。 根据保护级别,权限被称为正常权限,危险权限,签名权限或签名或系统权限。 以下部分中使用这些名称。 -------------------------------------------------------------------------------- /5.3.2.md: -------------------------------------------------------------------------------- 1 | ### 5.3.2 规则书 2 | 3 | 实现认证器应用时,遵循下列规则: 4 | 5 | #### 5.3.2.1 提供认证器的服务必须是私有的(必需) 6 | 7 | 前提是,提供认证器的服务由账户管理器使用,并且不应该被其他应用访问。 因此,通过使其成为私有服务,它可以避免其他应用的访问。 此外,账户管理器以系统权限运行,所以即使是私有服务,账户管理器也可以访问。 8 | 9 | #### 5.3.2.2 登录界面活动必须由认证器应用实现(必需) 10 | 11 | 用于添加新帐户并获取认证令牌的登录界面,应由认证应用实现。 自己的登录界面不应该在用户应用一端准备。 正如本文开头提到的,【账户管理器的优势在于,极其敏感的信息/密码不一定要由应用处理】,如果在用户应用一端准备登录界面,则密码由用户应用处理, 其设计越过了账户管理器的策略。 12 | 13 | 通过由身份验证器应用准备登录界面,操作登录界面的人仅限于设备用户。 这意味着,恶意应用无法通过尝试直接登录,或创建帐户来攻击帐户。 14 | 15 | #### 5.3.2.3 登录界面活动必须是公共活动,并假设其他应用的攻击访问(必需) 16 | 17 | 登录界面活动是由用户应用加载的系统。 为了即使在用户应用和身份验证器应用的签名密钥不同时,也能展示登录界面,登录界面活动应该实现为公共活动。 登录界面活动是公共活动,意味着有可能会被恶意应用启动。 永远不要相信任何输入数据。 因此,有必要采取“3.2 小心并安全处理输入数据”中提到的对策。 18 | 19 | #### 5.3.2.4 使用显示意图提供`KEY_INTENT `,带有登录界面活动的指定类名称(必需) 20 | 21 | 22 | 当认证器需要打开登录界面活动时,启动登录界面活动的意图,会在返回给账户管理器的 Bundle 中,由`KEY_INTENT`提供。 所提供的意图应该是指定登录界面活动的类名的显式意图。 在使用隐示意图,它指定动作名称的情况下,有可能并不启动由认证器应用本身准备的登录界面活动,而是其他应用准备的活动。 当恶意应用准备了和常规一样的登录界面时,用户可能会在伪造的登录界面中输入密码。 23 | 24 | #### 5.3.2.5 敏感信息(如帐户信息和认证令牌)不得输出到日志(必需) 25 | 26 | 访问在线服务的应用有时会遇到麻烦,例如无法成功访问在线服务。 访问失败的原因各不相同,如网络环境管理不善,通信协议实现失败,权限不足,认证错误等。一个常见的实现方式是,程序输出详细信息给日志,以便开发人员可以稍后分析问题的原因。 27 | 28 | 敏感信息(如密码或认证令牌)不应输出到日志中。 日志信息可以从其他应用读取,因此可能成为信息泄露的原因。 此外,如果帐户名称的泄漏可能导致损失,则不应将帐户名称输出到日志中。 29 | 30 | #### 5.3.2.6 密码不应该保存在账户管理器中(推荐) 31 | 32 | 两个认证信息,密码和认证令牌可以保存在一个账户中,来注册账户管理器。 这些信息将以明文形式(即不加密)存储在以下目录下的`accounts.db`中。 33 | 34 | + Android 4.1 及之前:`/data/system/accounts.db` 35 | + Android 4.2 及之后:`/data/system/0/accounts.db or /data/system//accounts.db` 36 | 37 | 要阅读`accounts.db`的内容,需要 root 权限或系统权限,并且无法从市场上的 Android 设备中读取它。 在 Android 操作系统中存在漏洞的情况下,攻击者可以获得 root 权限或系统权限,保存在`accounts.db`中的认证信息将处在风险边缘。 38 | 39 | 本文中介绍的认证应用旨在将认证令牌保存在账户管理器中,而不保存用户密码。 在一定时间内连续访问在线服务时,通常认证令牌的有效期限会延长,因此在大多数情况下,不保存密码的设计就足够了。 40 | 41 | 通常,认证令牌的有效期限比密码短,并且它的特点是可以随时禁用。 如果认证令牌泄漏,则可以将其禁用,因此与密码相比,认证令牌比较安全。 在认证令牌被禁用的情况下,用户可以再次输入密码以获得新的认证令牌。 42 | 43 | 如果在密码泄漏时禁用密码,用户将无法再使用在线服务。 在这种情况下,它需要呼叫中心支持等,这将花费巨大的成本。 因此,最好从设计中避免在账户管理器中保存密码。 在不能避免保存密码的设计的情况下,应该采取高级别的逆向工程对策,如加密密码和混淆加密密钥。 44 | 45 | #### 5.3.2.7 HTTPS 应该用于认证器和在线服务之间的通信(必需) 46 | 47 | 密码或认证令牌就是所谓的认证信息,如果被第三方接管,第三方可以伪装成有效用户。 由于认证器使用在线服务来发送/接收这些类型的认证信息,因此应使用可靠的加密通信方法,如 HTTPS。 48 | 49 | #### 5.3.2.8 应该在验证认证器是否正常之后,执行帐户流程(必需) 50 | 51 | 如果有多个认证器在设备中定义了相同的帐户类型,则先前安装的认证器将生效。 所以,安装自己的认证器之后,它不会被使用。 52 | 53 | 如果之前安装的认证器是恶意软件的伪装,则用户输入的帐户信息可能被恶意软件接管。 在执行帐户操作之前,用户应用应验证执行帐户操作的帐户类型,不管是否分配了常规认证器。 54 | 55 | 可以通过检查认证器的包的证书散列值,是否匹配预先确认的有效证书散列值,来验证分配给账户类型的认证器是否是正常的。 如果发现证书哈希值不匹配,则最好提示用户卸载程序包,它包含分配给该帐户类型的意外的认证验证器。 56 | -------------------------------------------------------------------------------- /5.3.3.md: -------------------------------------------------------------------------------- 1 | ### 5.3.3 高级话题 2 | 3 | #### 5.3.3.1 账户管理和权限的使用 4 | 5 | 要使用`AccountManager`类的每种方法,都需要在应用的`AndroidManifest.xml`中分别声明使用相应的权限。 表 5.3-1 显示了权限和方法的对应关系。 6 | 7 | 表 5.3-1 账户管理器的函数以及权限 8 | 9 | | | 账户管理器提供的函数 | | 10 | | --- | --- | ---- | 11 | | 权限 | 方法 | 解释 | 12 | | `AUTHENTICATE_ACCOUNTS`(只有由认证器的相同密钥签名的软件包才可以使用。) | `getPassword()` | 获取密码 | 13 | | | `getUserData()` | 获取用户信息 | 14 | | | `addAccountExplicitly()` | 将账户添加到 DB | 15 | | | `peekAuthToken()` | 获取缓存令牌 | 16 | | | `setAuthToken()` | 注册认证令牌 | 17 | | | `setPassword()` | 修改密码 | 18 | | | `setUserData()` | 设置用户数据 | 19 | | | `renameAccount()` | 重命名账户 | 20 | | `GET_ACCOUNTS` | `getAccounts()` | 获取所有账户的列表 | 21 | | | `getAccountsByType()` | 获取指定类型的账户列表 | 22 | | | `getAccountsByTypeAndFeatures()` | 获取带有特定特性的账户列表 | 23 | | | `addOnAccountsUpdatedListener()` | 注册事件监听器 | 24 | | | `hasFeatures()` | 它是否具有特定功能 | 25 | | `MANAGE_ACCOUNTS` | `getAuthTokenByFeatures()` | 获取带有特定功能的账户的认证令牌 | 26 | | | `addAccount()` | 请求用户添加账户 | 27 | | | `removeAccount()` | 移除账户 | 28 | | | `clearPassword()` | 初始化密码 | 29 | | | `updateCredentials()` | 请求用户修改密码 | 30 | | | `editProperties()` | 修改认证设置 | 31 | | | `confirmCredentials()` | 请求用户再次输入密码 | 32 | | `USE_CREDENTIALS` | `getAuthToken()` | 获取认证令牌 | 33 | | | `blockingGetAuthToken()` | 获取认证令牌 | 34 | | `MANAGE_ACCOUNTS`或`USE_CREDENTIALS` | `invalidateAuthToken()` | 删除缓存令牌 | 35 | 36 | 在使用需要`AUTHENTICATE_ACCOUNTS`权限的方法组的情况下,存在软件包的签名密钥以及权限相关的限制。 具体来说,提供认证器的包的签名密钥,和使用方法的应用的包的签名密钥应该是相同的。 因此,在分发使用方法组的应用时,除了认证器之外,必须使用`AUTHENTICATE_ACCOUNTS`权限,并且应使用认证器的相同密钥进行签名。 37 | 38 | 在 Android Studio 的开发阶段,由于固定的调试密钥库可能会被某些 Android Studio 项目共享,开发人员可能只考虑权限而不考虑签名,来实现和测试帐户管理器。 特别是,对于对每个应用使用不同签名密钥的开发人员来说,因为这种限制,在选择用于应用的密钥时要非常小心。 此外,由于`AccountManager`获得的数据包含敏感信息,因此需要小心处理,来减少泄漏或未授权使用的风险。 39 | 40 | #### 5.3.3.2 在 Android 4.0.x 中,用户应用和认证器应用的签名密钥不同时发生的异常 41 | 42 | 认证令牌获取功能是由开发者密钥签发的用户应用所需的,它不同于认证器应用的签名密钥。通过显示 认证令牌许可证屏幕(`GrantCredentialsPermissionActivity`),`AccountManager`验证用户是否授予认证令牌的使用权。但是 Android 4.0.x 的 Android 框架中存在一个错误,只要`AccountManager`打开此屏幕,就会发生异常并且应用被强制关闭 。 (图5.3-3)。 错误的详细信息,请参阅 。 这个 bug 在 Android 4.1.x 及更高版本中无法找到。 43 | 44 | ![](img/5-3-3.jpg) 45 | -------------------------------------------------------------------------------- /5.3.md: -------------------------------------------------------------------------------- 1 | ## 5.3 将内部账户添加到账户管理器 2 | 3 | 账户管理器是 Android OS 的系统,它集中管理帐户信息,是应用访问在线服务和认证令牌所必需的(帐户名称,密码)。 用户需要提前将账户信息注册到账户管理器,当应用尝试访问在线服务时,账户管理器在获得用户权限后,会自动提供应用认证令牌。 账户管理器的优势在于,应用不需要处理极其敏感的信息和密码。 4 | 5 | 使用账户管理器 6 | 的账户管理功能的结构如下图 5.3-1 所示。 “请求应用”是通过获取认证令牌,访问在线服务的应用,这是上述应用。 另一方面,“认证器应用”是账户管理器的功能扩展,并且向账户管理器提供称为认证器的对象,以便账户管理器可集中管理在线服务的账户信息和认证令牌。 请求应用和认证器应用不需要是单独的应用,因此这些应用可以实现为单个应用。 7 | 8 | ![](img/5-3-1.jpg) 9 | 10 | 最初,用户应用(请求应用)和认证器应用的开发人员签名密钥可以是不同的密钥。 但是,Android 框架的错误仅在 Android 4.0.x 设备中存在 ,并且当用户应用和认证期应用的签名密钥不同时,用户应用中会发生异常,并且不能使用内部账户。 以下示例代码没有针对此缺陷实现任何替代方式。 详细信息请参阅“5.3.3.2 在 Android 4.0.x 中,用户应用和认证程序的签名密钥不同时发生的异常”。 11 | -------------------------------------------------------------------------------- /5.4.2.md: -------------------------------------------------------------------------------- 1 | ### 5.4.2 规则书 2 | 3 | 使用 HTTP/S 通信时,遵循以下规则: 4 | 5 | #### 5.4.2.1 必须通过 HTTPS 通信发送/接收敏感信息(必需) 6 | 7 | 在 HTTP 事务中,发送和接收的信息可能被嗅探或篡改,并且连接的服务器可能被伪装。 敏感信息必须通过 HTTPS 通信发送/接收。 8 | 9 | #### 5.4.2.2 必须小心和安全地处理通过 HTTP 接收到的数据(必需) 10 | 11 | HTTP 通信中收到的数据可能由攻击者利用应用的漏洞产生。 因此,你必须假定应用收到任何值和格式的数据,然后小心实现数据处理来处理收到的数据,以免造成任何漏洞。此外,你不应该盲目信任来自 HTTPS 服务器的数据。 由于 HTTPS 服务器可能由攻击者制作,或者收到的数据可能在 HTTPS 服务器的其他位置制作。 请参阅“3.2 小心和安全地处理输入数据”。 12 | 13 | #### 5.4.2.3 `SSLException`必须适当处理,例如通知用户(必需) 14 | 15 | 在 HTTPS 通信中,当服务器证书无效或通信处于中间人攻击下时,`SSLException`会作为验证错误产生。 所以你必须为`SSLException`实现适当的异常处理。 通知用户通信失败,记录故障等,可被认为是异常处理的典型实现。 另一方面,在某些情况下可能不需要特别通知用户。 因为如何处理`SSLException`取决于应用规范和特性,你需要首先考虑彻底后再确定它。 16 | 17 | 如上所述,当`SSLException`产生时,应用可能受到中间人的攻击,所以它不能实现为,试图通过例如 HTTP 的非安全协议再次发送/接收敏感信息。 18 | 19 | #### 5.4.2.4 不要创建自定义的`TrustManager`(必需) 20 | 21 | 仅仅更改用于验证服务器证书的`KeyStore`,就足以通过 HTTPS ,与例如自签名证书的私有证书进行通信。 但是,正如在“5.4.3.3 禁用证书验证的危险代码”中所解释的那样,在因特网上有很多危险的`TrustManager`实现,与用于这种目的的示例代码一样。 通过引用这些示例代码而实现的应用可能有此漏洞。 22 | 23 | 当你需要通过 HTTPS 与私有证书进行通信时,请参阅“5.4.1.3 通过 HTTPS 与私有证书进行通信”中的安全示例代码。 24 | 25 | 当然,自定义的`TrustManager`可以安全地实现,但需要足够的加密处理和加密通信知识,以免执行存在漏洞的代码。 所以这个规则应为(必需)。 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /5.4.md: -------------------------------------------------------------------------------- 1 | ## 5.4 通过 HTTPS 的通信 2 | 3 | 大多数智能手机应用都与互联网上的 Web 服务器通信。 作为通信的方法,我们在这里集中讨论 HTTP 和 HTTPS 的两种方法。 从安全角度来看,HTTPS 通信更为可取。 最近,Google 或 Facebook 等主要 Web 服务已经开始使用 HTTPS 作为默认设置。 4 | 5 | 自 2012 年以来,Android 应用中 HTTPS 通信实现的许多缺陷已被指出。 这些缺陷可能用于访问由服务器证书操作的测试 Web 服务器,这些服务器证书不是由可信的第三方证书机构颁发,而是由私人(以下称为私有证书)颁发。 6 | 7 | 本节将解释 HTTP 和 HTTPS 通信方法,并介绍使用 HTTPS 安全地访问由私有证书操作的 Web 服务器的方法。 -------------------------------------------------------------------------------- /5.5.2.md: -------------------------------------------------------------------------------- 1 | ### 5.5.2 规则书 2 | 3 | 处理隐私策略时,遵循以下规则: 4 | 5 | #### 5.5.2.1 将用户数据的传输限制为最低需求(必需) 6 | 7 | 将使用数据传输到外部服务器或其他目标时,将传输限制在提供服务的最低需求。 特别是,你应该设计为,应用只能访问这些用户数据,用户可以根据应用描述来想象它们的使用目的。 8 | 9 | 例如,用户可以想象,它是个警报应用,但不能访问位置数据。另一方面,如果警报应用可以根据用户的位置发出警报,并将其功能写入应用的描述中,则应用可以访问位置数据。 10 | 11 | 在只需要在应用中访问信息的情况下,避免将信息传输到外部,并采取其他措施来减少无意中泄漏用户数据的可能性。 12 | 13 | #### 5.5.2.2 在首次加载(或应用更新)时,获得广泛同意来传输需要特别细致处理或用户可能难以更改的用户数据(必需) 14 | 15 | 如果应用向外部服务器,传输用户可能难以更改的任何用户数据,或需要特别细致处理的任何用户数据,则应用必须在用户开始使用之前,获得用户的预先同意(选择性加入) - 通知用户哪些类型的信息将被发送到服务器,以及是否会涉及任何第三方厂商。 更具体地说,首次启动时,应用应显示其应用隐私政策并确认该用户已阅读并同意。 此外,无论何时应用更新,通过将新类型的用户数据传输到外部服务器,它都必须再次确认用户已经阅读并同意这些更改。 如果用户不同意,应用应该终止或以其他方式采取措施,来确保所有需要传输数据的功能都被禁用。 16 | 17 | 这些步骤可以确保,用户了解他们在使用应用时如何处理数据,为用户提供安全感并增强他们对应用的信任。 18 | 19 | MainActivity.java 20 | 21 | ```java 22 | protected void onStart() { 23 | super.onStart(); 24 | 25 | // (some portions omitted) 26 | 27 | if (privacyPolicyAgreed <= VERSION_TO_SHOW_COMPREHENSIVE_AGREEMENT_ANEW) { 28 | // *** POINT *** On first launch (or application update), obtain broad consent to transmit user data that will be handled by the application. 29 | // When the application is updated, it is only necessary to renew the user’s grant of broad consent if the updated application will handle new types of user data. 30 | ConfirmFragment dialog = ConfirmFragment.newInstance( 31 | R.string.privacyPolicy, R.string.agreePrivacyPolicy, 32 | DIALOG_TYPE_COMPREHENSIVE_AGREEMENT); 33 | dialog.setDialogListener(this); 34 | FragmentManager fragmentManager = getSupportFragmentManager(); 35 | dialog.show(fragmentManager, "dialog"); 36 | } 37 | ``` 38 | 39 | ![](img/5-5-4.jpg) 40 | 41 | #### 5.5.2.3 在传输需要特殊处理的用户数据之前获得特定的同意(必需) 42 | 43 | 向外部服务器传输任何需要特别细致处理的用户数据时,除了需要获得一般同意之外,应用必须获得用户对每种这类用户数据(或涉及传输用户数据的每个功能)的预先同意(选择性加入)。 如果用户不同意,则应用不得将相应的数据发送到外部服务器。 这确保用户可以更全面地了解应用的功能(及其提供的服务)和用户对其授予一般同意的,用户数据的传输之间的关系;同时,应用提厂商可以基于更精确的决策,预计获得用户的同意。 44 | 45 | MainActivity.java 46 | 47 | ```java 48 | public void onSendToServer(View view) { 49 | // *** POINT *** Obtain specific consent before transmitting user data that requires particularly delicate handling. 50 | ConfirmFragment dialog = ConfirmFragment.newInstance(R.string.sendLocation, R.string.cofi 51 | rmSendLocation, DIALOG_TYPE_PRE_CONFIRMATION); 52 | dialog.setDialogListener(this); 53 | FragmentManager fragmentManager = getSupportFragmentManager(); 54 | dialog.show(fragmentManager, "dialog"); 55 | } 56 | ``` 57 | 58 | ![](img/5-5-5.jpg) 59 | 60 | #### 5.5.2.4 向用户提供查看应用隐私策略的方法(必需) 61 | 62 | 一般来说,Android 应用市场将提供应用隐私策略的链接,供用户在选择安装相应的应用之前进行复查。 除了支持此功能之外,应用还需要提供一些方法,用户在设备上安装应用后,可以查看应用隐私策略。 特别重要的是提供一些方法,用户可以轻易复查应用隐私政策。在同意的情况下,将用户数据传输到外部服务器来协助用户作出适当决定。 63 | 64 | MainActivity.java 65 | 66 | ```java 67 | @Override 68 | public boolean onOptionsItemSelected(MenuItem item) { 69 | switch (item.getItemId()) { 70 | case R.id.action_show_pp: 71 | // *** POINT *** Provide methods by which the user can review the application privacy policy. 72 | Intent intent = new Intent(); 73 | intent.setClass(this, WebViewAssetsActivity.class); 74 | startActivity(intent); 75 | return true; 76 | ``` 77 | 78 | ![](img/5-5-6.jpg) 79 | 80 | #### 5.5.2.5 在素材文件夹中放置应用隐私策略的摘要版本(推荐) 81 | 82 | 将应用隐私策略的摘要版本放在素材文件夹中,来确保用户可以按需对其进行复查,这是一个不错的主意。 确保素材文件夹中存在应用隐私策略,不仅可以让用户随时轻松访问它,还可以避免用户看到由恶意第三方准备的应用隐私策略的伪造或损坏版本的风险。 83 | 84 | #### 5.5.2.6 提供可以删除传输的数据的方法,以及可以通过用户操作停止数据传输的方法(推荐) 85 | 86 | 提供根据用户需要,删除传输到外部服务器的用户数据的方法,是一个好主意。与之相似,在应用本身已经在设备内存储用户数据(或其副本)的情况下,向用户提供用于删除该数据的方法是一个好主意。而且,提供可以根据用户要求停止用户数据发送的方法,是一个好主意。 87 | 88 | 这一规则(建议)由欧盟推行的“被遗忘权”编纂而成;更普遍的是,在未来,各种提案将要求进一步加强用户保护其数据的权利,这看起来很明显。为此在这些指导方针中,我们建议提供删除用户数据的方法,除非有一些具体原因不能这样做。并且,停止数据传输,主要由浏览器的对应观点“不追踪(否定追踪)”定义。 89 | 90 | 91 | MainActivity.java 92 | 93 | ```java 94 | 95 | @Override 96 | public boolean onOptionsItemSelected(MenuItem item) { 97 | switch (item.getItemId()) { 98 | (some portions omitted) 99 | 100 | case R.id.action_del_id: 101 | // *** POINT *** Provide methods by which transmitted data can be deleted by user 102 | operations. 103 | new SendDataAsyncTack().execute(DEL_ID_URI, UserId); 104 | return true; 105 | } 106 | ``` 107 | 108 | #### 5.5.2.7 从 UUID 和 Cookie 中分离设备特定的 ID(推荐) 109 | 110 | 不应通过与用户数据绑定的方式传输 IMEI 和其他设备特定 ID。 事实上,如果一个设备特定的 ID 和一段用户数据被捆绑在一起,并发布或泄露给公众 - 即使只有一次 - 随后也不可能改变该设备特定的 ID,因此对于把 ID 和用户数据绑定的服务器来说,这是不可能的(或至少 很难)。 在这种情况下,最好使用 UUID 或 cookie(即每次基于随机数重新生成的变量 ID),与用户数据一起传输时代替设备特定的 ID。 这允许实现上面讨论的“被遗忘的权利”的概念。 111 | 112 | MainActivity.java 113 | 114 | ```java 115 | @Override 116 | protected String doInBackground(String... params) { 117 | // *** POINT *** Use UUIDs or cookies to keep track of user data 118 | // In this sample we use an ID generated on the server side 119 | SharedPreferences sp = getSharedPreferences(PRIVACY_POLICY_PREF_NAME, MODE_PRIVATE); 120 | UserId = sp.getString(ID_KEY, null); 121 | if (UserId == null) { 122 | // No token in SharedPreferences; fetch ID from server 123 | try { 124 | UserId = NetworkUtil.getCookie(GET_ID_URI, "", "id"); 125 | } catch (IOException e) { 126 | // Catch exceptions such as certification errors 127 | extMessage = e.toString(); 128 | } 129 | // Store the fetched ID in SharedPreferences 130 | sp.edit().putString(ID_KEY, UserId).commit(); 131 | } 132 | return UserId; 133 | } 134 | ``` 135 | 136 | #### 5.5.2.8 如果你只在设备内使用用户数据,请通知用户,数据不会传输到外部(推荐) 137 | 138 | 即使在用户数据只在用户设备中临时访问的情况下,向用户传达这一事实也是一个好主意,来确保用户充分和透明地理解了应用行为。 更具体来说,应该告知用户,应用访问的用户数据只在设备内用于特定的目的,不会被存储或发送。 将此内容传达给用户的可能方法,包括在应用市场上的应用描述中指定它。 仅在设备中临时使用的信息,不需要在应用隐私策略中讨论。 139 | 140 | ![](img/5-5-7.jpg) 141 | -------------------------------------------------------------------------------- /5.5.3.md: -------------------------------------------------------------------------------- 1 | ### 5.5.3 高级话题 2 | 3 | #### 5.3.3.1 隐私政策的背景和上下文 4 | 5 | 对于智能手机应用获取用户数据,并向外传输该数据的情况,需要准备并显示应用隐私策略,来通知用户一些详细信息,例如收集的数据类型,以及数据被处理的方式。 应包含在应用隐私政策中的内容,在 JMIC SPI 所倡导的 Smartphone Privacy Initiative 中详细说明。 应用隐私策略的主要目标应该是,清楚地声明应用将访问的用户数据的所有类型,数据将用于何种用途,数据将存储在何处以及数据将发送到哪里。 6 | 7 | 除了应用隐私策略之外,另一个文档是企业隐私策略,它详细说明了公司从各种应用收集的所有用户数据将如何存储,管理和处置。 企业隐私政策对应隐私政策,传统上用于遵循日本个人信息保护法。 8 | 9 | 准备和展示隐私政策的适当方法的详细说明,以及各种不同类型的隐私政策所起的作用的讨论,可参见文件“对 JSSEC 智能手机创建和展示应用隐私政策的讨论”,可从以下 URL 获得:(只有日语)。 10 | 11 | #### 5.5.3.2 术语表 12 | 13 | 在下表中,我们定义了这些准则中使用的许多术语;这些定义摘自文件“对 JSSEC 智能手机创建和展示应用隐私政策的讨论”()(只有日语)。 14 | 15 | | 术语 | 描述 | 16 | | --- | --- | 17 | | 企业隐私政策 | 为保护个人数据而定义的企业政策。 根据日本的个人信息保护法创建。 | 18 | | 应用隐私政策 | 特定于应用的隐私策略。 根据日本内务和通信部(MIC)的智能手机隐私计划(SPI)的指导原则创建。 最好提供摘要,和包含容易理解的解释的详细版本。 | 19 | | 应用隐私政策的摘要版本 | 一份简要文件,简要概述了应用将使用哪些用户信息,用于何种目的,以及这些信息是否会提供给第三方。 | 20 | | 应用隐私政策的详细版本 | 这是一份详细的文件,符合智能手机隐私计划(SPI)和日本总务省(MIC)的智能手机隐私计划 II(SPI II)规定的 8 项内容。 | 21 | | 用户易于更改的用户数据 | Cookie,UUID,以及其他。 | 22 | | 用户难以更改的用户数据 | IMEIs, IMSIs, ICCIDs, MAC 地址, OS 生成的 ID, 以及其他。 | 23 | | 需要特别处理的用户数据 | 位置信息,地址本,电话号码,邮箱地址,以及其他。 | 24 | 25 | -------------------------------------------------------------------------------- /5.5.md: -------------------------------------------------------------------------------- 1 | ## 5.5 处理隐私数据 2 | 3 | 近年来,“隐私设计”概念已被提出,作为保护隐私数据的全球趋势。基于这一概念,各国政府正在推动隐私保护立法。 4 | 5 | 在智能手机中使用用户数据的应用必须采取措施,来确保用户可以安全地使用应用,而不必担心隐私和个人数据。这些步骤包括适当处理用户数据,并要求用户选择应用是否可以使用某些数据。为此,每个应用必须准备并显示应用隐私策略,指明应用将使用哪些信息,以及如何使用该信息;而且,在获取和使用某些信息时,应用必须首先向用户请求许可。请注意,应用隐私政策与过去可能存在的其他文档(例如“个人数据保护政策”或“使用条款”)不同,且必须与任何此类文档分开创建。 6 | 7 | 创建和执行隐私政策的详细信息,请参见日本总务省(MIC)发布的文档《Smartphone Privacy Initiative》和《Smartphone Privacy Initiative II》(JMIC 的 SPI)。 8 | 9 | 本节中使用的术语在“5.5.3.2 术语表”中以文本定义。 10 | 11 | -------------------------------------------------------------------------------- /5.6.2.md: -------------------------------------------------------------------------------- 1 | ### 5.6.2 规则书 2 | 3 | 使用加密技术时,遵循以下规则: 4 | 5 | #### 5.6.2.1 指定加密算法时,请显式指定加密模式和填充(必需) 6 | 7 | 在使用加密技术和数据验证等密码学技术时,加密模式和填充必须显式指定。 在 Android 应用开发中使用加密时,你将主要使用`java.crypto`中的`Cipher`类。 为了使用`Cipher`类,你将首先通过指定要使用的加密类型,来创建`Cipher`类对象的实例。 这个指定被称为转换,并且有两种格式可以指定转换: 8 | 9 | + 算法/模式/填充 10 | + 算法 11 | 12 | 在后一种情况下,加密模式和填充将隐式设置为 Android 可以访问的加密服务供应器的适当默认值。 这些默认值优先考虑便利性和兼容性而选择,并且在某些情况下可能不是特别安全的选择。 为此,为了确保正确的安全保护,必须使用两种格式中的前者,其中显式指定了加密模式和填充。 13 | 14 | #### 5.6.2.2 使用强算法(特别是符合相关标准的算法)(必需) 15 | 16 | 使用加密技术时,选择符合特定标准的强算法很重要。 此外,在算法允许多个密钥长度的情况下,重要的是要考虑应用的整个产品生命周期,并选择足以确保安全性的密钥长度。 此外,对于一些加密模式和填充模式,存在已知的攻击策略;对这些威胁做出有力的选择是非常重要的。 17 | 18 | 19 | 确实,选择弱加密方法会造成灾难性后果。 例如,被加密来防止第三方窃听的文件,实际上可能仅受到无效保护,并且可能允许第三方窃听。 由于 IT 的不断进步导致加密分析技术的持续改进,因此至关重要的是,考虑并选择一个算法,它能够在运行的整个期间,保证安全性。在此时间,你希望应用保持运行。 20 | 21 | 实际加密技术的标准因国家而异,详见下表(单位:位)。 22 | 23 | 表 5.6-1 NIST(USA) NIST SP800-57 24 | 25 | | 算法生命周期 | 对称密钥加密 | 非对称密钥加密 | 椭圆曲线加密 | HASH(数字签名) | HASH(随机数生成) | 26 | | --- | --- | --- | --- | --- | --- | 27 | | ~2010 | 80 | 1024 | 160 | 160 | 160 | 28 | | ~2030 | 112 | 2048 | 224 | 224 | 160 | 29 | | 2030~ | 128 | 3072 | 256 | 256 | 160 | 30 | 31 | 表 5.6-2 ECRYPT II (EU) 32 | 33 | | 算法生命周期 | 对称密钥加密 | 非对称密钥加密 | 椭圆曲线加密 | HASH | 34 | | --- | --- | --- | --- | --- | 35 | | 2009~2012 | 80 | 1248 | 160 | 160 | 36 | | 2009~2020 | 96 | 1776 | 192 | 192 | 37 | | 2009~2030 | 112 | 2432 | 224 | 224 | 38 | | 2009~2040 | 128 | 3248 | 256 | 256 | 39 | | 2009~ | 256 | 15424 | 512 | 512 | 40 | 41 | 表 5.6-3 CRYPTREC(Japan) CRYPTREC 加密算法列表 42 | 43 | | 技术族 | | 名称 | 44 | | --- | --- | --- | 45 | | 公钥加密 | 签名 | DSA,ECDSA,RSA-PSS,RSASSA-PKCS1-V1_5 | 46 | | | 机密性 | RSA-OAEP | 47 | | | 密钥共享 | DH,ECDH | 48 | | 共享密钥加密 | 64 位块加密 | 3-key Triple DES | 49 | | | 128 位块加密 | AES,Camellia | 50 | | | 流式加密 | KCipher-2 | 51 | | 哈希函数 | | SHA-256,SHA-384,SHA-512 | 52 | | 加密使用模式 | 密文模式 | CBC,CFB,CTR,OFB | 53 | | | 认证密文模式 | CCM,GCM | 54 | | 消息认证代码 | | CMAC,HMAC | 55 | | 实体认证 | | ISO/IEC 9798-2,ISO/IEC 9798-3 | 56 | 57 | #### 5.6.2.3 使用基于密码的加密时,不要在设备上存储密码(必需) 58 | 59 | 在基于密码的加密中,当根据用户输入的密码生成加密密钥时,请勿将密码存储在设备中。 基于密码的加密的优点是无需管理加密密钥;将密码存储在设备上消除了这一优势。 无需多说,在设备上存储密码会产生其他应用窃听的风险,因此出于安全原因,在设备上存储密码也是不可接受的。 60 | 61 | #### 5.6.2.4 从密码生成密钥时,使用盐(必需) 62 | 63 | 在基于密码的加密中,当根据用户输入的密码生成加密密钥时,请始终使用盐。 另外,如果你要在同一设备中为不同用户提供功能,请为每个用户使用不同的盐。 原因是,如果你仅使用简单的哈希函数生成加密密钥而不使用盐,则可以使用称为“彩虹表”的技术轻松恢复密码。使用了盐时,会使用相同的密码生成的密钥 将是不同的(不同的哈希值),防止使用彩虹表来搜索密钥。 64 | 65 | 示例: 66 | 67 | ```java 68 | public final byte[] encrypt(final byte[] plain, final char[] password) { 69 | byte[] encrypted = null; 70 | try { 71 | // *** POINT *** Explicitly specify the encryption mode and the padding. 72 | // *** POINT *** Use strong encryption methods (specifically, technologies that meet the relevant criteria), including algorithms, block cipher modes, and padding modes. 73 | Cipher cipher = Cipher.getInstance(TRANSFORMATION); 74 | // *** POINT *** When generating keys from passwords, use Salt. 75 | SecretKey secretKey = generateKey(password, mSalt); 76 | ``` 77 | 78 | #### 5.6.2.5 从密码生成密钥时,指定适当的哈希迭代计数(必需) 79 | 80 | 在基于密码的加密中,当根据用户输入的密码生成加密密钥时,你需要选择在密钥生成过程(“拉伸”)中,散列过程的重复次数;指定足够大的数字来确保安全性非常重要。一般来说,1,000 或更大的迭代次数是足够的。如果你使用密钥来保护更有价值的资产,请指定 1,000,000 或更高的计数。由于散列函数的单个计算所需的处理时间很少,因此攻击者可能很容易进行爆破攻击。因此,通过使用拉伸方法(其中散列处理重复多次),我们可以有意确保该过程消耗大量时间,因此爆破攻击的成本更高。请注意,拉伸重复次数也会影响应用的处理速度,因此请谨慎选择合适的值。 81 | 82 | 示例: 83 | 84 | ```java 85 | private static final SecretKey generateKey(final char[] password, final byte[] salt) { 86 | SecretKey secretKey = null; 87 | PBEKeySpec keySpec = null; 88 | 89 | (Omit) 90 | 91 | // *** POINT *** When generating a key from password, use Salt. 92 | // *** POINT *** When generating a key from password, specify an appropriate hash iteration count. 93 | // *** POINT *** Use a key of length sufficient to guarantee the strength of encryption. 94 | keySpec = new PBEKeySpec(password, salt, KEY_GEN_ITERATION_COUNT, KEY_LENGTH_BITS); 95 | ``` 96 | 97 | #### 5.6.2.6 采取措施来增加密码强度(推荐) 98 | 99 | 在基于密码的加密中,当基于用户输入的密码生成加密密钥时,生成的密钥的强度受用户密码强度的强烈影响,因此值得采取措施来加强从用户那里收到的密码。 例如,你可以要求密码长度至少为 8 个字符,并且包含多种类型的字符 - 可能至少包含一个字母,一个数字和一个符号。 100 | 101 | -------------------------------------------------------------------------------- /5.6.3.md: -------------------------------------------------------------------------------- 1 | ### 5.6.3 高级话题 2 | 3 | #### 5.6.3.1 选择加密方法 4 | 5 | 在上面的示例代码中,我们展示了三种加密方法的实现示例,每种加密方法用于加密解密以及数据伪造的检测。 你可以使用“图 5.6-1”,“图 5.6-2”,根据你的应用粗略选择使用哪种加密方法。 另一方面,加密方法的更加精细的选择,需要更详细地比较各种方法的特征。 在下面我们考虑一些这样的比较。 6 | 7 | 用于加密和解密的密码学方法的比较 8 | 9 | 公钥密码术具有很高的处理成本,因此不适合大规模数据处理。但是,因为用于加密和解密的密钥不同,所以仅仅在应用侧处理公钥(即,只执行加密),并且在不同(安全)位置执行解密的情况下,管理密钥相对容易。共享密钥加密是一种通用的加密方案,但限制很少,但在这种情况下,相同的密钥用于加密和解密,因此有必要将密钥安全地存储在应用中,从而使密钥管理变得困难。基于密码的密钥系统(基于密码的共享密钥系统)通过用户指定的密码生成密钥,避免了在设备中存储密钥相关的密码的需求。此方法用于仅仅保护用户资产,但不保护应用资产的应用。由于加密强度取决于密码强度,因此有必要选择密码,其复杂度与要保护的资产价值成比例增长。请参阅“5.6.2.6 采取措施来增加密码强度(推荐)”。 10 | 11 | 表 5.6-4 用于加密和解密的密码学方法的比较 12 | 13 | | 条目/加密方法 | 公钥 | 共享密钥 | 基于密码 | 14 | | --- | --- | --- | --- | 15 | | 处理大规模数据 | 否(开销太大) | OK | OK | 16 | | 保护应用(或服务)资产 | OK | OK | 否(允许用户窃取) | 17 | | 保护用户资产 | OK | OK | OK | 18 | | 加密强度 | 取决于密钥长度 | 取决于密钥长度 | 取决于密码强度,盐和哈希重复次数 | 19 | | 密钥存储 | 简单(仅公钥) | 困难 | 简单 | 20 | | 由应用执行的过程 | 加密(解密在服务器或其它地方完成) | 加密和解密 | 加密和解密 | 21 | 22 | 用于检测数据伪造的密码学方法的比较 23 | 24 | 这里的比较与上面讨论的加密和解密类似,除了与数据大小对应的条目不再相关。 25 | 26 | 表 5.6-5 用于检测数据伪造的密码学方法的比较 27 | 28 | | 条目/加密方法 | 公钥 | 共享密钥 | 基于密码 | 29 | | --- | --- | --- | --- | 30 | | 保护应用(或服务)资产 | OK | OK | 否(允许用户伪造) | 31 | | 保护用户资产 | OK | OK | OK | 32 | | 加密强度 | 取决于密钥长度 | 取决于密钥长度 | 取决于密码强度,盐和哈希重复次数 | 33 | | 密钥存储 | 简单(仅公钥) | 困难,请参考“5.6.3.4 保护密钥” | 简单 | 34 | | 由应用执行的过程 | 签名验证(签名在服务器或其它地方完成) | MAC 计算和验证 | MAC 计算和验证 | 35 | 36 | MAC:消息认证代码 37 | 38 | 39 | 请注意,这些准则主要关注被视为低级或中级资产的资产保护,根据“3.1.3 资产分类和保护对策”一节中讨论的分类。 由于使用加密涉及的问题,比其他预防性措施(如访问控制)更多,如密钥存储问题,因此只有资产不能在 Android 操作系统安全模式下有效保护时,才应该考虑加密。 40 | 41 | #### 5.6.3.2 随机数的生成 42 | 43 | 使用加密技术时,选择强加密算法和加密模式,以及足够长的密钥,来确保应用和服务处理的数据的安全性,这非常重要。 然而,即使所有这些选择都做得适当,当形成安全协议关键的密钥被泄漏或猜测时,所使用的算法所保证的安全强度立即下降为零。 44 | 45 | 即使对于在AES和类似协议下,用于共享密钥加密的初始向量(IV),或者用于基于密码的加密的盐,较大偏差也可以使第三方轻松发起攻击,从而增加数据泄漏或污染的风险 。 为了防止这种情况,有必要以第三方难以猜测它们的值的方式,产生密钥和 IV,而随机数在确保这一必要实现的方面,起着非常重要的作用。 产生随机数的设备称为随机数生成器。 尽管硬件随机数生成器(RNG)可能使用传感器或其他设备,通过测量无法预测或再现的自然现象来产生随机数,但更常见的是用软件实现的随机数生成器,称为伪随机数生成器(PRNG)。 46 | 47 | 在Android应用中,可以通过`SecureRandom`类生成用于加密的足够安全的随机数。 `SecureRandom`类的功能由一个称为`Provider`的实现提供。 多个供应器(实现)可以在内部存在,并且如果没有明确指定供应器,则会选择默认供应器。 出于这个原因,也可以在不知道供应器存在的情况下,使用`SecureRandom`来实现。 在下面,我们提供的例子演示了如何使用`SecureRandom`。 48 | 49 | 请注意,根据 Android 版本的不同,`SecureRandom`可能存在一些缺陷,需要在实施中采取预防措施。 请参阅“5.6.3.3 防止随机数生成器中的漏洞的措施”。 50 | 51 | 使用`SecureRandom`(默认实现) 52 | 53 | ```java 54 | import java.security.SecureRandom; 55 | 56 | [...] 57 | 58 | SecureRandom random = new SecureRandom(); 59 | byte[] randomBuf = new byte [128]; 60 | random.nextBytes(randomBuf); 61 | 62 | [...] 63 | ``` 64 | 65 | 使用`SecureRandom`(明确的特定算法) 66 | 67 | ```java 68 | import java.security.SecureRandom; 69 | 70 | [...] 71 | 72 | SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); 73 | byte[] randomBuf = new byte [128]; 74 | random.nextBytes(randomBuf); 75 | 76 | [...] 77 | ``` 78 | 79 | 使用`SecureRandom`(明确的特定实现(供应器)) 80 | 81 | ```java 82 | import java.security.SecureRandom; 83 | 84 | [...] 85 | 86 | SecureRandom random = SecureRandom.getInstance("SHA1PRNG", “Crypto”); 87 | byte[] randomBuf = new byte [128]; 88 | random.nextBytes(randomBuf); 89 | 90 | [...] 91 | ``` 92 | 93 | 程序中发现的伪随机数发生器,例如`SecureRandom`,通常基于一些基本过程来操作,如“图 5.6-3 伪随机数发生器的内部过程”中所述。 输入一个随机数种子来初始化内部状态;此后,每次生成随机数时更新内部状态,从而允许生成随机数序列。 94 | 95 | 随机数种子 96 | 97 | 种子在伪随机数发生器(PRNG)中起着非常重要的作用。 如上所述,PRNG 必须通过指定种子来初始化。 此后,用于生成随机数的过程是确定性算法,因此如果指定相同的种子,则会得到相同的随机数序列。 这意味着如果第三方获得(即窃听)或猜测 PRNG 的种子,他可以产生相同的随机数序列,从而破坏随机数提供的机密性和完整性属性。 98 | 99 | 出于这个原因,随机数生成器的种子本身就是一个高度机密的信息 - 而且必须以无法预测或猜测的方式来选择。 例如,不应使用时间信息或设备特定数据(例如 MAC 地址,IMEI 或 Android ID)来构建 RNG 种子。 在许多 Android 设备上,`/dev/urandom`或`/dev/random`可用,Android 提供的`SecureRandom`默认实现使用这些设备文件,来确定随机数生成器的种子。 就机密性而言,只要 RNG 种子仅存在于内存中,除获得 root 权限的恶意软件工具外,几乎没有由第三方发现的风险。 如果你需要实现,即使在已 root 的设备上仍然有效的安全措施,请咨询安全设计和实现方面的专家。 100 | 101 | 伪随机数生成器的内部状态 102 | 103 | 伪随机数发生器的内部状态由种子初始化,然后在每次生成随机数时更新。 就像由相同种子初始化的 PRNG 一样,具有相同内部状态的两个 PRNG 随后将产生完全相同的随机数序列。 因此,保护内部状态免受第三方窃听也很重要。 但是,由于内部状态存在于内存中,除了拥有 root 访问权的恶意软件工具外,几乎没有发现任何第三方的风险。 如果你需要实现,即使在已 root 的设备上仍然有效的安全措施,请咨询安全设计和实现方面的专家。 104 | 105 | #### 5.6.3.3 防范随机数生成器中的漏洞的措施 106 | 107 | 108 | 在 Android 4.3.x 及更早版本中发现,`SecureRandom`的`Crypto`供应器实现拥有内部状态熵(随机性)不足的缺陷。 特别是在 Android 4.1.x 及更早版本中,`Crypto`供应器是`SecureRandom`的唯一可用实现,因此大多数直接或间接使用`SecureRandom`的应用都受此漏洞影响。 同样,Android 4.2 和更高版本中,作为`SecureRandom`的默认实现而提供的`AndroidOpenSSL`供应器拥有这个缺陷,由`OpenSSL`使用的作为随机数种子的大部分数据在应用之间共享(Android 4.2.x-4.3 .x),产生了一个漏洞,任何应用都可以轻松预测其他应用生成的随机数。 下表详细说明了各种 Android OS 版本中存在的漏洞的影响。 109 | 110 | 表 5.6-6 Android操作系统版本和受到每个漏洞的影响的功能 111 | 112 | | Android OS/漏洞 | `SecureRandom`的`Crypto`供应器实现的内部状态熵不足 | 可以猜测其他程序中`OpenSSL`所使用的随机数 | 113 | | --- | --- | --- | 114 | | 4.1.x 及之前 | `SecureRandom`的默认实现,`Crypto`供应器的显式使用,由`Cipher`类提供的加密功能,HTTPS 通信功能等 | 无影响 | 115 | | 4.2 - 4.3.x | 使用明确标识的`Crypto`供应器 | `SecureRandom`的默认实现,Android OpenSSL 供应器的显式使用,`OpenSSL`提供的随机数生成功能的直接使用,由`Cipher`类提供的加密功能,HTTPS 通信功能等 | 116 | | 4.4 及之后 | 无影响 | 无影响 | 117 | 118 | 自 2013 年 8 月以来,Google 已经向其合作伙伴(设备制造商等),分发了用于消除这些 Android 操作系统漏洞的补丁。但是,与`SecureRandom`相关的这些漏洞影响了广泛的应用,包括加密功能和 HTTPS 通信功能,并且据推测许多设备仍未修补。 因此,在设计针对 Android 4.3.x 和更早版本的应用时,我们建议你采纳以下站点中讨论的对策(实现)。 119 | 120 | 121 | 122 | #### 5.6.3.4 密钥保护 123 | 124 | 使用加密技术来确保敏感数据的安全性(机密性和完整性)时,只要密钥本身的数据内容是可用的,即使最健壮的加密算法和密钥长度,也不能保护数据免受第三方攻击。 出于这个原因,正确处理密钥是使用加密时需要考虑的最重要的项目之一。 当然,根据你尝试保护的资产的级别,正确处理密钥可能需要非常复杂的设计和实现技术,这些技术超出了本指南的范围。 在这里,我们只能提供一些基本想法,有关安全处理各种应用和密钥的存储位置; 我们的讨论没有扩展到特定的实现方法,并且必要时我们建议你咨询安全设计和实现方面的专家。 125 | 126 | 首先,“图 5.6-4 加密密钥的位置和保护它们的策略”,说明了 Android 智能手机和平板电脑中,用于储存密钥和相关用途的各种位置,并概述了保护它们的策略。 127 | 128 | ![](img/5-6-4.jpg) 129 | 130 | 下表总结了受密钥保护的资产的资产类别,以及适用于各种资产所有者的保护策略。 资产类别的更多信息,请参阅“3.1.3 资产分类和保护对策”。 131 | 132 | 表 5.6-7 资产分类和保护对策 133 | 134 | | 资产所有者 | 设备用户 | | 应用/服务供应者 | | 135 | | --- | --- | --- | --- | --- | 136 | | 资产级别 | 高 | 中低 | 高 | 中低 | 137 | | 密钥储存位置 | | 保护策略 | | | 138 | | 用户内存 | 提高密码强度 | | 不允许使用用户密码 | | 139 | | 应用目录(非公共存储) | 密钥加密或混淆 | 禁止来自应用外部的读写操作 | 密钥加密或混淆 | 禁止来自应用外部的读写操作 | 140 | | APK 文件 | | 混淆密钥数据。注:要注意大多数 Java 混淆工具,例如 Proguard,不会混淆数据字符串。 | | | 141 | | SD 卡或者其它(公共存储) | | 加密或混淆密钥数据 | | | 142 | 143 | 在下文中,我们讨论适用于存储密钥的各个地方的保护措施。 144 | 145 | 储存在用户内存中的密钥 146 | 147 | 这里我们考虑基于密码的加密。 从密码生成密钥时,密钥存储位置是用户内存,因此不存在由于恶意软件而造成泄漏的危险。 但是,根据密码的强度,可能很容易重现密钥。 出于这个原因,有必要采取步骤来确保密码的强度, 类似于让用户指定服务登录密码时采取的步骤;例如,密码可能受到 UI 的限制,或者可能会使用警告消息。 请参阅“5.6.2.6 采取措施增加密码的强度(推荐)”。 当然,当密码存储在用户用户中时,必须记住密码将被遗忘的可能性。 为确保在忘记密码的情况下可以恢复数据,必须将备份数据存储在设备以外的安全位置(例如服务器上)。 148 | 149 | 储存在应用目录中的密钥 150 | 151 | 当密钥以私有模式,存储在应用目录中时,密钥数据不能被其他应用读取。 另外,如果应用禁用备份功能,用户也将无法访问数据。 因此,当存储用于保护应用资产的密钥时,应该禁用备份。 152 | 153 | 但是,如果你还需要针对使用 root 权限的应用或用户保护密钥,则必须对密钥进行加密或混淆。 对于用于保护用户资产的密钥,你可以使用基于密码的加密。 对于用于加密应用资产的密钥,你希望这些资产对于用户是不可见的,你必须将用于资产加密的密钥存储在 APK 文件中,并且必须对密钥数据进行混淆处理。 154 | 155 | 储存在 APK 文件中的密钥 156 | 157 | 由于可以访问APK文件中的数据,因此通常这不适合存储机密数据(如密钥)。 在 APK 文件中存储密钥时,你必须对密钥数据进行混淆处理,并采取措施确保数据无法轻易从 APK 文件中读取。 158 | 159 | 储存在公共存储位置(例如 SD 卡)的密钥 160 | 161 | 由于公共存储可以被所有应用访问,因此通常它不适合存储机密数据(如密码)。 将密钥存储在公共位置时,需要对密钥数据进行加密或混淆处理,来确保无法轻易访问数据。 另请参阅上面的“存储在应用目录中的密钥”中提出的保护措施,来了解还必须针对具有 root 权限的应用或用户来保护密钥。 162 | 163 | 在进程内存中处理密钥 164 | 165 | 使用 Android 中可用的加密技术时,必须在加密过程之前,在上图中所示的应用进程以外的地方,对加密或混淆的密钥数据进行解密(或者,对于基于密码的密钥,则需要生成密钥)。在这种情况下,密钥数据将以未加密的形式驻留在进程内存中。另一方面,应用的内存通常不会被其他应用读取,因此如果资产类别位于这些准则涵盖的范围内,则没有采取特定步骤来确保安全性的特别需求。在密钥数据以未加密的形式出现(即使它们以这种方式存在于进程内存中)是不可接受的的情况下,由于特定目标或由应用处理的资产级别,可能有必要对密钥数据和加密逻辑,采取混淆处理或其他技术。但是,这些方法在 Java 层面上难以实现;相反,你将在 JNI 层面上使用混淆工具。这些措施不在本准则的范围之内;咨询安全设计和实现方面的专家。 166 | 167 | #### 5.6.3.5 通过 Google Play 服务解决安全供应器的漏洞 168 | 169 | Google Play 服务(5.0 和更高版本)提供了一个称为供应器安装器的框架,可用于解决安全供应器中的漏洞。 170 | 171 | 首先,安全提供应器提供了基于 Java 密码体系结构(JCA)的各种加密相关的算法的实现。 这些安全供应器算法可以通过诸如`Cipher`,`Signature`和`Mac`等类来使用,来在 Android 应用中使用加密技术。 一般来说,只要在加密技术相关的实现中发现漏洞,就需要快速响应。 事实上,以恶意目的利用这些漏洞可能会导致严重损害。 由于加密技术也与安全供应器相关,所以希望用于解决漏洞的修订越快越好。 172 | 173 | 执行安全供应器修订的最常见方法是使用设备更新。通过设备更新执行修订的过程,起始于设备制造商准备更新,之后用户将此更新应用于其设备。因此,应用是否可以访问安全供应器的最新版本(包括最新版本),实际上取决于制造商和用户的遵从性。相反,使用来自 Google Play 服务的供应器安装器,可确保应用可以访问自动更新的安全供应器版本。 174 | 175 | 使用来自 Google Play 服务的供应器安装器,通过从应用调用供应器安装器,可以访问由 Google Play 服务提供的安全供应器。 Google Play 服务会通过 Google Play 商店自动更新,因此供应器安装器所提供的安全供应器,将自动更新到最新版本,而不依赖制造商或用户的遵从性。 176 | 177 | 调用供应器安装器的示例代码如下所示。 178 | 179 | 调用供应器安装器 180 | 181 | ```java 182 | import com.google.android.gms.common.GooglePlayServicesUtil; 183 | import com.google.android.gms.security.ProviderInstaller; 184 | 185 | public class MainActivity extends Activity 186 | implements ProviderInstaller.ProviderInstallListener { 187 | 188 | @Override 189 | protected void onCreate(Bundle savedInstanceState) { 190 | super.onCreate(savedInstanceState); 191 | ProviderInstaller.installIfNeededAsync(this, this); 192 | setContentView(R.layout.activity_main); 193 | } 194 | 195 | @Override 196 | public void onProviderInstalled() { 197 | // Called when Security Provider is the latest version, or when installation completes 198 | } 199 | 200 | @Override 201 | public void onProviderInstallFailed(int errorCode, Intent recoveryIntent) { 202 | GoogleApiAvailability.getInstance().showErrorNotification(this, errorCode); 203 | } 204 | } 205 | ``` -------------------------------------------------------------------------------- /5.6.md: -------------------------------------------------------------------------------- 1 | ## 5.6 密码学 2 | 3 | 在安全领域,术语“机密性”,“完整性”和“可用性”用于分析对威胁的响应。这三个术语分别指,防止第三方查看私人数据的措施,确保用户引用的数据未被修改的保护措施(或用于检测何时被伪造的技术),以及用户访问服务和数据的能力。在设计安全保护时,所有这三个要素都很重要。特别是,加密技术经常用于确保机密性和完整性,并且 Android 配备了各种加密功能,来允许应用实现机密性和完整性。在本节中,我们将使用示例代码来说明,Android 应用可以安全地执行加密和解密(来确保机密性)和消息认证代码(MAC)或数字签名(来确保完整性)的方法。 4 | 5 | -------------------------------------------------------------------------------- /5.md: -------------------------------------------------------------------------------- 1 | # 五、如何使用安全功能 2 | 3 | Android 中准备了各种安全功能,如加密,数字签名和权限等。如果这些安全功能使用不当,安全功能无法有效工作,并且会存在漏洞。 本章将解释如何正确使用安全功能。 -------------------------------------------------------------------------------- /6.md: -------------------------------------------------------------------------------- 1 | # 六、困难问题 2 | 3 | 在 Android 中,由于 Android 操作系统规范或 Android 操作系统提供的功能,难以确保应用实现的安全性。 这些功能被恶意第三方滥用或用户不小心使用,始终存在可能导致信息泄露等安全问题的风险。 本章通过指出开发人员可以针对这些功能采取的风险缓解计划,将一些需要引起注意的主题挑选为文章。 4 | 5 | ## 6.1 来自剪贴板的信息泄露风险 6 | 7 | 复制和粘贴是用户经常以不经意的方式使用的功能。 例如,不少用户使用这些功能来存储好奇或重要的信息,将邮件或网页中的东西记到记事本中,或者从存储密码的记事本复制并粘贴密码,以便不会提前忘记。 这些明显非常随意的行为,但实际上存在用户处理的信息可能被盗的隐藏风险。 8 | 9 | 这个风险与 Android 系统中的复制粘贴机制有关。 用户或应用复制的信息,曾经存储在称为剪贴板的缓冲区中。 存储在剪贴板中的信息,在被用户或应用粘贴时,分发给其他应用。 所以这个剪贴板功能中存在导致信息泄漏的风险。 这是因为剪贴板的实体在系统中是唯一的,并且任何应用都可以使用`ClipboardManager`,随时获取存储在剪贴板中的信息。 这意味着用户复制/剪切的所有信息都会泄露给恶意应用。 10 | 11 | 因此,考虑到 Android 操作系统的规范,应用开发人员需要采取措施,尽量减少信息泄露的可能性。 12 | 13 | ### 6.1.1 示例代码 14 | 15 | 粗略地说,有两种对策用于减轻来自剪贴板的信息泄露风险 16 | 17 | 1. 从其他应用复制到你的应用时采取对策。 18 | 2. 从你的应用复制到其他应用时采取对策。 19 | 20 | 首先,让我们讨论上面的对策(1)。 假设用户从其他应用(如记事本,Web 浏览器或邮件应用)复制字符串,然后将其粘贴到你的应用的`EditText`中。 事实证明,在这种情况下,基本没有对策,来防止由于复制和粘贴而导致的敏感信息泄漏。 由于 Android 中没有功能来控制第三方应用的复制操作。 因此,就对策(1)而言,除了向用户解释复制和粘贴敏感信息的风险外,没有任何方法,只能继续让用户自行减少操作。 21 | 22 | 接下来的讨论是上面的对策(2),假设用户复制应用中显示的敏感信息。 在这种情况下,防止泄漏的有效对策是,禁止来自视图(`TextView`,`EditText`等)的复制/剪切操作。 如果输入/输出敏感信息(如个人信息)的视图中,没有复制/剪切功能,信息泄漏永远不会通过剪贴板在你的应用发生。 23 | 24 | 有几种禁止复制/剪切的方法。 本节介绍简单有效的方法:一种方法是禁用视图的长按,另一种方法是在选择字符串时从菜单中删除复制/剪切条目。 25 | 26 | 对策的必要性可以根据图 6.1-1 的流程确定。 在图 6.1-1 中,“输入类型固定为密码属性”表示,输入类型在应用运行时必须是以下三种之一。 在这种情况下,由于默认禁止复制/剪切,因此不需要采取对策。 27 | 28 | + `InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD` 29 | + `InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD` 30 | + `InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD` 31 | 32 | ![](img/6-1-1.jpg) 33 | 34 | 以下小节使用每个示例代码详细介绍了对策。 35 | 36 | #### 6.1.1.1 选择字符串时,从菜单中删除复制/剪切条目 37 | 38 | 在 Android 3.0(API Level 11)之前不能使用`TextView.setCustomSelectionActionMODECallback()`方法。 在这种情况下,禁止复制/剪切的最简单方法是禁用视图的长按。 禁用视图的长按可以在`layout.xml`文件中规定。 39 | 40 | 下面展示了示例代码,用于从`EditText`中的字符串选择菜单中删除复制/剪切条目。 41 | 42 | 要点: 43 | 44 | 1. 从字符串选择菜单中删除`android.R.id.copy`。 45 | 2. 从字符串选择菜单中删除`android.R.id.cut`。 46 | 47 | UncopyableActivity.java 48 | 49 | ```java 50 | package org.jssec.android.clipboard.leakage; 51 | 52 | import android.app.Activity; 53 | import android.os.Bundle; 54 | import android.support.v4.app.NavUtils; 55 | import android.view.ActionMode; 56 | import android.view.Menu; 57 | import android.view.MenuItem; 58 | import android.widget.EditText; 59 | 60 | public class UncopyableActivity extends Activity { 61 | 62 | private EditText copyableEdit; 63 | private EditText uncopyableEdit; 64 | 65 | @Override 66 | public void onCreate(Bundle savedInstanceState) { 67 | super.onCreate(savedInstanceState); 68 | setContentView(R.layout.uncopyable); 69 | copyableEdit = (EditText) findViewById(R.id.copyable_edit); 70 | uncopyableEdit = (EditText) findViewById(R.id.uncopyable_edit); 71 | // By setCustomSelectionActionMODECallback method, 72 | // Possible to customize menu of character string selection. 73 | uncopyableEdit.setCustomSelectionActionModeCallback(actionModeCallback); 74 | } 75 | 76 | private ActionMode.Callback actionModeCallback = new ActionMode.Callback() { 77 | 78 | public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 79 | return false; 80 | } 81 | 82 | public void onDestroyActionMode(ActionMode mode) { 83 | } 84 | 85 | public boolean onCreateActionMode(ActionMode mode, Menu menu) { 86 | // *** POINT 1 *** Delete android.R.id.copy from the menu of character string selection. 87 | MenuItem itemCopy = menu.findItem(android.R.id.copy); 88 | if (itemCopy != null) { 89 | menu.removeItem(android.R.id.copy); 90 | } 91 | // *** POINT 2 *** Delete android.R.id.cut from the menu of character string selection. 92 | MenuItem itemCut = menu.findItem(android.R.id.cut); 93 | if (itemCut != null) { 94 | menu.removeItem(android.R.id.cut); 95 | } 96 | return true; 97 | } 98 | 99 | public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 100 | return false; 101 | } 102 | }; 103 | 104 | @Override 105 | public boolean onCreateOptionsMenu(Menu menu) { 106 | getMenuInflater().inflate(R.menu.uncopyable, menu); 107 | return true; 108 | } 109 | 110 | @Override 111 | public boolean onOptionsItemSelected(MenuItem item) { 112 | switch (item.getItemId()) { 113 | case android.R.id.home: 114 | NavUtils.navigateUpFromSameTask(this); 115 | return true; 116 | } 117 | return super.onOptionsItemSelected(item); 118 | } 119 | } 120 | ``` 121 | 122 | #### 6.1.1.2 禁用视图的长按 123 | 124 | 禁止复制/剪切也可以通过禁用视图的长按来实现。 禁用视图的长按可以在`layout.xml`文件中规定。 125 | 126 | 要点: 127 | 128 | 1. 在视图中将`android:longClickable`设置为`false`,来禁止复制/剪切。 129 | 130 | unlongclickable.xml 131 | 132 | ```xml 133 | 138 | 142 | 143 | 144 | 149 | 150 | ``` 151 | 152 | ### 6.1.2 规则书 153 | 154 | 将敏感信息从你的应用复制到其他应用时,请遵循以下规则: 155 | 156 | #### 6.1.2.1 禁用视图中显示的复制/剪切字符串(必需) 157 | 158 | 如果应用中存在显示敏感信息的视图,并且允许在视图中像`EditText`一样复制/剪切信息,信息可能会通过剪贴板泄漏。 因此,必须在显示敏感信息的视图中禁用复制/剪切。 有两种方法禁用复制/剪切。 一种方法是从字符串选择菜单中删除复制/剪切条目,另一种方法是禁用视图的长按。 请参阅“6.1.3.1 应用规则时的注意事项”。 159 | 160 | ### 6.1.3 高级话题 161 | 162 | #### 6.1.3.1 应用规则时的注意事项 163 | 164 | 在`TextView`中,选择字符串是不可能的,因此通常不需要对策,但在某些情况下,可以复制取决于应用的规范。选择/复制字符串的可能性可以通过使用`TextView.setTextIsSelectable()`方法动态决定。将`TextView`设置为可以复制时,应调查在`TextView`中显示任何敏感信息的可能性,并且如果有任何可能性,则不应将其设置为可复制的。 165 | 166 | 另外,在“6.1.1 示例代码”的决策流程中描述,根据`EditText`的输入类型(`InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD`等),假设输入类型是密码,通常不需要任何对策,因为复制字符串是默认禁止的。但是,如“5.1.2.2 提供以明文显示密码的选项(必需)”中所述,如果准备了【以明文显示密码】的选项,则在以明文显示密码的情况下,输入类型将会改变,并且启用复制/剪切。因此应该要求采取同样的对策。 167 | 168 | 请注意,开发者在应用规则时,还应考虑到应用的可用性。 例如,在用户可以自由输入文本的视图的情况下,如果因输入敏感信息的可能性很小而禁用了复制/剪切,用户可能会感到不便。 当然,该规则应该无条件地,应用于处理非常重要的信息或独立的敏感信息的视图,但在视图之外的情况下,以下问题将帮助开发人员了解如何正确处理视图。 169 | 170 | + 准备一些专门用于敏感信息的其他组件 171 | + 当向应用的粘贴是显而易见的时候,用其他方法发送信息 172 | + 提醒用户注意输入/输出信息 173 | + 重新审视视图的必要性 174 | 175 | 信息泄露风险的根源在于,Android 操作系统中剪贴板和剪贴板管理器的规范不考虑安全风险。 应用开发人员需要在用户完整性,可用性,功能等方面创建更高质量的应用。 176 | 177 | #### 6.1.3.2 存储在剪贴板中的操作信息 178 | 179 | 正如“6.1 来自剪贴板的信息泄漏风险”中所述,应用可以使用`ClipboardManager`,操作存储在剪贴板中的信息。另外,不需要为使用`ClipboardManager`设置特定的权限,因此应用可以在不被用户识别的情况下,使用`ClipboardManager`。 180 | 181 | 存储在剪贴板中的信息称为`ClipData`,可以通过`ClipboardManager.getPrimaryClip()`方法获得。如果通过`ClipboardManager.addPrimaryClipChangedListener()`方法,将侦听器注册到`ClipboardManager`,并实现了`OnPrimaryClipChangedListener`,则每次用户执行复制/剪切操作时都会调用监听器。因此可以在不忽略时间的情况下获得`ClipData`。在任何应用中执行复制/剪切操作时,都会调用监听器。 182 | 183 | 下面显示了服务的源代码,无论什么时候在设备中执行复制/剪切,它都会获取`ClipData`并通过`Toast`显示。你可以意识到,存储在剪贴板中的信息被泄露出来,就是由于下面的简单代码。有必要注意,敏感信息至少不会由以下源代码使用。 184 | 185 | ClipboardListeningService.java 186 | 187 | ```java 188 | package org.jssec.android.clipboard; 189 | 190 | import android.app.Service; 191 | import android.content.ClipData; 192 | import android.content.ClipboardManager; 193 | import android.content.ClipboardManager.OnPrimaryClipChangedListener; 194 | import android.content.Context; 195 | import android.content.Intent; 196 | import android.os.IBinder; 197 | import android.util.Log; 198 | import android.widget.Toast; 199 | 200 | public class ClipboardListeningService extends Service { 201 | 202 | private static final String TAG = "ClipboardListeningService"; 203 | private ClipboardManager mClipboardManager; 204 | 205 | @Override 206 | public IBinder onBind(Intent arg0) { 207 | return null; 208 | } 209 | 210 | @Override 211 | public void onCreate() { 212 | super.onCreate(); 213 | mClipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); 214 | if (mClipboardManager != null) { 215 | mClipboardManager.addPrimaryClipChangedListener(clipListener); 216 | } else { 217 | Log.e(TAG, "Failed to get ClipboardService . Service is closed."); 218 | this.stopSelf(); 219 | } 220 | } 221 | 222 | @Override 223 | public void onDestroy() { 224 | super.onDestroy(); 225 | if (mClipboardManager != null) { 226 | mClipboardManager.removePrimaryClipChangedListener(clipListener); 227 | } 228 | } 229 | 230 | private OnPrimaryClipChangedListener clipListener = new OnPrimaryClipChangedListener() { 231 | 232 | public void onPrimaryClipChanged() { 233 | if (mClipboardManager != null && mClipboardManager.hasPrimaryClip()) { 234 | ClipData data = mClipboardManager.getPrimaryClip(); 235 | ClipData.Item item = data.getItemAt(0); 236 | Toast.makeText( 237 | getApplicationContext(), 238 | "Character stirng that is copied or cut:¥n" 239 | + item.coerceToText(getApplicationContext()), 240 | Toast.LENGTH_SHORT) 241 | .show(); 242 | } 243 | } 244 | }; 245 | } 246 | ``` 247 | 248 | 接下来,下面显示了`Activity`的示例代码,它使用上面涉及的`ClipboardListeningService`。 249 | 250 | ClipboardListeningActivity.java 251 | 252 | ```py 253 | package org.jssec.android.clipboard; 254 | 255 | import android.app.Activity; 256 | import android.content.ComponentName; 257 | import android.content.Intent; 258 | import android.os.Bundle; 259 | import android.util.Log; 260 | import android.view.View; 261 | 262 | public class ClipboardListeningActivity extends Activity { 263 | 264 | private static final String TAG = "ClipboardListeningActivity"; 265 | 266 | @Override 267 | public void onCreate(Bundle savedInstanceState) { 268 | super.onCreate(savedInstanceState); 269 | setContentView(R.layout.activity_clipboard_listening); 270 | } 271 | 272 | public void onClickStartService(View view) { 273 | if (view.getId() != R.id.start_service_button) { 274 | Log.w(TAG, "View ID is incorrect."); 275 | } else { 276 | ComponentName cn = startService( 277 | new Intent(ClipboardListeningActivity.this, ClipboardListeningService.class)); 278 | if (cn == null) { 279 | Log.e(TAG, "Failed to launch the service."); 280 | } 281 | } 282 | } 283 | public void onClickStopService(View view) { 284 | if (view.getId() != R.id.stop_service_button) { 285 | Log.w(TAG, "View ID is incorrect."); 286 | } else { 287 | stopService(new Intent(ClipboardListeningActivity.this, ClipboardListeningService.class)); 288 | } 289 | } 290 | } 291 | ``` 292 | 293 | 到目前为止,我们已经介绍了获取存储在剪贴板上的数据的方法。 也可以使用`ClipboardManager.setPrimaryClip()`方法在剪贴板上存储新数据。 294 | 295 | 请注意,`setPrimaryClip()`方法将覆盖存储在剪贴板中的信息,因此用户的复制/剪切存储的信息可能会丢失。 当使用这些方法提供自定义复制/剪切功能时,必须按需设计/实现,以防止存储在剪贴板中的内容改变为意外内容,通过显示对话框来通知内容将被改变。 296 | 297 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 安卓应用安全指南 中文版 2 | 3 | 原文:[Android Application Secure Design/Secure Coding Guidebook](http://www.jssec.org/dl/android_securecoding_en.pdf) 4 | 5 | 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 版本:2017.2.1 8 | 9 | 自豪地采用[谷歌翻译](https://translate.google.cn/) 10 | 11 | + [在线阅读](https://www.gitbook.com/book/wizardforcel/android-app-sec-guidebook/details) 12 | + [PDF格式](https://www.gitbook.com/download/pdf/book/wizardforcel/android-app-sec-guidebook) 13 | + [EPUB格式](https://www.gitbook.com/download/epub/book/wizardforcel/android-app-sec-guidebook) 14 | + [MOBI格式](https://www.gitbook.com/download/mobi/book/wizardforcel/android-app-sec-guidebook) 15 | + [Github](https://github.com/wizardforcel/android-app-sec-guidebook-zh) 16 | 17 | ## 赞助我 18 | 19 | ![](img/qr_alipay.png) 20 | 21 | ## 协议 22 | 23 | [CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) 24 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | + [安卓应用安全指南 中文版](README.md) 2 | + [一、简介](1.md) 3 | + [二、本书结构](2.md) 4 | + [三、安全设计和编程的基础知识](3.md) 5 | + [四、以安全方式使用技术](4.md) 6 | + [4.1 创建或使用活动](4.1.md) 7 | + [4.1.1 示例代码](4.1.1.md) 8 | + [4.1.1.1 创建/使用私有活动](4.1.1.1.md) 9 | + [4.1.1.2 创建/使用公共活动](4.1.1.2.md) 10 | + [4.1.1.3 创建/使用伙伴活动](4.1.1.3.md) 11 | + [4.1.1.4 创建/使用内部活动](4.1.1.4.md) 12 | + [4.1.2 规则书](4.1.2.md) 13 | + [4.1.3 高级话题](4.1.3.md) 14 | + [4.2 接收/发送广播](4.2.md) 15 | + [4.2.1 示例代码](4.2.1.md) 16 | + [4.2.1.1 私有广播接收器](4.2.1.1.md) 17 | + [4.2.1.2 公共广播接收器](4.2.1.2.md) 18 | + [4.2.1.3 内部广播接收器](4.2.1.3.md) 19 | + [4.2.2 规则书](4.2.2.md) 20 | + [4.2.3 高级话题](4.2.3.md) 21 | + [4.3 创建/使用内容供应器](4.3.md) 22 | + [4.3.1 示例代码](4.3.1.md) 23 | + [4.3.1.1 创建/使用私有内容供应器](4.3.1.1.md) 24 | + [4.3.1.2 创建/使用公共内容供应器](4.3.1.2.md) 25 | + [4.3.1.3 创建/使用伙伴内容供应器](4.3.1.3.md) 26 | + [4.3.1.4 创建/使用内部内容供应器](4.3.1.4.md) 27 | + [4.3.1.5 创建/使用临时内容供应器](4.3.1.5.md) 28 | + [4.3.2 规则书](4.3.2.md) 29 | + [4.4 创建/使用服务](4.4.md) 30 | + [4.4.1 示例代码](4.4.1.md) 31 | + [4.4.1.1 创建/使用私有服务](4.4.1.1.md) 32 | + [4.4.1.2 创建/使用公共服务](4.4.1.2.md) 33 | + [4.4.1.3 创建/使用伙伴服务](4.4.1.3.md) 34 | + [4.4.1.4 创建/使用内部服务](4.4.1.4.md) 35 | + [4.4.2 规则书](4.4.2.md) 36 | + [4.4.3 高级话题](4.4.3.md) 37 | + [4.5 使用 SQLite](4.5.md) 38 | + [4.5.1 示例代码](4.5.1.md) 39 | + [4.5.2 规则书](4.5.2.md) 40 | + [4.5.3 高级话题](4.5.3.md) 41 | + [4.6 处理文件](4.6.md) 42 | + [4.6.1 示例代码](4.6.1.md) 43 | + [4.6.1.1 使用私有文件](4.6.1.1.md) 44 | + [4.6.1.2 使用公共只读文件](4.6.1.2.md) 45 | + [4.6.1.3 创建公共读写文件](4.6.1.3.md) 46 | + [4.6.1.4 使用外部存储器(公共读写)文件](4.6.1.4.md) 47 | + [4.6.2 规则书](4.6.2.md) 48 | + [4.6.3 高级话题](4.6.3.md) 49 | + [4.7 使用可浏览的意图](4.7.md) 50 | + [4.8 输出到 LogCat](4.8.md) 51 | + [4.9 使用`WebView`](4.9.md) 52 | + [4.10 使用通知](4.10.md) 53 | + [五、如何使用安全功能](5.md) 54 | + [5.1 创建密码输入界面](5.1.md) 55 | + [5.2 权限和保护级别](5.2.md) 56 | + [5.2.1 示例代码](5.2.1.md) 57 | + [5.2.2 规则书](5.2.2.md) 58 | + [5.2.3 高级话题](5.2.3.md) 59 | + [5.3 将内部账户添加到账户管理器](5.3.md) 60 | + [5.3.1 示例代码](5.3.1.md) 61 | + [5.3.2 规则书](5.3.2.md) 62 | + [5.3.3 高级话题](5.3.3.md) 63 | + [5.4 通过 HTTPS 的通信](5.4.md) 64 | + [5.4.1 示例代码](5.4.1.md) 65 | + [5.4.2 规则书](5.4.2.md) 66 | + [5.4.3 高级话题](5.4.3.md) 67 | + [5.5 处理隐私数据](5.5.md) 68 | + [5.5.1 示例代码](5.5.1.md) 69 | + [5.5.2 规则书](5.5.2.md) 70 | + [5.5.3 高级话题](5.5.3.md) 71 | + [5.6 密码学](5.6.md) 72 | + [5.6.1 示例代码](5.6.1.md) 73 | + [5.6.2 规则书](5.6.2.md) 74 | + [5.6.3 高级话题](5.6.3.md) 75 | + [5.7 使用指纹认证功能](5.7.md) 76 | + [六、困难问题](6.md) 77 | -------------------------------------------------------------------------------- /cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/cover.jpg -------------------------------------------------------------------------------- /img/4-1-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/4-1-1.jpg -------------------------------------------------------------------------------- /img/4-1-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/4-1-2.jpg -------------------------------------------------------------------------------- /img/4-1-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/4-1-4.jpg -------------------------------------------------------------------------------- /img/4-1-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/4-1-5.jpg -------------------------------------------------------------------------------- /img/4-10-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/4-10-1.jpg -------------------------------------------------------------------------------- /img/4-10-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/4-10-2.jpg -------------------------------------------------------------------------------- /img/4-10-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/4-10-3.jpg -------------------------------------------------------------------------------- /img/4-2-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/4-2-1.jpg -------------------------------------------------------------------------------- /img/4-2-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/4-2-4.jpg -------------------------------------------------------------------------------- /img/4-2-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/4-2-5.jpg -------------------------------------------------------------------------------- /img/4-3-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/4-3-1.jpg -------------------------------------------------------------------------------- /img/4-4-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/4-4-1.jpg -------------------------------------------------------------------------------- /img/4-4-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/4-4-4.jpg -------------------------------------------------------------------------------- /img/4-4-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/4-4-5.jpg -------------------------------------------------------------------------------- /img/4-4-6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/4-4-6.jpg -------------------------------------------------------------------------------- /img/4-5-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/4-5-1.jpg -------------------------------------------------------------------------------- /img/4-8-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/4-8-1.jpg -------------------------------------------------------------------------------- /img/4-8-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/4-8-2.jpg -------------------------------------------------------------------------------- /img/4-8-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/4-8-3.jpg -------------------------------------------------------------------------------- /img/4-9-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/4-9-1.jpg -------------------------------------------------------------------------------- /img/4-9-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/4-9-2.jpg -------------------------------------------------------------------------------- /img/5-1-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/5-1-1.jpg -------------------------------------------------------------------------------- /img/5-1-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/5-1-2.jpg -------------------------------------------------------------------------------- /img/5-1-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/5-1-3.jpg -------------------------------------------------------------------------------- /img/5-1-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/5-1-4.jpg -------------------------------------------------------------------------------- /img/5-2-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/5-2-1.jpg -------------------------------------------------------------------------------- /img/5-2-10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/5-2-10.jpg -------------------------------------------------------------------------------- /img/5-2-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/5-2-2.jpg -------------------------------------------------------------------------------- /img/5-2-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/5-2-5.jpg -------------------------------------------------------------------------------- /img/5-2-6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/5-2-6.jpg -------------------------------------------------------------------------------- /img/5-2-7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/5-2-7.jpg -------------------------------------------------------------------------------- /img/5-2-8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/5-2-8.jpg -------------------------------------------------------------------------------- /img/5-2-9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/5-2-9.jpg -------------------------------------------------------------------------------- /img/5-3-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/5-3-1.jpg -------------------------------------------------------------------------------- /img/5-3-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/5-3-2.jpg -------------------------------------------------------------------------------- /img/5-3-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/5-3-3.jpg -------------------------------------------------------------------------------- /img/5-4-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/5-4-1.jpg -------------------------------------------------------------------------------- /img/5-4-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/5-4-2.jpg -------------------------------------------------------------------------------- /img/5-4-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/5-4-3.jpg -------------------------------------------------------------------------------- /img/5-4-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/5-4-4.jpg -------------------------------------------------------------------------------- /img/5-5-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/5-5-1.jpg -------------------------------------------------------------------------------- /img/5-5-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/5-5-2.jpg -------------------------------------------------------------------------------- /img/5-5-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/5-5-3.jpg -------------------------------------------------------------------------------- /img/5-5-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/5-5-4.jpg -------------------------------------------------------------------------------- /img/5-5-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/5-5-5.jpg -------------------------------------------------------------------------------- /img/5-5-6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/5-5-6.jpg -------------------------------------------------------------------------------- /img/5-5-7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/5-5-7.jpg -------------------------------------------------------------------------------- /img/5-6-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/5-6-1.jpg -------------------------------------------------------------------------------- /img/5-6-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/5-6-2.jpg -------------------------------------------------------------------------------- /img/5-6-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/5-6-3.jpg -------------------------------------------------------------------------------- /img/5-6-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/5-6-4.jpg -------------------------------------------------------------------------------- /img/6-1-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/6-1-1.jpg -------------------------------------------------------------------------------- /img/qr_alipay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wizardforcel/android-app-sec-guidebook-zh/6a7c967ea8090e4c3fb1b5f56f5abd5edcd08635/img/qr_alipay.png -------------------------------------------------------------------------------- /styles/ebook.css: -------------------------------------------------------------------------------- 1 | /* GitHub stylesheet for MarkdownPad (http://markdownpad.com) */ 2 | /* Author: Nicolas Hery - http://nicolashery.com */ 3 | /* Version: b13fe65ca28d2e568c6ed5d7f06581183df8f2ff */ 4 | /* Source: https://github.com/nicolahery/markdownpad-github */ 5 | 6 | /* RESET 7 | =============================================================================*/ 8 | 9 | html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { 10 | margin: 0; 11 | padding: 0; 12 | border: 0; 13 | } 14 | 15 | /* BODY 16 | =============================================================================*/ 17 | 18 | body { 19 | font-family: Helvetica, arial, freesans, clean, sans-serif; 20 | font-size: 14px; 21 | line-height: 1.6; 22 | color: #333; 23 | background-color: #fff; 24 | padding: 20px; 25 | max-width: 960px; 26 | margin: 0 auto; 27 | } 28 | 29 | body>*:first-child { 30 | margin-top: 0 !important; 31 | } 32 | 33 | body>*:last-child { 34 | margin-bottom: 0 !important; 35 | } 36 | 37 | /* BLOCKS 38 | =============================================================================*/ 39 | 40 | p, blockquote, ul, ol, dl, table, pre { 41 | margin: 15px 0; 42 | } 43 | 44 | /* HEADERS 45 | =============================================================================*/ 46 | 47 | h1, h2, h3, h4, h5, h6 { 48 | margin: 20px 0 10px; 49 | padding: 0; 50 | font-weight: bold; 51 | -webkit-font-smoothing: antialiased; 52 | } 53 | 54 | h1 tt, h1 code, h2 tt, h2 code, h3 tt, h3 code, h4 tt, h4 code, h5 tt, h5 code, h6 tt, h6 code { 55 | font-size: inherit; 56 | } 57 | 58 | h1 { 59 | font-size: 24px; 60 | border-bottom: 1px solid #ccc; 61 | color: #000; 62 | } 63 | 64 | h2 { 65 | font-size: 18px; 66 | color: #000; 67 | } 68 | 69 | h3 { 70 | font-size: 14px; 71 | } 72 | 73 | h4 { 74 | font-size: 14px; 75 | } 76 | 77 | h5 { 78 | font-size: 14px; 79 | } 80 | 81 | h6 { 82 | color: #777; 83 | font-size: 14px; 84 | } 85 | 86 | body>h2:first-child, body>h1:first-child, body>h1:first-child+h2, body>h3:first-child, body>h4:first-child, body>h5:first-child, body>h6:first-child { 87 | margin-top: 0; 88 | padding-top: 0; 89 | } 90 | 91 | a:first-child h1, a:first-child h2, a:first-child h3, a:first-child h4, a:first-child h5, a:first-child h6 { 92 | margin-top: 0; 93 | padding-top: 0; 94 | } 95 | 96 | h1+p, h2+p, h3+p, h4+p, h5+p, h6+p { 97 | margin-top: 10px; 98 | } 99 | 100 | /* LINKS 101 | =============================================================================*/ 102 | 103 | a { 104 | color: #4183C4; 105 | text-decoration: none; 106 | } 107 | 108 | a:hover { 109 | text-decoration: underline; 110 | } 111 | 112 | /* LISTS 113 | =============================================================================*/ 114 | 115 | ul, ol { 116 | padding-left: 30px; 117 | } 118 | 119 | ul li > :first-child, 120 | ol li > :first-child, 121 | ul li ul:first-of-type, 122 | ol li ol:first-of-type, 123 | ul li ol:first-of-type, 124 | ol li ul:first-of-type { 125 | margin-top: 0px; 126 | } 127 | 128 | ul ul, ul ol, ol ol, ol ul { 129 | margin-bottom: 0; 130 | } 131 | 132 | dl { 133 | padding: 0; 134 | } 135 | 136 | dl dt { 137 | font-size: 14px; 138 | font-weight: bold; 139 | font-style: italic; 140 | padding: 0; 141 | margin: 15px 0 5px; 142 | } 143 | 144 | dl dt:first-child { 145 | padding: 0; 146 | } 147 | 148 | dl dt>:first-child { 149 | margin-top: 0px; 150 | } 151 | 152 | dl dt>:last-child { 153 | margin-bottom: 0px; 154 | } 155 | 156 | dl dd { 157 | margin: 0 0 15px; 158 | padding: 0 15px; 159 | } 160 | 161 | dl dd>:first-child { 162 | margin-top: 0px; 163 | } 164 | 165 | dl dd>:last-child { 166 | margin-bottom: 0px; 167 | } 168 | 169 | /* CODE 170 | =============================================================================*/ 171 | 172 | pre, code, tt { 173 | font-size: 12px; 174 | font-family: Consolas, "Liberation Mono", Courier, monospace; 175 | } 176 | 177 | code, tt { 178 | margin: 0 0px; 179 | padding: 0px 0px; 180 | white-space: nowrap; 181 | border: 1px solid #eaeaea; 182 | background-color: #f8f8f8; 183 | border-radius: 3px; 184 | } 185 | 186 | pre>code { 187 | margin: 0; 188 | padding: 0; 189 | white-space: pre; 190 | border: none; 191 | background: transparent; 192 | } 193 | 194 | pre { 195 | background-color: #f8f8f8; 196 | border: 1px solid #ccc; 197 | font-size: 13px; 198 | line-height: 19px; 199 | overflow: auto; 200 | padding: 6px 10px; 201 | border-radius: 3px; 202 | } 203 | 204 | pre code, pre tt { 205 | background-color: transparent; 206 | border: none; 207 | } 208 | 209 | kbd { 210 | -moz-border-bottom-colors: none; 211 | -moz-border-left-colors: none; 212 | -moz-border-right-colors: none; 213 | -moz-border-top-colors: none; 214 | background-color: #DDDDDD; 215 | background-image: linear-gradient(#F1F1F1, #DDDDDD); 216 | background-repeat: repeat-x; 217 | border-color: #DDDDDD #CCCCCC #CCCCCC #DDDDDD; 218 | border-image: none; 219 | border-radius: 2px 2px 2px 2px; 220 | border-style: solid; 221 | border-width: 1px; 222 | font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; 223 | line-height: 10px; 224 | padding: 1px 4px; 225 | } 226 | 227 | /* QUOTES 228 | =============================================================================*/ 229 | 230 | blockquote { 231 | border-left: 4px solid #DDD; 232 | padding: 0 15px; 233 | color: #777; 234 | } 235 | 236 | blockquote>:first-child { 237 | margin-top: 0px; 238 | } 239 | 240 | blockquote>:last-child { 241 | margin-bottom: 0px; 242 | } 243 | 244 | /* HORIZONTAL RULES 245 | =============================================================================*/ 246 | 247 | hr { 248 | clear: both; 249 | margin: 15px 0; 250 | height: 0px; 251 | overflow: hidden; 252 | border: none; 253 | background: transparent; 254 | border-bottom: 4px solid #ddd; 255 | padding: 0; 256 | } 257 | 258 | /* TABLES 259 | =============================================================================*/ 260 | 261 | table th { 262 | font-weight: bold; 263 | } 264 | 265 | table th, table td { 266 | border: 1px solid #ccc; 267 | padding: 6px 13px; 268 | } 269 | 270 | table tr { 271 | border-top: 1px solid #ccc; 272 | background-color: #fff; 273 | } 274 | 275 | table tr:nth-child(2n) { 276 | background-color: #f8f8f8; 277 | } 278 | 279 | /* IMAGES 280 | =============================================================================*/ 281 | 282 | img { 283 | max-width: 100% 284 | } -------------------------------------------------------------------------------- /styles/runoob.css: -------------------------------------------------------------------------------- 1 | .example_code { 2 | font-size: 12px; 3 | font-family: Consolas, "Liberation Mono", Courier, monospace; 4 | background-color: #f8f8f8; 5 | border: 1px solid #ccc; 6 | font-size: 13px; 7 | line-height: 19px; 8 | overflow: auto; 9 | padding: 6px 10px; 10 | border-radius: 3px; 11 | white-space: pre-wrap; 12 | } 13 | 14 | .example_code>code { 15 | margin: 0; 16 | padding: 0; 17 | white-space: pre; 18 | border: none; 19 | background: transparent; 20 | } 21 | 22 | .example_code code, .example_code tt { 23 | background-color: transparent; 24 | border: none; 25 | } 26 | 27 | .tryitbtn { 28 | display: none; 29 | } --------------------------------------------------------------------------------