适用于Android的加密教程:保护您的私人用户数据免遭黑客攻击

有没有想过如何使用数据加密来保护您的私人用户数据免遭黑客攻击?不要再看了,在本教程中你会做到这一点!

适用于Android的加密教程:保护您的私人用户数据免遭黑客攻击_第1张图片
科林斯图尔特

由于最近的所有数据泄露和新的隐私法律(例如GDPR),您的应用程序的可信度取决于您管理用户数据的方式。有强大的Android API专注于数据加密,在开始项目时有时会被忽略。您可以充分利用它们并从头开始考虑安全性。

在本教程中,您将获得存储医疗信息的兽医诊所的应用程序。在此过程中,您将学习如何:

收紧应用权限
加密您的数据
使用KeyStore

入门

如要下载入门项目请看文末。花点时间熟悉项目的结构。构建并运行应用程序以查看您正在使用的内容。

你会看到一个简单的注册屏幕。输入密码并选择注册后,系统会在后续应用程序启动时提示您输入密码。在那一步之后,你会得到一份宠物清单。大多数应用程序都是完整的,因此您将专注于保护它。点击列表中的条目以显示宠物的医疗信息:

适用于Android的加密教程:保护您的私人用户数据免遭黑客攻击_第2张图片
宠物详情屏幕

如果在Android 7+上,你会遇到错误java.lang.SecurityException崩溃:不再支持MODE_WORLD_READABLE,请不要担心。你很快就会解决它。

保护基础

要开始加密应用程序并保护重要数据,首先必须防止数据泄漏到世界其他地方。对于Android,这通常意味着保护基于用户的数据不被任何其他应用程序读取,并限制应用程序的安装位置。我们先这样做,这样你就可以开始加密私人信息了。

使用权限

当您第一次开始构建应用程序时,重要的是要考虑您实际需要保留多少用户数据。如今,最好的做法是避免存储私人数据(如果您不需要) - 特别是对于我们可爱的小Lightning,他担心自己的隐私。

从Android 6.0开始,文件和SharedPreferences保存都是使用MODE_PRIVATE常量设置的。这意味着只有您的应用才能访问数据。Android 7不允许任何其他选项。首先,首先要确保安全地设置项目。

打开MainActivity.kt文件。你会发现有两个废弃警告MODE_WORLD_READABLEMODE_WORLD_WRITABLE。这些允许在早期Android版本上公开访问您的文件。找到设置MODE_WORLD_WRITABLE并用以下内容替换它的行:

val preferences = getSharedPreferences(“MyPrefs”,Context.MODE_PRIVATE)

然后,找到设置的行MODE_WORD_READABLE并将其替换为:

val editor = getSharedPreferences(“MyPrefs”,Context.MODE_PRIVATE).edit()

太棒了,你刚刚让你的喜好更安全一点!此外,如果您现在构建并运行该应用程序,则由于安全性违反Android 7+版本,您不应该遇到之前遇到的崩溃。您现在应该为应用安装目录强制实施安全位置。

限制安装目录

Android在过去几年中面临的一个重大问题是没有足够的内存来安装大量应用程序。这主要是由于设备的存储容量较低,但由于技术已经发展,手机变得更便宜,现在大多数设备都为大量应用程序提供了大量存储空间。但是,为了减少存储空间不足,Android允许您将应用程序安装到外部存储。这非常有效,但多年来,围绕这种方法引发了许多安全问题。在外部SD卡上安装应用程序是节省存储空间的一种很酷的方式,但也存在安全漏洞,因为任何能够访问SD卡的人都可以访问应用程序数据。这些数据可能包含敏感信息。这就是为什么鼓励将您的应用限制为内部存储的原因。

为此,请打开AndroidManifest.xml文件,找到读取的行android:installLocation="auto"并将其替换为以下内容:

android:installLocation = “internalOnly”

现在,安装位置仅限于设备,但您仍然可以备份应用及其数据。这意味着用户可以使用adb backup访问应用程序私有数据文件夹的内容。要禁止备份,请找到读取android:allowBackup="true"并替换值的行"false"

遵循这些最佳实践,您已经在某种程度上强化了您的应用程序。但是,您可以在有根设备上绕过这些权限措施。解决方案是使用潜在攻击者无法找到的一条信息对数据进行加密。

