保护Android网络数据教程

安全是发展的重要组成部分。用户希望您保护他们的数据免受意外窥探。Android默认使用机制来控制谁可以查看您的应用在设备上收集的信息,但几乎每个应用都通过网络进行通信。您可以通过确保您的应用保护传输中的网络数据来保护用户信息的私密性。

在本教程中,您将获得一个名为PetMed的简单Android应用程序,用于通过网络交换医疗信息的兽医诊所。

在此过程中,您将学习以下最佳实践:

  • 使用HTTPS进行网络呼叫。
  • 信任与证书固定的连接。
  • 验证传输数据的完整性。

入门

下载并解压缩本教程的资料请看文末。在Android Studio 3.1.3或更高版本中打开入门项目,然后导航到PetRequester.kt文件。现在,该retrievePets()方法正在进行一个简单的调用,以检索宠物及其医疗数据列表的JSON数据。

构建并运行项目以查看您将使用的内容。

入门项目

了解HTTPS

通过在屏幕上向上滑动来浏览宠物的选择。点击宠物的照片可以看到其医疗数据的详细视图。表面上的一切看起来都很好,但是在方法的第一行retrievePets(),您会注意到URL以http://开头。

HTTP数据以明文形式传输。这意味着,例如,有关Pom the Pomeranian的所有医疗信息都被检索到,未经任何人查看。许多流行的工具可用于监控HTTP流量。一些例子是WiresharkmitmproxyCharles

因为Pomeranians往往对他们的隐私很挑剔,所以你会将此请求更改为HTTPS。HTTPS使用TLS(或传输层安全性)来加密传输中的数据。更改此请求所需要做的就是将“s”附加到URL字符串的“http”部分retrievePets(),并将连接类更改为HttpsURLConnection。只要主机支持HTTPS,就会建立安全连接。这使得使用前面提到的工具来监控数据变得非常困难。

当然,这不仅适用于医学数据的例子。登录请求,银行详细信息或任何带有个人身份信息(PII)的信息都应通过HTTPS发送。但是,不是试图猜测哪种类型的信息是个人信息,而是从一开始就对所有请求进行HTTPS更好的做法。从Android N开始,您可以使用网络安全配置强制执行此操作

注意:将您从应用程序发送的数据量限制为基本要素也是一种很好的做法。

执行HTTPS

要在Android N及更高版本上强制实施HTTPS流量,请右键单击app / res目录,然后选择新建▸目录。将其命名为xml。右键单击新创建的目录,然后选择“ 新建”▸“文件”。将其命名为network_security_config.xml。在新创建的文件中,添加以下代码:

<?xml version =“1.0”encoding =“utf-8”?> 
< network-security-config > 
  < domain-config  cleartextTrafficPermitted = “false” > 
    < domain  includeSubdomains = “true” > github.io </ domain > 
  < / domain-config > 
</ network-security-config >

在这里,您将cleartextTrafficPermitted属性设置为false,以阻止您指定的特定域的任何不使用HTTPS的网络请求。然后github.io,您添加为域,将其includeSubdomains属性设置为true,这将需要HTTPS用于子域collinstuart.github.io。现在你需要告诉Android系统使用这个文件。

AndroidManifest.xml文件中,<application用此行替换开始标记:

< application  android:networkSecurityConfig = “@ xml / network_security_config”

在Android N(或更新版本)仿真器或设备上再次构建和调试项目。您应该在Debug选项卡中看到一条错误消息,其中显示了java.io.IOException:Cleartext不允许向collinstuart.github.io发送HTTP流量

错误信息

那是因为Android阻止了调用,因此没有清除任何数据。您的应用应如下所示:

应用程序没有数据

悲伤的脸

现在您已启用HTTPS强制执行,现在可以修复违规。在PetRequester.kt文件中retrievePets()方法的开头,将所有代码替换为doAsync块,直到该块:

val urlString = “https://collinstuart.github.io/posts.json” 
val url = URL(urlString)
 val connection = url.openConnection()as HttpsURLConnection

此代码将连接分解为您稍后将使用的单独变量。这里的主要区别是:

  • URL从http://更改为https://
  • 连接变量现在是HttpsURLConnection类型。

再次构建并运行应用程序。该应用程序再次显示数据,这次是通过HTTPS。那很简单!

注意:通常在软件中发现安全漏洞时,会发布补丁。确保修补HTTPS的安全提供程序是一个好主意。如果在调试期间发现SSL23_GET_SERVER_HELLO:sslv3警报握手失败等错误,则通常意味着提供者也需要更新。有关此过程的详细信息,请参阅“ 更新安全提供程序”页面。

了解证书和公钥固定

既然您已经迈出了保护数据的第一步,那么请回过头来讨论HTTPS的工作原理。

当您启动HTTPS连接时,服务器会显示一个证书,证明它是真实实体。这是可能的,因为受信任的证书颁发机构签署了证书。证书可能由中间证书颁发机构签名。该中间证书可以由另一个中间机构签署。只要第一个证书由Android信任的根证书颁发机构签名,该连接就是安全的。

Android系统评估该证书链。如果证书无效,则会关闭连接。这听起来不错,但它远非万无一失。存在许多弱点,可以使Android信任攻击者的证书而不是合法签名的证书。例如,黑客可以手动指示Android接受自己安装的证书。或者公司可能将工作设备配置为接受自己的证书。这允许拥有证书的实体能够解密,读取和修改流量,称为中间人攻击。

通过阻止这些场景的连接,证书锁定可以解决问题。它的工作原理是根据预期证书的副本检查服务器的证书。

幸运的是,在Android N上,这很容易实现。它不是比较整个证书,而是比较公钥的哈希值(稍后会详细说明),通常称为引脚

证书固定

要获得您正在与之交谈的主机的引脚,请访问SSL实验室。在主机名字段中键入github.io,然后单击提交

SSL实验室网站

在下一页上,从列表中选择一个服务器:

github.io服务器

您将看到列出了两个证书,第二个是备份。每个条目都有一个Pin SHA256部分:

SHA256

这些是您将添加到应用程序中的公钥的哈希值。返回到network_security_config.xml文件并在domain标记之后添加它们github.io

< pin-set > 
  <! - 注意:这些值可能会随着时间的推移而发生变化,因此请务必使用您在本教程中所做的ssllabs查找中获得的值 - > 
  < pin  digest = “SHA- 256“ > sm6xYAA3V3PtiyWIX6G / FY2kgHCRzR1k9XndcF5A0mg = </ pin > 
  < pin  digest = ”SHA-256“ > k2v657xBsOVe1PQRwOsHsw3bsGT2VzIqz5K + 59sNQws = </ pin > 
</ pin-set >

注意:有很多方法可以获取公钥哈希。另一种方法是直接从网站下载证书并在其上运行OpenSSL命令。如果您正在为公司开发应用程序,您可能会直接对其进行错误处理。:]

实施TrustKit

您已为Android N及更高版本添加了证书固定支持,但如果您的应用需要支持N下的版本,该怎么办?TrustKit是一个库,它在network_security_config.xml文件中使用相同的格式来添加对Android N下版本的支持。

您现在将TrustKit库添加到项目中。转到app模块 build.gradle文件并将其添加到依赖项列表中:

实现“com.datatheorem.android.trustkit:trustkit:$ trustkit_version”

接下来,将TrustKit版本添加到块开头的项目级 build.gradle文件中buildscript

ext.trustkit_version = '1.0.3'

在继续之前,请务必同步Gradle文件。

然后,在network_security_config.xml文件中,在以下pin-set部分后面添加:

< trustkit-config  enforcePinning = “true” />

这告诉TrustKit使用我们在上面添加的现有引脚启用证书固定。在发出任何网络请求之前,您需要在应用启动附近的某处安装具有该安全配置的TrustKit。在MainActivity.kt文件中,将初始化代码添加到onCreate()方法中,就在设置petRequester变量的最后一行之前:

TrustKit.initializeWithNetworkSecurityConfiguration(this)

需要导入TrustKit。您可以在Mac上使用选项+返回或在PC 上使用Alt + Enter,也可以手动将其添加到文件顶部的导入列表中:

import com.datatheorem.android.trustkit.TrustKit

现在,回过头来告诉HttpsURLConnection在建立连接时让TrustKit参与进来。在PetRequester.kt文件中,将TrustKit添加到导入列表中,然后在doAsync块之前添加以下内容:

connection.sslSocketFactory = TrustKit.getInstance()。getSSLSocketFactory(url.host)

现在HttpsURLConnection将使用TrustKit套接字工厂,它将确保证书匹配。

构建并运行应用程序。如果一切顺利,应用程序将在屏幕上显示宠物。要测试一切正常,请导航到network_security_config.xml文件。为每个引脚摘要条目更改=以外的任何字符。这是一个例子:

< 销 消化 = “SHA-256” > sm6xYAA3V3PtiyWIX6G / FY2kgHCRzR1k9XndcF5A0mz = </ 销 > 
< 销 消化 = “SHA-256” > k2v657xBsOVe1PQRwOsHsw3bsGT2VzIqz5K + 59sNQwz = </ 销 >

构建和调试应用程序。您现在应该看到一个错误,指出javax.net.ssl.SSLHandshakeException:引脚验证失败

引脚验证失败错误

您刚刚成功将证书固定添加到您的应用中!不要忘记撤消导致引脚验证失败的更改。:]

还有许多其他第三方解决方案可用于证书固定。有关证书固定的更多信息,请参阅OWASP文档

了解身份验证

在第二次世界大战期间,德国轰炸机使用洛伦兹无线电波束在英国进行导航和寻找目标。这项技术的一个问题是英国人开始在相同的波长上传输他们自己的更强的光束来掩盖德国光束。德国人需要的是某种签名,以便能够辨别真实的假梁。我们今天拥有数字签名,可以验证信息的完整性。

数字签名确保您访问银行数据,开始聊天或登录服务。他们还确保其他人没有改变数据。

数字签名的核心是哈希函数。哈希函数采用可变数量的数据并输出固定长度的签名。这是一个单向的功能。鉴于产生的结果,没有计算上可行的方法来反转它以揭示原始输入是什么。

如果输入相同,则散列函数的输出将始终相同。即使您更改一个字节或字符,输出也会完全不同。这使得您可以通过散列数据来验证大量数据是否损坏,然后将该散列与预期散列进行比较。

您将使用安全散列算法(SHA),这是一种众所周知的标准,它引用一组散列函数。

公钥加密

在许多情况下,当API通过网络发送数据时,数据也将包含签名。但是,如何使用它来了解恶​​意用户是否篡改了数据?现在,攻击者需要做的就是更改数据,然后重新计算签名。您需要的是在散列数据时添加到混合中的一些秘密信息,以便攻击者无法在不知道该秘密的情况下重新计算签名。但即使你有秘密,如果没有被截获,双方如何让对方知道秘密是什么?这就是Public-Key Cryptography进入图片的地方。

公钥加密通过创建一组密钥(一个公钥和一个私钥)来工作。私钥用于创建签名,而公钥验证签名。

给定公钥,导出私钥在计算上是不可行的。即使恶意用户知道公钥,他们所能做的就是验证原始邮件的完整性。攻击者无法更改消息,因为他们没有重建签名所需的私钥。最新和最好的方法是通过椭圆曲线加密(ECC)

椭圆曲线密码学

椭圆曲线密码学

ECC是一组基于有限域上的椭圆曲线的新算法。虽然它可以用于加密,但您可以将其用于身份验证,通常称为ECDSA(椭圆曲线数字签名算法)。

右键单击com.raywenderlich.android.petmed文件夹,然后选择新建▸Kotlin 文件/类。说它验证器,并选择。在文件的顶部,在包声明下面,导入必要的键和工厂类:

import java.security.KeyFactory
 import java.security.KeyPairGenerator
 import java.security.PrivateKey
 import java.security.PublicKey
 import java.security.Signature
 import java.security.spec.X509EncodedKeySpec

添加公钥和私钥对
向该类添加公钥和私钥对,使其如下所示:

class  Authenticator  {

  private  val publicKey:PublicKey
   private  val privateKey:PrivateKey

}

初始化私钥和公钥

您需要初始化这些私钥和公钥。在变量之后,添加init块:

init {
   val keyPairGenerator = KeyPairGenerator.getInstance(“EC”)// 1 
  keyPairGenerator.initialize(256)// 2 
  val keyPair = keyPairGenerator.genKeyPair()// 3
    
  // 4 
  publicKey = keyPair。public 
  privateKey = keyPair。私人的
}