使用密码保护用户数据

适用于Android的加密教程:保护您的私人用户数据免遭黑客攻击_第3张图片
设备锁定屏幕

您将使用众所周知的推荐标准高级加密标准(AES)对数据进行加密。AES使用替换置换网络使用密钥加密您的数据。使用这种方法,它将一个表中的字节替换为另一个表中的字节,并因此创建数据的排列。要开始使用AES,您必须首先创建加密密钥,所以让我们这样做。

创建密钥

如上所述,AES使用密钥进行加密。该密钥也用于解密数据。这称为对称加密。密钥可以是不同的长度,但256位是标准的。直接使用用户的密码进行加密是很危险的。它可能不会随机或足够大。因此,用户密码与加密密钥不同。

一个名为基于密码的密钥派生函数(PBKDF2)的功能来拯救。它需要一个密码,并且通过多次对随机数据进行散列,它会创建一个密钥。随机数据称为salt。这会创建一个强大且唯一的密钥,即使其他人使用相同的密码也是如此。

适用于Android的加密教程:保护您的私人用户数据免遭黑客攻击_第4张图片
PBKDF2图

由于每个密钥都是唯一的,如果攻击者窃取并在线发布密钥,则不会公开使用相同密码的所有用户。

首先生成盐。打开Encryption.kt文件,并将以下代码添加到第一个encrypt方法,其中包含//TODO: Add code here

val random = SecureRandom()
 val salt = ByteArray(256)
random.nextBytes(盐)

在这里,您使用SecureRandom类,这确保输出难以预测。这被称为加密强大的随机数生成器

现在,您将使用用户的密码和salt生成密钥。在刚刚添加的代码下添加以下内容:

VAL pbKeySpec = PBEKeySpec(密码,盐,1324,256)// 1 
VAL secretKeyFactory = SecretKeyFactory.getInstance(“PBKDF2WithHmacSHA1” )// 2 
VAL keyBytes = secretKeyFactory.generateSecret(pbKeySpec).encoded // 3 
VAL keySpec = SecretKeySpec(keyBytes ,“AES”)// 4

这是代码内部的内容。您:

  1. 将salt和密码放入PBEKeySpec基于密码的加密对象中。构造函数采用迭代计数(1324)。数字越大,在暴力攻击期间操作一组键所需的时间越长。
  2. 通过PBEKeySpec进入SecretKeyFactory
  3. 生成密钥为ByteArray
  4. 将原始包装ByteArray成一个SecretKeySpec物体。

注意:对于密码,大多数这些函数都使用CharArray而不是String对象。这是因为对象String是不可变的。A CharArray可以被覆盖,允许您在完成后从内存中删除敏感信息。

添加初始化向量

您几乎已准备好加密数据,但还有一件事要做。

AES工作在不同的模式。标准模式称为密码块链接(CBC)。CBC一次加密一个数据块。

CBC是安全的,因为管道中的每个数据块与它加密的前一个块是异或的。这种对先前块的依赖使加密变得强大,但是你能看到问题吗?第一块怎么样?

如果您对与另一条消息相同的消息进行加密,则第一个加密块将是相同的!这为攻击者提供了线索。要解决此问题,您将使用初始化向量(IV)

对于与第一个块进行异或的随机数据块,IV是一个奇特的术语。请记住,每个块都依赖于此前处理的所有块。这意味着使用相同密钥加密的相同数据集将不会产生相同的输出。

现在通过在刚刚添加的代码之后添加以下代码来创建IV:

val ivRandom = SecureRandom()//不缓存以前的种子SecureRandom实例
// 1 
val iv = ByteArray(16)
ivRandom.nextBytes(ⅳ)
val ivSpec = IvParameterSpec(iv)// 2

在这里,您:

  1. 创建了16个字节的随机数据。
  2. 将其打包到IvParameterSpec对象中。

注意:在Android 4.3及更低版本中SecureRandom存在漏洞。它与底层伪随机数发生器(PRNG)的不正确初始化有关。如果您支持Android 4.3及更低版本,则可以使用此修复程序。

加密数据

适用于Android的加密教程:保护您的私人用户数据免遭黑客攻击_第5张图片
加密图标

现在您已拥有所有必需的部分,请添加以下代码以执行加密:

val cipher = Cipher.getInstance(“AES / CBC / PKCS7Padding”)// 1 
密码。init(Cipher.ENCRYPT_MODE,keySpec,ivSpec)
 val encrypted = cipher.doFinal(dataToEncrypt)// 2

这里:

  1. 您传入了规范字符串“AES / CBC / PKCS7Padding”。它使用密码块链接模式选择AES。PKCS7Padding是一个众所周知的填充标准。由于您正在使用块,并非所有数据都完全适合块大小,因此您需要填充剩余空间。顺便说一句,块长128位,AES在加密前添加填充。
  2. doFinal 实际加密。

接下来,添加以下内容:

  map [ “salt” ] =盐
  map [ “iv” ] = iv
  map [ “encrypted” ] =加密

您将加密数据打包成一个HashMap。您还将salt和初始化向量添加到地图中。那是因为所有这些部分都是解密数据所必需的。

如果您正确地执行了这些步骤,则不应该有任何错误,并且该encrypt函数已准备好保护某些数据!存储盐和IV是可以的,但重复使用或顺序递增它们会削弱安全性。但你永远不应该存储钥匙!就在现在,你构建了加密数据的方法,但是为了稍后阅读它,你仍然需要解密它。让我们看看如何做到这一点。

解密数据

适用于Android的加密教程:保护您的私人用户数据免遭黑客攻击_第6张图片
解密图标

现在,您已经获得了一些加密数据。为了解密,你必须改变的模式Cipherinit从方法ENCRYPT_MODEDECRYPT_MODE。将以下内容添加到Encryption.kt文件中的decrypt方法,该行右侧的行如下所示://TODO: Add code here

// 1 
val salt = map [ “salt” ]
 val iv = map [ “iv” ]
 val encrypted = map [ “encrypted” ]

// 2 
//再生从密码密钥
VAL pbKeySpec = PBEKeySpec(密码,盐,1324,256)
 VAL secretKeyFactory = SecretKeyFactory.getInstance(“PBKDF2WithHmacSHA1” )
 VAL keyBytes = secretKeyFactory.generateSecret(pbKeySpec).encoded
 VAL keySpec = SecretKeySpec(keyBytes ,“AES”)

// 3 
// Decrypt 
val cipher = Cipher.getInstance(“AES / CBC / PKCS7Padding”)
 val ivSpec = IvParameterSpec(iv)
密码。init(Cipher.DECRYPT_MODE,keySpec,ivSpec)
decrypted = cipher.doFinal(加密)

在此代码中,您执行了以下操作:

  1. 使用包含解密所需的加密数据,salt和IV 的HashMap
  2. 根据信息加上用户密码重新生成密钥。
  3. 解密数据并将其作为ByteArray返回。

请注意您是如何使用相同的配置进行解密的,但已追溯到您的步骤。这是因为您使用的是对称加密算法。您现在可以加密数据并解密它!

哦,我提到永远不会存储密钥吗?:]

保存加密数据

现在加密过程已完成,您需要保存该数据。该应用程序已在读取和写入存储数据。您将更新这些方法以使其与加密数据一起使用。

MainActivity.kt文件中,使用以下createDataSource方法替换方法内的所有内容:

val inputStream = applicationContext.assets。open(filename)
 val bytes = inputStream.readBytes()
inputStream.close()

val password = CharArray(login_password.length())
login_password.text.getChars(0,login_password.length(),password,0)
 val map = Encryption()。encrypt(bytes,password)
ObjectOutputStream(FileOutputStream(outFile))。use {
  它 - > it.writeObject(map)
}

在更新的代码中,您将数据文件作为输入流打开,并将数据提供给加密方法。您HashMap使用ObjectOutputStream该类序列化,然后将其保存到存储中。

构建并运行应用程序。请注意,列表中现在缺少宠物:

适用于Android的加密教程:保护您的私人用户数据免遭黑客攻击_第7张图片
空列表
适用于Android的加密教程:保护您的私人用户数据免遭黑客攻击_第8张图片
按键

那是因为保存的数据是加密的。您需要更新代码才能读取加密内容。在PetViewModel.kt文件的loadPets方法中,删除和注释标记。然后,将以下代码添加到其读取的位置:/*``*/``//TODO: Add decrypt call here