你在这做什么

  1. KeyPairGenerator为Elliptic Curve(EC)类型创建了一个实例。
  2. 使用256位的建议密钥大小初始化对象。
  3. 生成密钥对,其中包含公钥和私钥。
  4. 将类的publicKeyprivateKey变量设置为新生成的键。

添加签名和验证方法

要完成此类,请添加sign和verify方法。将此代码放在init块后面:

有趣的 标志(数据:ByteArray):ByteArray {
   val signature = Signature.getInstance( “SHA1withECDSA”)
  signature.initSign(专用密钥)
  signature.update(data)
   return signature.sign()
}

这个方法需要一个ByteArray。它Signature使用用于签名的私钥初始化对象,添加ByteArray数据然后返回ByteArray签名。

现在,将验证方法添加到您的类:

fun  verify (signature:ByteArray,data:ByteArray): Boolean {
   val verifySignature = Signature.getInstance( “SHA1withECDSA”)
  verifySignature.initVerify(公钥)
  verifySignature.update(data)
   返回 verifySignature.verify(签名)
}

这次,Signature使用验证所需的公钥初始化对象。使用要验证的数据更新签名对象,然后调用更新方法进行验证。true如果验证成功,则返回该方法。

您还需要一种方法来验证发送给您的公钥的数据。创建第二个接受外部公钥的验证方法:

fun  verify (signature:ByteArray,data:ByteArray,publicKeyString:String):
     Boolean {
   val verifySignature = Signature.getInstance( “SHA1withECDSA”)
   val bytes = android.util.Base64.decode(publicKeyString,
      android.util.Base64.DEFAULT)
  val publicKey =
      KeyFactory.getInstance(“EC”)。GeneratePublic(X509EncodedKeySpec(bytes))
  verifySignature.initVerify(公钥)
  verifySignature.update(data)
   返回 verifySignature.verify(签名)
}

此代码与先前的验证方法类似,不同之处在于它将Base64公钥字符串转换为PublicKey对象。Base64是一种格式,允许原始数据字节作为字符串轻松地通过网络传递。

现在你有了一个Authenticator类,你将在PetRequester中使用它。

验证签名

在一种情况下,可能需要应用程序注册传递公钥的服务; 这通常被称为令牌秘密。例如,对于聊天应用,每个用户可以在发起聊天会话时交换公钥。在此示例中,您正在与之通信的Github服务器的公钥将包含在代码中。它将用于验证来自itemsJSON列表的宠物数据。

打开PetRequester.kt并将公钥添加到文件的顶部,就在import语句下面:

private  const  val SERVER_PUBLIC_KEY = “MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEP9M / My4tmNiaZRcQtYj58EjGN8N3uSnW / s7FpTh4Q + T3tNVkwVCjmDN + a2qIRTcedQyde0d8CoG3Lp2ZlnPhcw ==”

接下来,authenticatorretrievePets()方法中创建一个实例,位于定义URL的前三行之下:

val authenticator = Authenticator()

然后,使用以下内容替换uiThread块内的内容:

//验证收到的签名
// 1 
val jsonElement = JsonParser()。parse(json)
 val jsonObject = jsonElement.asJsonObject
 val result = jsonObject。get(“items”)。toString ()
 val resultBytes = result.toByteArray(Charsets.UTF_8)

// 2 
val signature = jsonObject。get(“signature”)。toString()
 val signatureBytes = android.util.Base64.decode(signature,android.util.Base64.DEFAULT)

// 3 
val success = authenticator.verify(signatureBytes,resultBytes,SERVER_PUBLIC_KEY)

// 4 
如果(成功){
   //处理数据
  VAL receivedPets = GSON()。fromJson(JSON,PetResults :: 类。java的)
  responseListener.receivedNewPets(receivedPets)
}

这是更新块中发生的事情:

  1. 您正在获取所有JSON内容items并将其转换为ByteArray
  2. 您还要检索返回的签名字符串,然后将其转换为ByteArray
  3. 现在,您将使用authenticator服务器的公钥来验证带有签名字节的数据字节。
  4. 如果数据已经过验证,则会将其传递给响应侦听器。