decrypted = Encryption()。decrypt(
    hashMapOf(“iv”到iv,“salt”到salt,“加密”到加密),密码)

decrypt使用加密数据IV和salt 调用了该方法。现在输入流来自一个ByteArray而不是File,替换val inputStream = file.inputStream()用这个读取的行:

val inputStream = ByteArrayInputStream(解密)

如果你现在构建并运行应用程序,你应该看到几个友好的面孔!

适用于Android的加密教程:保护您的私人用户数据免遭黑客攻击_第9张图片
image

数据现在是安全的,但是用户数据存储在Android上的另一个常见位置是SharedPreferences,您已经使用过它。

保护SharedPreferences

该应用程序还会跟踪上次访问时间SharedPreferences,因此它是应用程序中的另一个要保护的位置。在敏感信息中存储SharedPreferences可能是不安全的,因为即使使用Context.MODE_PRIVATE标记,您仍然可以从应用程序中泄漏信息。你会稍微解决这个问题。

打开MainActivity.kt文件,并saveLastLoggedInTime用以下代码替换该方法:

//获取密码
val password = CharArray(login_password.length())
login_password.text.getChars(0,login_password.length(),password,0)

// Base64数据
val currentDateTimeString = DateFormat.getDateTimeInstance()。format(Date())
 // 1 
val map =
    加密()。encrypt(currentDateTimeString.toByteArray(Charsets.UTF_8),密码)
// 2 
val valueBase64String = Base64.encodeToString(map [ “encrypted” ],Base64.NO_WRAP)
 val saltBase64String = Base64.encodeToString(map [ “salt” ],Base64.NO_WRAP)
 val ivBase64String = Base64.encodeToString(map [ “iv “ ],Base64.NO_WRAP)

//保存到共享的首选项
val editor = getSharedPreferences(“MyPrefs”,Context.MODE_PRIVATE).edit()
 // 3 
editor.putString(“l”,valueBase64String)
editor.putString(“lsalt”,saltBase64String)
editor.putString(“liv”,ivBase64String)
editor.apply()

在这里,您:

  1. 使用UTF-8编码转换String为a 并加密它。在前面的代码中,您将文件打开为二进制文件,但在使用字符串时,您需要考虑字符编码。ByteArray
  2. 将原始数据转换为String表示形式。SharedPreferences不能ByteArray直接存储,但它可以使用String。Base64是将原始数据转换为字符串表示的标准。
  3. 将字符串保存到SharedPreferences。您可以选择加密首选项键和值。这样,攻击者无法通过查看密钥来弄清楚值是什么,并且使用“密码”之类的密钥不适用于暴力破解,因为它也会被加密。

现在,替换该lastLoggedIn方法以获取加密的字节:

//获取密码
val password = CharArray(login_password.length())
login_password.text.getChars(0,login_password.length(),password,0)

//检索共享首选项数据
// 1个
VAL偏好= getSharedPreferences(“MyPrefs”,Context.MODE_PRIVATE)
 VAL base64Encrypted = preferences.getString(“L” , “” )
 VAL base64Salt = preferences.getString(“lsalt” , “” )
 val base64Iv = preferences.getString(“liv”,“”)

// Base64 decode 
// 2 
val encrypted = Base64.decode(base64Encrypted,Base64.NO_WRAP)
 val iv = Base64.decode(base64Iv,Base64.NO_WRAP)
 val salt = Base64.decode(base64Salt,Base64.NO_WRAP)

// Decrypt 
// 3 
val decrypted = Encryption()。decrypt(
    hashMapOf(“iv”到iv,“salt”到salt,“加密”到加密),密码)

var lastLoggedIn:String?= null
解密?.let {
  lastLoggedIn = String(它,Charsets.UTF_8)
}
返回 lastLoggedIn

你做了以下事情:

  1. 检索加密数据,IV和salt的字符串表示。
  2. 对字符串应用Base64解码,将它们转换回原始字节。
  3. 将该数据传递HashMapdecrypt方法。

现在你已经存储设置安全,开始通过导航到新的设置应用PetMed 2存储清除数据

构建并运行应用程序。如果一切正常,登录后您应该再次看到宠物回到屏幕上。Esther很高兴她的私人数据已加密。:]

适用于Android的加密教程:保护您的私人用户数据免遭黑客攻击_第10张图片
宠物清单屏幕