构建并运行应用程序以检查它是否有效。if (success) {在行上设置断点以检查successtrue

成功断点

要测试出现问题时会发生什么,您将改变收到的数据。之后添加以下内容val resultBytes = result.toByteArray(Charsets.UTF_8)

resultBytes [resultBytes.size  - 1 ] = 0

该代码将替换接收数据的最后一个字节0。再次构建并运行应用程序。这次,不会显示任何数据,因为success它将是false

成功断点

完成后别忘了删除该测试。

签署请求

另一种常见情况是当您使用后端API连接到服务器时。通常,您需要通过发送公钥来注册,然后才能访问特定端点,例如/ send_messagePublicKey可以通过调用检索一个字节publicKey.encoded。然后,应用程序需要将其请求签名到/ send_message端点才能成功使用它。

签署请求时,通常会采用请求的选定部分,例如HTTP标头,GET或POST参数以及URL,并将它们连接成一个字符串。该字符串用于创建签名。在后端,服务器重复加入字符串并创建签名的过程。如果签名匹配,则证明用户必须拥有私钥。没有其他用户可以冒充用户,因为他们没有该私钥。

由于请求的特定参数是要签名的字符串的一部分,因此它也保证了请求的完整性; 它可以防止攻击者根据自己的喜好改变请求参数。例如,如果攻击者可以更改汇款的目的地帐号,或者能够改变邮寄地址以接收邮件中受害者的信用证,银行就不会高兴。

您将为宠物的请求创建一个简单的签名。回到PetRequester.kt,将以下代码添加到retrievePets()方法中,就在设置authenticator值的行下面:

val bytesToSign = urlString.toByteArray(Charsets.UTF_8)// 1 
val signedData = authenticator.sign(bytesToSign)// 2 
val requestSignature = android.util.Base64.encodeToString(signedData,android.util.Base64.DEFAULT)// 3 
Log.d(“PetRequester”,“请求签名:$ requestSignature ”)

这里:

  1. 您获取请求字符串并将其转换为ByteArray
  2. 使用内部私钥对字节进行签名,并返回签名字节。
  3. 您将签名字节转换为Base64字符串,以便可以通过网络轻松发送。

现在,添加以下行以验证签名是否有效:

val signingSuccess = authenticator.verify(signedData,bytesToSign)
Log.d(“PetRequester”,“成功:$ signingSuccess ”)

构建并运行应用程序以在“ 调试”选项卡中查看结果。

签名请求已验证

现在,您将更改请求数据以查看发生的情况。在authenticator.verify()通话之前添加以下代码:

bytesToSign [bytesToSign.size  - 1 ] = 0

构建并运行应用程序。这一次,successfalseDebug选项卡中。

请求签名失败

恭喜!您刚刚使用签名保护数据。

笑脸

虽然您一直在验证数据的完整性,但它并不能替代常规数据验证检查,例如类型和边界检查。例如,如果您的方法需要128个字符或更少的字符串,您仍应检查此字符串。

您还应该了解其他一些标准:

  • RSA是一种受欢迎且被接受的标准。它的密钥大小必须大得多(例如4096位),密钥生成速度较慢。如果团队的其他成员已经熟悉或使用该标准,您可以使用它。
  • HMAC是另一种流行的解决方案,它不依赖于公共密钥密码术,而是依赖于单个共享密钥。必须安全地交换密钥。当速度考虑非常重要时,使用HMAC。

然后去哪儿?

您刚刚获得了处理敏感医疗数据的应用程序。使用本教程顶部或底部的“下载材料”按钮下载最终项目。

虽然您已确保与服务器的连接,但一旦到达,流量就会被解密。通常,公司需要能够看到这些信息,但最近有一种趋势是端到端加密。解释端到端加密的最佳方法是聊天应用,其中只有发送方和接收方具有解密彼此消息的密钥。该服务或公司无法知道内容是什么。这是避免在服务器端数据泄露或泄露时承担责任的好方法。要了解有关实现此方法的更多信息,一个好的起点是开源Signal App GitHub repo

要了解有关Android安全性的更多信息,请加qq群私聊群主:]

如果您对所涵盖的内容有任何疑问,请加入以下讨论!

+qq群457848807:。获取本文入门项目,以及相关技术的免费视频学习资料

猜你喜欢

转载自blog.csdn.net/weixin_44893918/article/details/89244186