使用服务器的密钥

您刚刚完成了一个很棒的真实示例,但很多应用都需要良好的入职体验。除登录屏幕外,显示密码屏幕可能不是一个好的用户体验。对于这样的要求,您有一些选择。

第一种是利用登录密码来获得密钥。您也可以让服务器生成该密钥。密钥将是唯一的,并在用户使用其凭据进行身份验证后安全地传输。

如果您要使用服务器路由,重要的是要知道由于服务器生成密钥,因此它具有解密存储在设备上的数据的能力。有人可能泄漏密钥。

如果这些解决方案都不适合您,您可以利用设备安全性来保护应用程序。

使用KeyStore

Android M引入了使用KeyStore API处理 AES密钥的功能。这有一些额外的好处。KeyStore允许您在不泄露秘密内容的情况下操作密钥。只能从应用程序空间访问对象而不是私有数据。

生成新的随机密钥

适用于Android的加密教程:保护您的私人用户数据免遭黑客攻击_第11张图片
按键

Encryption.kt文件中,将以下代码添加到keystoreTest方法以生成随机密钥。这一次,KeyStore保护密钥:

val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES,“AndroidKeyStore”)// 1 
val keyGenParameterSpec = KeyGenParameterSpec.Builder(“MyKeyAlias”,
    KeyProperties.PURPOSE_ENCRYPT或KeyProperties.PURPOSE_DECRYPT)
    .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
    //.setUserAuthenticationRequired(true)// 2需要锁定屏幕,如果锁定屏幕被禁用
    则无效//.setUserAuthenticationValidityDurationSeconds(120)// 3仅在密码验证时可用x秒。-1需要指纹 - 每次 
    .setRandomizedEncryptionRequired(true)//每次调用时相同明文的4个不同密文
    。建立()
的KeyGenerator。init(keyGenParameterSpec)
keyGenerator.generateKey()

这是代码内部发生的事情:

  1. 您创建了一个KeyGenerator实例并将其设置为“AndroidKeyStore”提供程序。
  2. 或者,您添加了.setUserAuthenticationRequired(true)需要设置锁定屏幕的功能。
  3. 您可以选择添加,.setUserAuthenticationValidityDurationSeconds(120)以便在设备身份验证后120秒可以使用该密钥。
  4. 你打过电话.setRandomizedEncryptionRequired(true)。这告诉KeyStore每次都使用新的IV。如前所述,这意味着如果您再次加密相同的数据,加密的输出将不相同。它可以防止攻击者根据相同的输入信息获取有关加密数据的线索。

您应该了解的KeyStore选项还有一些其他内容:

  1. 对于.setUserAuthenticationValidityDurationSeconds(),您可以在每次要访问密钥时传递-1以要求指纹身份验证。
  2. 一旦用户移除或更改锁定屏幕引脚或密码,启用屏幕锁定要求将撤消密钥。
  3. 将密钥存储在与加密数据相同的位置就像在门垫下放一把钥匙。KeyStore尝试使用严格的权限和内核级代码来保护密钥。在某些设备上,密钥是硬件支持的。
  4. 你可以用.setUserAuthenticationValidWhileOnBody(boolean remainsValid)。这使得一旦设备检测到密钥不再在该人身上,该密钥就不可用。

加密数据

现在,您将使用存储在KeyStore中的密钥。在Encryption.kt文件中,将以下内容添加到keystoreEncrypt方法中//TODO: Add code here

// 1 
//获取密钥
val keyStore = KeyStore.getInstance(“AndroidKeyStore”)
keyStore.load(null)

val secretKeyEntry =
    keyStore.getEntry(“MyKeyAlias”,null)as KeyStore.SecretKeyEntry
 val secretKey = secretKeyEntry.secretKey

// 2 
//加密数据
val cipher = Cipher.getInstance(“AES / GCM / NoPadding”)
密码。init(Cipher.ENCRYPT_MODE,secretKey)
 val ivBytes = cipher.iv
 val encryptedBytes = cipher.doFinal(dataToEncrypt)

// 3 
map [ “iv” ] = ivBytes
map [ “encrypted” ] = encryptedBytes

这里:

  1. 这次,您从KeyStore中检索密钥。
  2. 您使用该Cipher对象加密了数据SecretKey
  3. 像以前一样,您返回一个HashMap包含解密数据所需的加密数据和IV。

解密为字节数组

将以下内容添加到keystoreDecrypt方法中,右下方//TODO: Add code here

// 1 
//获取密钥
val keyStore = KeyStore.getInstance(“AndroidKeyStore”)
keyStore.load(null)

val secretKeyEntry =
    keyStore.getEntry(“MyKeyAlias”,null)as KeyStore.SecretKeyEntry
 val secretKey = secretKeyEntry.secretKey

// 2 
//从地图中提取信息
val encryptedBytes = map [ “encrypted” ]
 val ivBytes = map [ “iv” ]

// 3 
//解密数据
val cipher = Cipher.getInstance(“AES / GCM / NoPadding”)
 val spec = GCMParameterSpec(128,ivBytes)
密码。init(Cipher.DECRYPT_MODE,secretKey,spec)
decrypted = cipher.doFinal(encryptedBytes)

在此代码中,您:

  1. 从KeyStore再次获得密钥。
  2. 从中提取必要的信息map
  3. Cipher使用DECRYPT_MODE常量初始化对象并将数据解密为a ByteArray

测试示例

现在您已经创建了使用KeyStore API加密和解密数据的方法,现在是时候测试它们了。将以下内容添加到keystoreTest方法的末尾:

// 1 
val map = keystoreEncrypt(“我非常敏感的字符串!”。 toByteArray(Charsets.UTF_8))// 2 
val decryptedBytes
 = keystoreDecrypt(map)
decryptedBytes?.let {
  val decryptedString = String(it,Charsets.UTF_8)
  Log.e(“MyApp”,“解密的字符串是:$ decryptedString ”)
}

在更新的代码中,您:

  1. 创建了一个测试string并对其进行了加密。
  2. 调用decrypt加密输出上的方法来测试一切是否正常。

MainActivity.kt文件的onCreate方法中,取消注释读取的行。构建并运行应用程序以检查它是否有效。您应该看到解密的字符串://Encryption().keystoreTest()

适用于Android的加密教程:保护您的私人用户数据免遭黑客攻击_第12张图片
控制台成功输出

然后去哪儿?

恭喜,您已经学会了在Android上加密和解密数据的方法!


适用于Android的加密教程:保护您的私人用户数据免遭黑客攻击_第13张图片
快乐的数据

您还学习了使用Keystore处理密钥的其他方法。您可以使用本教程顶部或底部的“ 下载材料”按钮下载最终项目,如果您跳过某些步骤,则可以使用完整的项目并填写所有代码。

很高兴知道如何正确实现安全性。有了这些知识,您将能够确认第三方安全库是否符合最佳实践。然而,自己实施它,特别是如果匆忙,可能会导致错误。如果您在该船上,请考虑使用经过行业认可或经过时间考验的第三方。

Conceal是第三方加密库的绝佳选择。它可以让您启动并运行,而无需担心底层细节。一个缺点 - 当黑客暴露流行的图书馆中的漏洞时。这会影响同时依赖该第三方的所有应用程序。具有自定义实现的应用程序通常不受广泛的脚本攻击的影响。

客户经理是Android操作系统的一部分,并拥有相应的API。它是用户帐户凭据的集中管理器,因此您的应用程序不必直接存储或使用密码和登录。最着名的例子是请求* OAuth2令牌*。

Keychain API在Android 4.0(API Level 14)中引入,处理密钥管理。它专门用于PrivateKeyX509Certificate对象,并提供比使用应用程序的数据存储更安全的容器。您可以使用它来安装证书并直接使用私钥对象。

只要有人不篡改您的应用程序,您的安全代码就可以很好地保护您的应用程序。查看ProGuard教程,了解如何帮助防止逆向工程或篡改与安全相关的代码。

现在您已经保护了数据,为什么不了解如何保护传输中的数据?对于那些寻找高级加密技术的人,请查看AES的GCM模式。

顺便说一句,样本数据字符Esther,Cornelius,Lightning和Birgit都是真实的!:]

如果您对所学内容有任何疑问或意见,请加入以下QQ群讨论!

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

你可能感兴趣的:(适用于Android的加密教程:保护您的私人用户数据免遭黑客攻击)